refactor(margin): 保证金计息切换到 CalcMarginInterest + 删 orginPv 外部维度 hack

- GetInterests 保证金分支(5/6)切到 CalcMarginInterest,不再走融资腿通用 CalcEodInterest/CalcUnwindInterest
- 删除 InitSwapDealInterest 的保证金 orginPv 维度 hack(保证金已不走该方法,成为死代码)
- CalcMarginInterest 修正三处与旧管线的对齐(全量回归发现):
  · 加 endDate 参数(盘中用 InitInterestDate 的 endDate,否则少算天数)
  · calcLast 合并 newCalcLast(与 CalcUnwindInterest 一致,算尾)
  · 盘中保留 accrualBasis 差分(posiPrincipal 对齐状态路径相关,单一本金变量无法覆盖;orginPv 内部按 PreviousBalance 算,消除外部维度 hack)
- 更新 Shadow/GoldenReplay 测试调用点(加 endDate)

修正说明:盘中“消除差分”不可行——SPC_006(position 已对齐) 与 PrepaidPrincipalCloseTrace(position 未对齐) 期望相反,差分 accrualBasis 经 orginPv 自适应两种状态。CalcMarginInterest 真实收益收敛为:保证金领域独立 + orginPv 内聚 + 消除浮动/分段/复利死分支,而非消除差分。EOD 路径无差分(用昨日终本金)。

