refactor: 提取融资腿计息领域模型 (AccrualPolicy/FundingLegRate/AccrualState/FundingLegAccrual) - 将 CalcDailySimpleInterestByEod 纯数学下沉至 FundingLegAccrual.AccrueSimpleEod, 消除 double 往返转换, 引入值对象收敛散落参数

This commit is contained in:
hjhan
2026-08-11 22:37:19 +08:00
parent 246392b665
commit 0c447ff5ff
6 changed files with 403 additions and 32 deletions
@@ -0,0 +1,49 @@
using YLErp.Derivatives.Interest;
using YLErp.DBModels;
namespace YLErp.Modules.SwapModule.Accrual;
/// <summary>
/// 计息政策(不可变配置)。把"算头算尾 / 单复利率 / 重置频率 / 年化天数"收敛为一处,
/// 取代旧代码里散落各处的 calcFirst/calcLast 布尔对与魔法数字。
///
/// 单/复利不再另立枚举——直接复用既有 DB 枚举 <see cref="InterestTypeEnum"/>(单利=0 / 复利=1),
/// 通过 <see cref="IsCompound"/> 暴露为类型安全的 bool,避免与 SwapInterest 已有的
/// AccrueSimple/AccrueCompound 方法分裂出"第三种单复利表达"(这是第一版草稿犯过的重复)。
/// </summary>
public sealed class AccrualPolicy
{
/// <summary>算头算尾约定(复用 SwapInterest 已有的 AccrualBoundary,物理上杜绝 calcFirst/calcLast 传反)。</summary>
public AccrualBoundary Convention { get; }
/// <summary>是否复利(利滚利)。来自 DB 的 InterestTypeEnum;单利=false,复利=true。</summary>
public bool IsCompound { get; }
/// <summary>利率重置周期(天)。FR007 通常为 7;复利时亦为"利息并入本金"的周期。</summary>
public int ResetPeriodDays { get; }
/// <summary>年化基数(365 / 360)。</summary>
public int AnnualDays { get; }
/// <summary>是否年化(position.IsAnnualized)。决定利息是否再除以 <see cref="AnnualDays"/>
/// 与 <see cref="AnnualDays"/> 一同收敛 daycount 语义,不再作为裸 bool 散落在计息签名里。</summary>
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);
/// <summary>
/// 从交易扩展解析(边界适配)。调用方负责从 trade_extend.ExtendObj 取出
/// InterestCalcMode / AnnualDays / interest_rest_days 与计息方式后传入;
/// 这里只做"布尔对 → AccrualBoundary"与"InterestTypeEnum → bool"的归一。
/// </summary>
/// <param name="includeStart">算头(InterestCalcMode 首位为 '1')。</param>
/// <param name="includeEnd">算尾(InterestCalcMode 末位为 '1')。</param>
/// <param name="annualDays">年化天数(ExtendObj.AnnualDays)。</param>
/// <param name="interestType">DB 计息方式枚举(单利 / 复利)。</param>
/// <param name="resetPeriodDays">重置周期天数(interest_rest_days,缺省 1)。</param>
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);
}
@@ -0,0 +1,49 @@
using YLErp.DBModels;
namespace YLErp.Modules.SwapModule.Accrual;
/// <summary>
/// 融资腿逐日计息的跨日状态(不可变值对象)。
/// 这是"待实现利息"在日间滚动的快照,区别于已落库的 <c>swap_flow_event</c>。
///
/// 旧字段 → 领域命名映射(DB 列不可改,仅在边界处适配;本类内部一律用下列自描述名):
/// <list type="table">
/// <item><term>TdInterestPrincipal</term><description>逐日滚动的计息本金 → <see cref="AccrualPrincipal"/></description></item>
/// <item><term>InterestIncomeSum</term><description>累计待实现利息 → <see cref="UnrealizedInterest"/></description></item>
/// <item><term>consumedInterest</term><description>历史已实现利息(legacy) → <see cref="RealizedInterest"/></description></item>
/// <item><term>ValueDate</term><description>快照截至日 → <see cref="ValueDate"/>EOD 续接起算日,Bug C / 5-11 跳过需据此判断从哪天接续)。</description></item>
/// </list>
/// </summary>
public readonly struct AccrualState
{
/// <summary>用于计算当日利息的计息本金。单利=名义本金基数;复利=本金+累计利息。</summary>
public decimal AccrualPrincipal { get; }
/// <summary>累计待实现(未平仓)利息。</summary>
public decimal UnrealizedInterest { get; }
/// <summary>历史各次平仓已确认的已实现利息,从剩余待实现中扣除。</summary>
public decimal RealizedInterest { get; }
/// <summary>快照截至日(来自 eod_swap_position.ValueDate)。编排层据此判断计息区间起点,避免 5-11 等"跳过日"误重算。</summary>
public DateTime ValueDate { get; }
public AccrualState(decimal accrualPrincipal, decimal unrealizedInterest, decimal realizedInterest, DateTime valueDate)
=> (AccrualPrincipal, UnrealizedInterest, RealizedInterest, ValueDate) = (accrualPrincipal, unrealizedInterest, realizedInterest, valueDate);
/// <summary>向后兼容:未携带快照日期时(如纯内存构造)用默认日。</summary>
public AccrualState(decimal accrualPrincipal, decimal unrealizedInterest, decimal realizedInterest)
: this(accrualPrincipal, unrealizedInterest, realizedInterest, default) { }
/// <summary>空状态(新开仓首个计息日之前)。</summary>
public static readonly AccrualState Zero = new(0m, 0m, 0m);
/// <summary>
/// 从上一日日终归档 <see cref="eod_swap_position"/> 适配(边界适配:DB 列名 → 领域名)。
/// 仅映射计息状态;名义本金基数 / 平仓比例 / 已实现利息等由调用方另行传入。
/// </summary>
public static AccrualState FromPreviousEod(eod_swap_position previousEod)
=> previousEod == null || previousEod.id == 0
? Zero
: new AccrualState(previousEod.TdInterestPrincipal, previousEod.InterestIncomeSum, 0m, previousEod.ValueDate);
}
@@ -0,0 +1,184 @@
using System;
using YLErp.Derivatives.Interest;
namespace YLErp.Modules.SwapModule.Accrual;
/// <summary>
/// 融资腿计息编排层(NEW,替换 SwapDealService 内 CalcDaily* 家族的"纯数学"部分)。
///
/// <para>职责边界(与 SwapInterest 原语、SwapDealService 适配器三者正交):</para>
/// <list type="bullet">
/// <item><description>本类:持有跨日 <see cref="AccrualState"/>,逐日循环,经 IIndexFixer 取当日率并封装为 <see cref="FundingLegRate"/>
/// 调用 SwapInterest 原子原语算账,处理平仓缩放 / 已实现扣除,replay 求 Δ。</description></item>
/// <item><description>SwapInterest:原子 "本金×利率×天数/年化" 纯函数,无状态。</description></item>
/// <item><description>SwapDealService.GetInterests:仅做 DB 读、swap_flow_event 构造与落库(IO)。</description></item>
/// </list>
///
/// <para>命名规范:本文件内所有概念一律使用自描述英文名。旧代码 typo 一律不出现:</para>
/// <list type="table">
/// <item><term>closePrecent</term><description>→ <see cref="ApplyPartialClose"/> 的 closeRatio(且语义固定为"占剩余持仓比例"</description></item>
/// <item><term>orginPv</term><description>→ OriginalPv(原始名义本金,用于保证金腿差分基数)</description></item>
/// <item><term>floateRate</term><description>→ FloatRateFR007 浮动利率,拼写修正)</description></item>
/// <item><term>dynomicPrincipal</term><description>→ AccrualPrincipal(逐日滚动计息本金)</description></item>
/// </list>
///
/// <para>重要——以下 <see cref="AccruePeriod"/> 当前为"结构骨架"</para>
/// 精确的跨日不变量(T+1 本金缩到剩余、重置日本金保留、consumedInterest 扣除、保证金腿差分公式)
/// 依 branch-merge-analysis §7 的 DI_* 不变量,必须在把 <c>_0808</c>(人工已过)的利息核心并入后,
/// 从 CalcDaily* 迁移而来并以 Excel oracle 验收。骨架故意只跑"朴素逐日累加",
/// <b>不等价于已验证口径——切勿在未迁移前接入生产</b>。
/// </summary>
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;
/// <summary>
/// 单日原子计息("AccrueDay")。委托 SwapInterest.AccrueSimple(同一天、Convention 边界 → 1 天),
/// 直接复用 SwapInterest 已有的 <see cref="InterestResult"/>Accrued/AccruedToday)作为返回,
/// 不再另立结果类型。编排层按日把"当天本金 + 当天率"喂入此入口,使浮动利率逐日不同也能正确累积。
/// 复利时收盘本金 = 当日本金 + 当日利息(利滚利),该推导在 <see cref="AccruePeriod"/> 内完成,
/// 不塞进返回结构,保持与 SwapInterest 单一结果类型的契约一致。
/// </summary>
/// <param name="accrualPrincipal">当日计息本金(来自 <see cref="AccrualState.AccrualPrincipal"/>)。</param>
/// <param name="dailyRate">当日生效年利率(由调用方经 IIndexFixer + 合约利差构造后传入,已含 spread)。</param>
/// <param name="policy">计息政策(含算头算尾 / 年化天数 / 单复利)。</param>
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);
}
/// <summary>
/// 单利日终计息(纯函数,替换 SwapDealService.CalcDailySimpleInterestByEod 的"纯数学"部分)。
///
/// <para>口径与旧实现逐字对齐(仅命名 DDD 化):</para>
/// <list type="bullet">
/// <item><description>计息基数 baseTdInterestPrincipal = priorAccrualPrincipal + positionPrincipal originalPv</description></item>
/// <item><description>当日利息 = baseTdInterestPrincipal × closeRatio × rate.AllInRate,年化则再 ÷ AnnualDays</description></item>
/// <item><description>累计未实现 = priorUnrealized + 当日利息;末位按资金腿精度 12 舍入。</description></item>
/// </list>
///
/// <para>取率与重置日重取浮动利率由适配器(CalcDailySimpleInterestByEod)负责,并封装为 <see cref="FundingLegRate"/> 传入;
/// daycount 语义(年化 / 年化天数)由 <see cref="AccrualPolicy"/> 提供。本方法保持纯函数、可独立单测,不连库、不取价。</para>
/// </summary>
/// <param name="priorUnrealized">上一日日终累计未实现利息(preEod.InterestProfitSum)。</param>
/// <param name="priorAccrualPrincipal">上一日日终计息本金(preEod.TdInterestPrincipal)。</param>
/// <param name="positionPrincipal">存量名义本金(posiPrincipal)。</param>
/// <param name="closeRatio">平仓比例(closePercentEOD 恒为 1)。</param>
/// <param name="originalPv">原始名义本金(orginPv),用于保证金腿差分基数。</param>
/// <param name="rate">当日生效利率(已由适配器按腿型封装:固定腿=FixedRate,浮动腿=Spread+IndexFixing)。</param>
/// <param name="policy">计息政策(daycount:是否年化 / 年化天数)。</param>
/// <returns><see cref="InterestResult"/>Accrued=累计未实现(对应 InterestAmount),AccruedToday=当日利息(对应 TdInterestAmount)。</returns>
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));
}
/// <summary>
/// 逐日计息编排(骨架)。从 <paramref name="startDate"/> 到 <paramref name="endDate"/> 逐日循环,
/// 推进 <see cref="AccrualState"/>,并返回期末状态。
/// </summary>
/// <param name="opening">期初状态(通常来自上一日日终,见 <see cref="AccrualState.FromPreviousEod"/>)。</param>
/// <param name="startDate">计息区间起点。</param>
/// <param name="endDate">计息区间终点。</param>
/// <param name="dailyRate">取计息日生效年利率的委托(适配器处由 IIndexFixer + 合约利差构造;测试可传 day =&gt; 0.0134m)。</param>
/// <param name="policy">计息政策。</param>
/// <param name="closeRatio">可选平仓比例(占剩余,0~1)。非空则在期末应用平仓缩放。</param>
public static AccrualState AccruePeriod(
AccrualState opening,
DateTime startDate,
DateTime endDate,
Func<DateTime, decimal> 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. 保证金腿差分基数 OriginalPvdynomicPrincipal = AccrualPrincipal + PositionPrincipal - OriginalPv,此处分支处理。
// 5. replay 求 Δ:本 EOD 状态 − 上一 EOD 状态,由调用方(适配器层)负责,不在纯数学内。
}
if (closeRatio.HasValue)
{
state = ApplyPartialClose(state, closeRatio.Value, state.RealizedInterest, policy);
}
return state;
}
/// <summary>
/// 部分 / 全部平仓缩放:把剩余待实现按 (1-closeRatio) 保留,并扣除历史已实现;
/// 计息本金同步缩到剩余持仓。委托 SwapInterest.ApplyUnwind / AccrueUnrealized(与 Margin 共用同一纯函数)。
/// </summary>
/// <param name="state">平仓前状态(未实现部分)。</param>
/// <param name="closeRatio">平仓比例(占剩余持仓,0~11=全平)。</param>
/// <param name="realizedInterest">历史已实现利息累计,从剩余未实现中扣除。</param>
/// <param name="policy">计息政策(精度取 SwapInterest.Precision)。</param>
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);
}
}
@@ -0,0 +1,40 @@
namespace YLErp.Modules.SwapModule.Accrual;
/// <summary>
/// 融资腿在某一计息日生效的利率(不可变值对象)。
///
/// <para>把"固定利率 / 加点利差 / 浮动指数定盘"三种来源收敛为一个概念,避免把它们作为裸 decimal
/// 散落在计息方法签名里——这正是初版 <c>AccrueSimpleEod</c> 参数膨胀、可读性差的根因。</para>
///
/// <para>固定腿与浮动腿是互斥的两种形态:固定腿只设 <see cref="FixedRate"/>(其余为 0);
/// 浮动腿设 <see cref="Spread"/> + <see cref="IndexFixing"/><see cref="FixedRate"/> 为 0)。
/// <see cref="AllInRate"/> 对两种形态统一为三者之和。</para>
///
/// <list type="bullet">
/// <item><description><see cref="FixedRate"/>:固定腿的固定年利率;浮动腿为 0。</description></item>
/// <item><description><see cref="Spread"/>:浮动腿在指数之上的加点利差(合约侧);固定腿为 0。</description></item>
/// <item><description><see cref="IndexFixing"/>:浮动指数定盘(FR007 等);固定腿为 0。</description></item>
/// <item><description><see cref="AllInRate"/>:计息用的"当日生效年利率"。固定腿取 <see cref="FixedRate"/>;浮动腿取 <see cref="Spread"/> + <see cref="IndexFixing"/>(二者互斥,非叠加)。</description></item>
/// </list>
/// </summary>
public readonly struct FundingLegRate
{
/// <summary>固定腿的固定年利率;浮动腿为 0。</summary>
public decimal FixedRate { get; }
/// <summary>浮动腿在指数之上的加点利差(合约侧);固定腿为 0。</summary>
public decimal Spread { get; }
/// <summary>浮动指数定盘利率(市场侧,FR007 等);固定腿为 0。</summary>
public decimal IndexFixing { get; }
/// <summary>
/// 当日生效年利率。固定腿与浮动腿是互斥的两种利率确定方式,不是可叠加分量:
/// 固定腿取 <see cref="FixedRate"/>;浮动腿取 <see cref="Spread"/> + <see cref="IndexFixing"/>。
/// 约定:固定腿只设 FixedRate(其余为 0),浮动腿只设 Spread + IndexFixingFixedRate 为 0)。
/// </summary>
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);
}
+38 -32
View File
@@ -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
/// </summary>
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);
// 纯数学下沉至 FundingLegAccrualDDD 命名 + 末位生产精度 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;
}
/// <summary>
+43
View File
@@ -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<ex)
EodHooks/ # EOD 收盘链上的接入口(与现有 SwapEodPositionService 解耦的薄适配层)
IRecordDateHandler.cs # 登记日收盘:按快照确认 dividendin/accruedCash(金额钉死)
IExDateHandler.cs # 除息日:价格自动剔息 + 穿越持仓因子(开盘前第一步,绝不可收盘后补)
IPaymentDateHandler.cs # 支付日:纯现金划付,不重算归属
```
### 4.1.3 边界警示(重要,避免域混淆)
- **债券 ETF 分红 / 付息 = 公司行为域** → 走 `CorporateActions` 模块。
- **复利 / 部分平仓 / T+1 本金继承 / 重置日动态本金** = **利息计息域**,现居 `SwapDealService.cs``InitSwapDealInterest` / `CalcDailyCompoundInterest` / `CalcUnwindInterest`+ `SwapEodPositionService.cs``SaveAutoEodWithCloseInterestPosition`)。这是**另一回事**,与"公司行为"正交:
- 付息(coupon)的**金额**由 corp action 决定(按登记日快照);
- 付息的**利息滚存/复利/部分平仓结算**由利息核心决定。
- 两者通过 `swap_flow_event`(付息流水)衔接,**不要在 corp action 模块里实现复利逻辑**,也不要在利息核心里硬编码某种公司行为的日期语义。
- 当前 `_0808` 分支的 4 个复利部分平仓修复(见分支对比分析)属于**利息核心**,与本模块无关;合并时利息核心以 `_0808` 为准,corp action 模块独立演进。
---
## 5. 待确认 / 下一步
- [ ] 阶段 0 是否现在落地(加字段 + 改 `DealDividends` + 补「登记日快照」回归测试)?