diff --git a/YLErpDAL/Modules/SwapModule/Accrual/AccrualPolicy.cs b/YLErpDAL/Modules/SwapModule/Accrual/AccrualPolicy.cs new file mode 100644 index 00000000..7215b662 --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/Accrual/AccrualPolicy.cs @@ -0,0 +1,49 @@ +using YLErp.Derivatives.Interest; +using YLErp.DBModels; + +namespace YLErp.Modules.SwapModule.Accrual; + +/// +/// 计息政策(不可变配置)。把"算头算尾 / 单复利率 / 重置频率 / 年化天数"收敛为一处, +/// 取代旧代码里散落各处的 calcFirst/calcLast 布尔对与魔法数字。 +/// +/// 单/复利不再另立枚举——直接复用既有 DB 枚举 (单利=0 / 复利=1), +/// 通过 暴露为类型安全的 bool,避免与 SwapInterest 已有的 +/// AccrueSimple/AccrueCompound 方法分裂出"第三种单复利表达"(这是第一版草稿犯过的重复)。 +/// +public sealed class AccrualPolicy +{ + /// 算头算尾约定(复用 SwapInterest 已有的 AccrualBoundary,物理上杜绝 calcFirst/calcLast 传反)。 + public AccrualBoundary Convention { get; } + + /// 是否复利(利滚利)。来自 DB 的 InterestTypeEnum;单利=false,复利=true。 + public bool IsCompound { get; } + + /// 利率重置周期(天)。FR007 通常为 7;复利时亦为"利息并入本金"的周期。 + public int ResetPeriodDays { get; } + + /// 年化基数(365 / 360)。 + public int AnnualDays { get; } + + /// 是否年化(position.IsAnnualized)。决定利息是否再除以 ; + /// 与 一同收敛 daycount 语义,不再作为裸 bool 散落在计息签名里。 + public bool IsAnnualized { get; } + + public AccrualPolicy(AccrualBoundary convention, bool isCompound, int resetPeriodDays, int annualDays, bool isAnnualized = false) + => (Convention, IsCompound, ResetPeriodDays, AnnualDays, IsAnnualized) = (convention, isCompound, resetPeriodDays, annualDays, isAnnualized); + + /// + /// 从交易扩展解析(边界适配)。调用方负责从 trade_extend.ExtendObj 取出 + /// InterestCalcMode / AnnualDays / interest_rest_days 与计息方式后传入; + /// 这里只做"布尔对 → AccrualBoundary"与"InterestTypeEnum → bool"的归一。 + /// + /// 算头(InterestCalcMode 首位为 '1')。 + /// 算尾(InterestCalcMode 末位为 '1')。 + /// 年化天数(ExtendObj.AnnualDays)。 + /// DB 计息方式枚举(单利 / 复利)。 + /// 重置周期天数(interest_rest_days,缺省 1)。 + public static AccrualPolicy FromLegacy(bool includeStart, bool includeEnd, int annualDays, InterestTypeEnum interestType, int resetPeriodDays, bool isAnnualized = false) + => new(AccrualBoundary.Of(includeStart, includeEnd), + interestType == InterestTypeEnum.复利, + resetPeriodDays, annualDays, isAnnualized); +} diff --git a/YLErpDAL/Modules/SwapModule/Accrual/AccrualState.cs b/YLErpDAL/Modules/SwapModule/Accrual/AccrualState.cs new file mode 100644 index 00000000..3ba2c6fb --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/Accrual/AccrualState.cs @@ -0,0 +1,49 @@ +using YLErp.DBModels; + +namespace YLErp.Modules.SwapModule.Accrual; + +/// +/// 融资腿逐日计息的跨日状态(不可变值对象)。 +/// 这是"待实现利息"在日间滚动的快照,区别于已落库的 swap_flow_event。 +/// +/// 旧字段 → 领域命名映射(DB 列不可改,仅在边界处适配;本类内部一律用下列自描述名): +/// +/// TdInterestPrincipal逐日滚动的计息本金 → +/// InterestIncomeSum累计待实现利息 → +/// consumedInterest历史已实现利息(legacy) → +/// ValueDate快照截至日 → (EOD 续接起算日,Bug C / 5-11 跳过需据此判断从哪天接续)。 +/// +/// +public readonly struct AccrualState +{ + /// 用于计算当日利息的计息本金。单利=名义本金基数;复利=本金+累计利息。 + public decimal AccrualPrincipal { get; } + + /// 累计待实现(未平仓)利息。 + public decimal UnrealizedInterest { get; } + + /// 历史各次平仓已确认的已实现利息,从剩余待实现中扣除。 + public decimal RealizedInterest { get; } + + /// 快照截至日(来自 eod_swap_position.ValueDate)。编排层据此判断计息区间起点,避免 5-11 等"跳过日"误重算。 + public DateTime ValueDate { get; } + + public AccrualState(decimal accrualPrincipal, decimal unrealizedInterest, decimal realizedInterest, DateTime valueDate) + => (AccrualPrincipal, UnrealizedInterest, RealizedInterest, ValueDate) = (accrualPrincipal, unrealizedInterest, realizedInterest, valueDate); + + /// 向后兼容:未携带快照日期时(如纯内存构造)用默认日。 + public AccrualState(decimal accrualPrincipal, decimal unrealizedInterest, decimal realizedInterest) + : this(accrualPrincipal, unrealizedInterest, realizedInterest, default) { } + + /// 空状态(新开仓首个计息日之前)。 + public static readonly AccrualState Zero = new(0m, 0m, 0m); + + /// + /// 从上一日日终归档 适配(边界适配:DB 列名 → 领域名)。 + /// 仅映射计息状态;名义本金基数 / 平仓比例 / 已实现利息等由调用方另行传入。 + /// + public static AccrualState FromPreviousEod(eod_swap_position previousEod) + => previousEod == null || previousEod.id == 0 + ? Zero + : new AccrualState(previousEod.TdInterestPrincipal, previousEod.InterestIncomeSum, 0m, previousEod.ValueDate); +} diff --git a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs new file mode 100644 index 00000000..8c5902ca --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegAccrual.cs @@ -0,0 +1,184 @@ +using System; +using YLErp.Derivatives.Interest; + +namespace YLErp.Modules.SwapModule.Accrual; + +/// +/// 融资腿计息编排层(NEW,替换 SwapDealService 内 CalcDaily* 家族的"纯数学"部分)。 +/// +/// 职责边界(与 SwapInterest 原语、SwapDealService 适配器三者正交): +/// + /// 本类:持有跨日 ,逐日循环,经 IIndexFixer 取当日率并封装为 , + /// 调用 SwapInterest 原子原语算账,处理平仓缩放 / 已实现扣除,replay 求 Δ。 +/// SwapInterest:原子 "本金×利率×天数/年化" 纯函数,无状态。 +/// SwapDealService.GetInterests:仅做 DB 读、swap_flow_event 构造与落库(IO)。 +/// +/// +/// 命名规范:本文件内所有概念一律使用自描述英文名。旧代码 typo 一律不出现: +/// +/// closePrecent 的 closeRatio(且语义固定为"占剩余持仓比例") +/// orginPv→ OriginalPv(原始名义本金,用于保证金腿差分基数) +/// floateRate→ FloatRate(FR007 浮动利率,拼写修正) +/// dynomicPrincipal→ AccrualPrincipal(逐日滚动计息本金) +/// +/// +/// 重要——以下 当前为"结构骨架": +/// 精确的跨日不变量(T+1 本金缩到剩余、重置日本金保留、consumedInterest 扣除、保证金腿差分公式) +/// 依 branch-merge-analysis §7 的 DI_* 不变量,必须在把 _0808(人工已过)的利息核心并入后, +/// 从 CalcDaily* 迁移而来并以 Excel oracle 验收。骨架故意只跑"朴素逐日累加", +/// 不等价于已验证口径——切勿在未迁移前接入生产。 +/// +public static class FundingLegAccrual +{ + // 中性锚点日期:AccrueDay 只需"同日起止 → 1 天"的语义,不依赖真实时钟,保持纯函数。 + private static readonly DateTime Epoch = new(2000, 1, 1); + + // 资金腿计息精度(生产口径)。与 SwapDealService.InterestCalculationPrecision 一致。 + // 注意:SwapInterest.Precision=11 仅服务于保证金腿(MarginAccount),与资金腿 12 不一致属已知 TODO; + // 资金腿所有落库/对账均以 12 为准,此处显式锁定,避免静默引入尾差。 + private const int ProductionPrecision = 12; + + /// + /// 单日原子计息("AccrueDay")。委托 SwapInterest.AccrueSimple(同一天、Convention 边界 → 1 天), + /// 直接复用 SwapInterest 已有的 (Accrued/AccruedToday)作为返回, + /// 不再另立结果类型。编排层按日把"当天本金 + 当天率"喂入此入口,使浮动利率逐日不同也能正确累积。 + /// 复利时收盘本金 = 当日本金 + 当日利息(利滚利),该推导在 内完成, + /// 不塞进返回结构,保持与 SwapInterest 单一结果类型的契约一致。 + /// + /// 当日计息本金(来自 )。 + /// 当日生效年利率(由调用方经 IIndexFixer + 合约利差构造后传入,已含 spread)。 + /// 计息政策(含算头算尾 / 年化天数 / 单复利)。 + public static InterestResult AccrueDay( + decimal accrualPrincipal, + decimal dailyRate, + AccrualPolicy policy, + int precision = SwapInterest.Precision) + { + // 单日计息:start == end,按 policy.Convention 决定首日是否计;Both 时恰 1 天。 + return SwapInterest.AccrueSimple(accrualPrincipal, dailyRate, Epoch, Epoch, policy.Convention, policy.AnnualDays, precision); + } + + /// + /// 单利日终计息(纯函数,替换 SwapDealService.CalcDailySimpleInterestByEod 的"纯数学"部分)。 + /// + /// 口径与旧实现逐字对齐(仅命名 DDD 化): + /// + /// 计息基数 baseTdInterestPrincipal = priorAccrualPrincipal + positionPrincipal − originalPv; + /// 当日利息 = baseTdInterestPrincipal × closeRatio × rate.AllInRate,年化则再 ÷ AnnualDays; + /// 累计未实现 = priorUnrealized + 当日利息;末位按资金腿精度 12 舍入。 + /// + /// + /// 取率与重置日重取浮动利率由适配器(CalcDailySimpleInterestByEod)负责,并封装为 传入; + /// daycount 语义(年化 / 年化天数)由 提供。本方法保持纯函数、可独立单测,不连库、不取价。 + /// + /// 上一日日终累计未实现利息(preEod.InterestProfitSum)。 + /// 上一日日终计息本金(preEod.TdInterestPrincipal)。 + /// 存量名义本金(posiPrincipal)。 + /// 平仓比例(closePercent,EOD 恒为 1)。 + /// 原始名义本金(orginPv),用于保证金腿差分基数。 + /// 当日生效利率(已由适配器按腿型封装:固定腿=FixedRate,浮动腿=Spread+IndexFixing)。 + /// 计息政策(daycount:是否年化 / 年化天数)。 + /// :Accrued=累计未实现(对应 InterestAmount),AccruedToday=当日利息(对应 TdInterestAmount)。 + public static InterestResult AccrueSimpleEod( + decimal priorUnrealized, + decimal priorAccrualPrincipal, + decimal positionPrincipal, + decimal closeRatio, + decimal originalPv, + FundingLegRate rate, + AccrualPolicy policy) + { + var baseTdInterestPrincipal = priorAccrualPrincipal + positionPrincipal - originalPv; + var baseInterestPrincipal = baseTdInterestPrincipal * closeRatio; + + var combinedRate = rate.AllInRate; + var dayInterest = baseInterestPrincipal * combinedRate; + var tdInterest = baseTdInterestPrincipal * combinedRate; + if (policy.IsAnnualized) + { + dayInterest /= policy.AnnualDays; + tdInterest /= policy.AnnualDays; + } + + var totalUnrealized = priorUnrealized + dayInterest; + return new InterestResult( + Math.Round(totalUnrealized, ProductionPrecision, MidpointRounding.AwayFromZero), + Math.Round(tdInterest, ProductionPrecision, MidpointRounding.AwayFromZero)); + } + + /// + /// 逐日计息编排(骨架)。从 逐日循环, + /// 推进 ,并返回期末状态。 + /// + /// 期初状态(通常来自上一日日终,见 )。 + /// 计息区间起点。 + /// 计息区间终点。 + /// 取计息日生效年利率的委托(适配器处由 IIndexFixer + 合约利差构造;测试可传 day => 0.0134m)。 + /// 计息政策。 + /// 可选平仓比例(占剩余,0~1)。非空则在期末应用平仓缩放。 + public static AccrualState AccruePeriod( + AccrualState opening, + DateTime startDate, + DateTime endDate, + Func dailyRate, + AccrualPolicy policy, + decimal? closeRatio = null) + { + var state = opening; + + for (var day = startDate.Date; day <= endDate.Date; day = day.AddDays(1)) + { + var rate = dailyRate(day); + var dayResult = AccrueDay(state.AccrualPrincipal, rate, policy); + + // 收盘本金:复利时并入当日利息(利滚利),单利时维持原基数。 + var nextPrincipal = state.AccrualPrincipal + (policy.IsCompound ? dayResult.AccruedToday : 0m); + var nextUnrealized = state.UnrealizedInterest + dayResult.AccruedToday; + state = new AccrualState(nextPrincipal, nextUnrealized, state.RealizedInterest); + + // ── PORT(合并 _0808 后从 CalcDaily* 迁移,受 DI_* 不变量 + Excel oracle 验收)── + // 1. 重置日(距 StartDate 每 resetPeriodDays 天):复利时累计利息并入本金(已在 ClosingAccrualPrincipal 体现); + // 但"重置日部分平仓后 EOD 本金必须保留计算出的复利本金、不得被 closeRatio 二次缩放"。 + // 2. 部分平仓次日(T+1):AccrualPrincipal 必须缩到剩余(× (1-closeRatio)); + // 重置日 / 算尾日不得二次缩放,否则剩余本金被打折。 + // 3. consumedInterest(历史已实现)在平仓日经 SwapInterest.ApplyUnwind 扣除。 + // 4. 保证金腿差分基数 OriginalPv:dynomicPrincipal = AccrualPrincipal + PositionPrincipal - OriginalPv,此处分支处理。 + // 5. replay 求 Δ:本 EOD 状态 − 上一 EOD 状态,由调用方(适配器层)负责,不在纯数学内。 + } + + if (closeRatio.HasValue) + { + state = ApplyPartialClose(state, closeRatio.Value, state.RealizedInterest, policy); + } + + return state; + } + + /// + /// 部分 / 全部平仓缩放:把剩余待实现按 (1-closeRatio) 保留,并扣除历史已实现; + /// 计息本金同步缩到剩余持仓。委托 SwapInterest.ApplyUnwind / AccrueUnrealized(与 Margin 共用同一纯函数)。 + /// + /// 平仓前状态(未实现部分)。 + /// 平仓比例(占剩余持仓,0~1;1=全平)。 + /// 历史已实现利息累计,从剩余未实现中扣除。 + /// 计息政策(精度取 SwapInterest.Precision)。 + public static AccrualState ApplyPartialClose( + AccrualState state, + decimal closeRatio, + decimal realizedInterest, + AccrualPolicy policy) + { + // 待实现利息缩放:unrealized × (1-closeRatio) − realizedInterest + var scaled = SwapInterest.ApplyUnwind( + new InterestResult(state.UnrealizedInterest, state.UnrealizedInterest), + closeRatio, realizedInterest, SwapInterest.Precision); + + // 计息本金按剩余持仓缩放(全平 closeRatio=1 → 归零) + var principalRemaining = Math.Round( + state.AccrualPrincipal * (1m - closeRatio), + SwapInterest.Precision, + MidpointRounding.AwayFromZero); + + return new AccrualState(principalRemaining, scaled.Accrued, realizedInterest); + } +} diff --git a/YLErpDAL/Modules/SwapModule/Accrual/FundingLegRate.cs b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegRate.cs new file mode 100644 index 00000000..284210a6 --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/Accrual/FundingLegRate.cs @@ -0,0 +1,40 @@ +namespace YLErp.Modules.SwapModule.Accrual; + +/// +/// 融资腿在某一计息日生效的利率(不可变值对象)。 +/// +/// 把"固定利率 / 加点利差 / 浮动指数定盘"三种来源收敛为一个概念,避免把它们作为裸 decimal +/// 散落在计息方法签名里——这正是初版 AccrueSimpleEod 参数膨胀、可读性差的根因。 +/// +/// 固定腿与浮动腿是互斥的两种形态:固定腿只设 (其余为 0); +/// 浮动腿设 + 为 0)。 +/// 对两种形态统一为三者之和。 +/// +/// +/// :固定腿的固定年利率;浮动腿为 0。 +/// :浮动腿在指数之上的加点利差(合约侧);固定腿为 0。 +/// :浮动指数定盘(FR007 等);固定腿为 0。 +/// :计息用的"当日生效年利率"。固定腿取 ;浮动腿取 + (二者互斥,非叠加)。 +/// +/// +public readonly struct FundingLegRate +{ + /// 固定腿的固定年利率;浮动腿为 0。 + public decimal FixedRate { get; } + + /// 浮动腿在指数之上的加点利差(合约侧);固定腿为 0。 + public decimal Spread { get; } + + /// 浮动指数定盘利率(市场侧,FR007 等);固定腿为 0。 + public decimal IndexFixing { get; } + + /// + /// 当日生效年利率。固定腿与浮动腿是互斥的两种利率确定方式,不是可叠加分量: + /// 固定腿取 ;浮动腿取 + 。 + /// 约定:固定腿只设 FixedRate(其余为 0),浮动腿只设 Spread + IndexFixing(FixedRate 为 0)。 + /// + public decimal AllInRate => FixedRate != 0m ? FixedRate : Spread + IndexFixing; + + public FundingLegRate(decimal fixedRate = 0m, decimal spread = 0m, decimal indexFixing = 0m) + => (FixedRate, Spread, IndexFixing) = (fixedRate, spread, indexFixing); +} diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 7cc9f904..ecb68d59 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -7,6 +7,7 @@ using YLErp.Derivatives.Interest; using YLErp.Helpers; using YLErp.Modules.DataProviderModule; using YLErp.Modules.EodModule; +using YLErp.Modules.SwapModule.Accrual; using YLErp.Modules.SwapModule.FundingLegs; using YLErp.Modules.SwapModule.Margin; using YLErp.Modules.SwapModule.ReturnLegs; @@ -1552,49 +1553,54 @@ namespace YLErp.Modules.SwapModule /// public void CalcDailySimpleInterestByEod(eod_swap_position preEodPosition, DateTime endDate, DateTime tradeDate, swap_position position, decimal principal, decimal posiPrincipal, swap_flow_event flowEvent, int annualDays, bool needPrice, decimal floateRate, decimal closePercent, decimal orginPv, ref decimal InterestAmount, ref decimal TdInterestAmount) { - decimal interestProfitSum = preEodPosition.InterestProfitSum; - int interestPeriod = position.interest_rest_days ?? 1; - double floatRate = Convert.ToDouble(floateRate); - var calcDays = (endDate - tradeDate).Days; - // 修复:首次操作时(preEodPosition.id == 0),TdInterestPrincipal 需要正确初始化 + // 首次操作(preEod.id == 0):计息基数按存量本金初始化——保留旧行为(含对 preEod 的就地修正)。 if (preEodPosition.id == 0) { preEodPosition.TdInterestPrincipal = posiPrincipal; } - // 检查是否到达重置周期 - if (calcDays % interestPeriod == 0) + // 取率:重置日按 interest_rule 重新定盘浮动利率(GLMS-JIATT-20260805 根因—— + // 重置日=平仓日必须用新利率,否则沿用旧周期利率并污染后续 EOD)。 + decimal effectiveFloat = floateRate; + int interestPeriod = position.interest_rest_days ?? 1; + if ((endDate - tradeDate).Days % interestPeriod == 0 + && !string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) { - // 获取新的浮动利率 - if (!string.IsNullOrEmpty(position.FloatRateUnderlyingCode)) + var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule); + if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing)) { - var fixingDate = IndexFixerBase.GetFixingDate(endDate, position.interest_rule); - if (IndexFixer.TryGetFixing(fixingDate, position.FloatRateUnderlyingCode, out decimal fixing)) - { - if (fixing != 0m) floatRate = Convert.ToDouble(fixing); - } - else - { - throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格"); - } + if (fixing != 0m) effectiveFloat = fixing; + } + else + { + throw new Exception($"获取不到{position.FloatRateUnderlyingCode}在{fixingDate:yyyy年MM月dd日}的价格"); } } - flowEvent.FloatRate = Convert.ToDecimal(floatRate); - var baseTdInterestPrincipal = preEodPosition.TdInterestPrincipal + posiPrincipal - orginPv; - var baseInterestPrincipal = baseTdInterestPrincipal * closePercent; + flowEvent.FloatRate = effectiveFloat; - // 修复:正确计算本次利息(基于实际持仓本金) - decimal interest = baseInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); - decimal tdinterest = baseTdInterestPrincipal * (flowEvent.InterestRate + Convert.ToDecimal(floatRate)); - if (position.IsAnnualized) - { - interest /= annualDays; - tdinterest /= annualDays; - } - - InterestAmount = Math.Round(interestProfitSum + interest, InterestCalculationPrecision, MidpointRounding.AwayFromZero); - TdInterestAmount = Math.Round(tdinterest, InterestCalculationPrecision, MidpointRounding.AwayFromZero); + // 纯数学下沉至 FundingLegAccrual(DDD 命名 + 末位生产精度 12 舍入),行为与上版逐字对齐。 + // 利率构成按腿型封装:固定腿 → FixedRate;浮动腿 → Spread + IndexFixing(沿用旧实现 InterestRate+浮动利率 的口径)。 + var isFixedLeg = string.IsNullOrEmpty(position.FloatRateUnderlyingCode); + var legRate = isFixedLeg + ? new FundingLegRate(fixedRate: flowEvent.InterestRate) + : new FundingLegRate(spread: flowEvent.InterestRate, indexFixing: effectiveFloat); + var accrualPolicy = new AccrualPolicy( + convention: AccrualBoundary.Both, + isCompound: false, + resetPeriodDays: position.interest_rest_days ?? 1, + annualDays: annualDays, + isAnnualized: position.IsAnnualized); + var result = FundingLegAccrual.AccrueSimpleEod( + priorUnrealized: preEodPosition.InterestProfitSum, + priorAccrualPrincipal: preEodPosition.TdInterestPrincipal, + positionPrincipal: posiPrincipal, + closeRatio: closePercent, + originalPv: orginPv, + rate: legRate, + policy: accrualPolicy); + InterestAmount = result.Accrued; + TdInterestAmount = result.AccruedToday; } /// diff --git a/corp-action-refactor-proposal.md b/corp-action-refactor-proposal.md index 80c6a5d0..b21d529b 100644 --- a/corp-action-refactor-proposal.md +++ b/corp-action-refactor-proposal.md @@ -142,6 +142,49 @@ curretEod.PosiQuantity = qty < 0 ? 0 : Math.Abs(qty); --- +## 4.1 阶段1 落地形态建议:独立 `CorporateActions` 模块(新增建议 · **待深度验证与评审**) + +> ⚠️ 以下为**架构建议草案**,尚未经过逐文件源码复核与评审。仅作方向性参考,落地前须: +> 1. 逐文件确认现有 `DividendService` / `BondPaymentService` / `ex_dividend_info` 的调用边,避免重复造轮子; +> 2. 确认 `YLErp.Core` 是否合适承载(须被 OMS / 期权 / TRS 多程序集引用,不能反向依赖业务层); +> 3. 与**利息核心(复利/部分平仓)**明确划界——见下方"边界警示"。 + +### 4.1.1 为什么必须新模块,而不是往现有屎山堆 + +- 现有 `SwapDealService` / `SwapEodPositionService` 已高度耦合(计息、平仓、EOD 递推、公司行为全搅在一起),继续往里加 if/else 只会放大"隐式不变量跨函数跨日不可见"的风险(这正是"测试绿但全错"的温床)。 +- 公司行为域(除权除息 / 复权 / 付息 / 分红 / 拆股 / 送股 / 配股)在 **OMS、期权、TRS** 多个业务都要用,**必须抽到共享核心程序集**,各业务只消费、不各写一份。 + +### 4.1.2 推荐目录形态 + +``` +YLErp.Core / CorporateActions/ ← 共享核心,被 OMS/期权/TRS 引用,不反向依赖业务层 + CorporateAction.cs # 统一实体:actionType + record/ex/effective/payment 四日期 + factor/splitRatio + 金额 + ICorporateActionSource.cs # 上游数据源适配(聚源付息日历已含全日期,仅做映射) + ActionType.cs # 枚举:CashDividend / StockDividend / Split / BondCoupon / RightsIssue / Merger ... + handlers/ # 每种行为一个 typed handler(新增行为 = 加类,不动旧代码) + CashDividendHandler.cs # 现金分红(含债券 ETF 分红) + BondCouponHandler.cs # 债券付息(record 日快照归属) + SplitHandler.cs # 拆股 + StockDividendHandler.cs # 送股 + RightsIssueHandler.cs # 配股 + IAdjustmentFactorProvider.cs # 前复权 / 后复权 / 累计 CAF(t)=1(t≥ex)/=ratio(t