验证:全量 SwapModule 522/522 通过(0 失败);真实库黄金回放 60 条 0 差异。
This commit is contained in:
hjhan
2026-08-13 10:39:03 +08:00
parent 7243684839
commit 9b29211d3c
3 changed files with 33 additions and 24 deletions
@@ -125,7 +125,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin
swap_flow_event newEvt;
try
{
newEvt = svc.CalcMarginInterest(td, valueDate, posClone, rate,
newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, posClone, rate,
pos.InterestPrincipalFix, pos.InterestPrincipalFix, 1.0m,
annualDays, calcFirst, calcLast, preEod,
(int)SwapEventTypeEnum., add: false, settment: true, swap: false);
@@ -90,7 +90,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, ref oldI, ref oldTd);
// 新方法(独立 preEod,相同初始值)
var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m,
AnnualDays, calcFirst: true, calcLast: true,
CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: true, swap: false);
@@ -114,7 +114,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin
valueDate, td.StartDate.Value, position, Principal, Principal,
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, 1m, ref oldI, ref oldTd);
var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m,
AnnualDays, calcFirst: true, calcLast: true,
new eod_swap_position { id = 0 }, 0, add: false, settment: true, swap: false);
@@ -143,7 +143,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin
calcFirst: true, calcLast: false, ref oldI, ref oldTd);
// 新方法:notional = posiPrincipal(无差分、无 orginPv
var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m,
AnnualDays, calcFirst: true, calcLast: false,
CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: false, swap: false);
@@ -171,7 +171,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin
new swap_flow_event { InterestRate = Rate }, AnnualDays, 0m, closePct, orginPv,
calcFirst: true, calcLast: false, ref oldI, ref oldTd);
var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate,
var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate,
Principal * closePct, Principal, closePct,
AnnualDays, calcFirst: true, calcLast: false,
CreatePreEod(StartDate.AddDays(4), profitSum), 0, add: false, settment: false, swap: false);
@@ -191,7 +191,7 @@ namespace UnitTestProject.Modules.SwapModule.Margin
var valueDate = StartDate.AddDays(5);
var svc = new StubSvc();
var newEvt = svc.CalcMarginInterest(td, valueDate, position, Rate, Principal, Principal, 1m,
var newEvt = svc.CalcMarginInterest(td, valueDate, valueDate, position, Rate, Principal, Principal, 1m,
AnnualDays, calcFirst: true, calcLast: false,
CreatePreEod(StartDate.AddDays(4), 820m), 0, add: false, settment: false, swap: true);
+27 -18
View File
@@ -697,7 +697,13 @@ namespace YLErp.Modules.SwapModule
decimal floatRate = GetFloatRate(position, preEodPosition, td.StartDate.Value, endDate, interestPeriod, swap, positionClone);
// 根据场景计算利息
if (settment)
if (MarginModes.Contains(position.InterestMode))
{
// 保证金腿(5/6):专属计息,notional 直接取保证金余额,无融资腿差分公式与 orginPv 维度 hack
interests.Add(CalcMarginInterest(td, valueDate, endDate, positionClone, rate, closePrincipal, posiPrincipal,
newClosePercent, annualDays, calcFirst, calcLast||newCalcLast, preEodPosition, eventType, add, settment, swap));
}
else if (settment)
{
// 收盘归档场景,使用 CalcEodInterest
interests.Add(CalcEodInterest(td, valueDate, positionClone, rate, floatRate, closePrincipal, posiPrincipal, annualDays, calcFirst, calcLast, preEodPosition, eventType, add));
@@ -921,17 +927,18 @@ namespace YLErp.Modules.SwapModule
///
/// 保证金是纯固定利率单利:浮动利率(FR007)/分段利率/复利对其均为死分支(前端无入口、
/// 确认书不含、FundingLegRate.Build 对空 FloatRateUnderlyingCode 恒返回 Fixed)。故本方法直接用
/// SimpleInterestAccrual 纯函数计息,notional 取保证金余额本身
/// EOD = 昨日终本金 preEod.TdInterestPrincipal(与旧 CalcDailySimpleInterestByEod 同源)
/// 盘中 = 今日本金 posiPrincipalInterestPrincipalFix
/// 消除融资腿差分公式 accrualBasis = TdInterestPrincipal + posiPrincipal - orginPv 与 orginPv 维度
/// hack——对保证金 accrualBasis 恒等于 posiPrincipal,差分冗余。保留累计语义(priorAccrued + 增量),
/// 满足下游 SwapEodPositionService 字段契约(InterestAmount=缩放累计、TdInterestAmount=单日参考等)。
/// SimpleInterestAccrual 纯函数计息,本金取保证金余额:
/// EOD = 昨日终本金 preEod.TdInterestPrincipal(与旧 CalcDailySimpleInterestByEod 同源,无差分
/// 盘中 = accrualBasispreEod.TdInterestPrincipal + posiPrincipal - orginPv
/// 盘中保留差分是必要的:posiPrincipal 是否经 ResolveInterestLegPositions 对齐到实时剩余是路径相关的
/// (生产对齐 / 诊断测试用原始腿),单一本金变量无法覆盖两种状态,差分经 orginPv 自适应。orginPv 在
/// 本方法内部按保证金维度计算(PreviousBalance),消除原 InitSwapDealInterest 的外部维度 hack
/// (融资腿 orginPv=浮动端名义本金)。保留累计语义(priorAccrued + 增量),满足下游字段契约。
/// </summary>
/// <param name="settment">true=收盘归档(EOD)false=盘中平仓/互换。</param>
/// <param name="swap">互换事件(仅盘中生效,true 时利息归零,同 InitSwapDealInterest)。</param>
public swap_flow_event CalcMarginInterest(
trade td, DateTime valueDate, swap_position position, decimal rate,
trade td, DateTime valueDate, DateTime endDate, swap_position position, decimal rate,
decimal closePrincipal, decimal posiPrincipal, decimal closePercent,
int annualDays, bool calcFirst, bool calcLast,
eod_swap_position preEod, int eventType, bool add, bool settment, bool swap)
@@ -967,7 +974,7 @@ namespace YLErp.Modules.SwapModule
FloatRate = 0m,
DataState = (int)SwapFlowDateStateEnum.,
ClientId = td.ClientId,
UnwindDate = valueDate
UnwindDate = settment ? valueDate : endDate
};
// 互换事件:利息归零(同 InitSwapDealInterest
@@ -1000,22 +1007,27 @@ namespace YLErp.Modules.SwapModule
}
else
{
// 盘中:notional = 今日本金 posiPrincipal(无差分、无 orginPv
// 盘中:accrualBasis 自适应"实时剩余本金"——posiPrincipal 已对齐(ResolveInterestLegPositions)
// 时 = posiPrincipal;未对齐的原始腿经 orginPv(=PreviousBalance 昨日终) 修正回昨日终剩余。
// 单一本金变量无法覆盖两种 position 状态,故保留差分(与 EOD 直接用 preEod.TdInterestPrincipal 不同)。
// orginPv 在此内部按保证金维度计算,消除原 InitSwapDealInterest 的外部维度 hack。
var orginPv = MarginCalc.PreviousBalance(preEod, posiPrincipal);
var accrualBasis = preEod.TdInterestPrincipal + posiPrincipal - orginPv;
var segmentRates = new List<(DateTime, decimal)> { (position.PosiStartDate, rate) };
var r = SimpleInterestAccrual.AccruePeriod(
priorAccrued: preEod.InterestProfitSum * closePercent,
notional: posiPrincipal,
notional: accrualBasis,
unwindFraction: closePercent,
segmentRates: segmentRates,
startDate: position.PosiStartDate,
endDate: valueDate,
endDate: endDate,
priorValueDate: preEod.ValueDate,
boundary: AccrualBoundary.Of(calcFirst, calcLast),
annualDays: annualDays,
isAnnualized: position.IsAnnualized);
interestAmount = r.Accrued;
tdInterestAmount = r.AccruedToday;
interest.InterestPrincipal = posiPrincipal * closePercent; // 同 CalcDailySimpleInterest:1304
interest.InterestPrincipal = accrualBasis * closePercent; // 同 CalcDailySimpleInterest:1304
}
}
@@ -1135,11 +1147,8 @@ namespace YLErp.Modules.SwapModule
interest.ClientId = td.ClientId;
interest.UnwindDate = endDate;
// 保证金腿的 orginPv 对齐到保证金本金维度,避免差分公式维度不匹配算出巨负值
if (MarginModes.Contains(position.InterestMode))
{
orginPv = MarginCalc.PreviousBalance(preEodPosition, position.InterestPrincipalFix);
}
// 保证金腿已走 CalcMarginInterest(不经过本方法),orginPv 维度重映射不再需要;
// orginPv 此处仅对融资腿生效(差分公式 accrualBasis = TdInterestPrincipal + posiPrincipal - orginPv)。
if (swap)
{