refactor(swap): 保证金腿在 GetInterests 循环入口提前路由,融资腿分支树去除 MarginModes 依赖

- 保证金腿(5/6)在最外层路由并 continue:本金(InterestPrincipalFix×closePrecent)、
  方向翻转、CalcMarginInterest 收口一处;newClosePercent 在保证金腿恒等于
  closePrecent,直接传参
- 融资腿(1/2/9)分支树内 3 处 MarginModes.Contains 归零,后续融资腿改动与保证金解耦
- GetFixedRate 前移为两路共用(纯读函数,已验证无副作用)
- 有意跳过 GetFloatRate:CalcMarginInterest 纯固定利率(FundingLegRate.Fixed/
  FloatRate 恒 0),浮动取价对保证金无意义;脏数据 FloatRateUnderlyingCode
  缺价时不再阻断保证金结算(唯一行为变化,方向更安全)
- 验证:SwapModule 537 用例 A/B(带改/不带改)摘要完全一致(71F/459P/7S),
  失败项均为本机无库环境失败;内网全量单测待跑

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
hjhan
2026-08-16 11:03:11 +08:00
co-authored by Claude Opus 4.7
parent 44e24b3172
commit db447aa584
+24 -33
View File
@@ -657,27 +657,30 @@ namespace YLErp.Modules.SwapModule
// true 跳过 不计利息; false 正常利息
bool swap = InitInterestDate(unwindDate, preDealDate, td, tdClose, out DateTime startDate, out DateTime endDate);
// 计算名义本金
decimal closePrincipal;
decimal posiPrincipal;
decimal newClosePercent = closePrecent;
var mode = (InterestModeEnum)position.InterestMode;
// 获取利率(保证金/融资腿共用:SwapIntervalList 取当日适用固定利率 + 精度收口)
decimal rate = Math.Round(GetFixedRate(position, unwindDate), InterestCalculationPrecision, MidpointRounding.AwayFromZero); // 做精度调整 原数据有精度误差
// ── 边界隔离:保证金腿(5/6)在循环最外层路由,后续融资腿分支树不感知保证金概念 ──
// 有意跳过 GetFloatRateCalcMarginInterest 纯固定利率(FundingLegRate.Fixed)且 FloatRate 恒 0
// 浮动取价/回写对保证金无意义;即使脏数据填了 FloatRateUnderlyingCode 且缺价,也不应阻断保证金结算。
if (MarginModes.Contains(position.InterestMode))
{
// 保证金腿: 计息基数 = InterestPrincipalFix(保证金余额)
closePrincipal = position.InterestPrincipalFix * closePrecent;
posiPrincipal = position.InterestPrincipalFix;
}
else
{
// 融资腿(1/2/9): 走策略工厂
var r = FundingLegStrategyFactory.Get(mode)
.CalcNotional(position.InterestPrincipalFix, posiNotionalValue, closePrecent);
closePrincipal = r.ClosePrincipal;
posiPrincipal = r.PosiPrincipal;
newClosePercent = r.ClosePercent;
positionClone.InterestDirection = MarginCalc.FlipDirection(position.InterestDirection);
// 保证金腿: 计息基数 = InterestPrincipalFix(保证金余额),无融资腿差分公式与 orginPv 维度 hack
interests.Add(CalcMarginInterest(td, valueDate, endDate, positionClone, rate,
position.InterestPrincipalFix * closePrecent, position.InterestPrincipalFix,
closePrecent, annualDays, calcFirst, calcLast || newCalcLast, preEodPosition, eventType, add, settment, swap));
continue;
}
// 计算名义本金(以下仅融资腿 1/2/9:走策略工厂)
var mode = (InterestModeEnum)position.InterestMode;
var r = FundingLegStrategyFactory.Get(mode)
.CalcNotional(position.InterestPrincipalFix, posiNotionalValue, closePrecent);
decimal closePrincipal = r.ClosePrincipal;
decimal posiPrincipal = r.PosiPrincipal;
decimal newClosePercent = r.ClosePercent;
// 根因位置:SwapEodPositionService.SaveAutoEodWithCloseInterestPosition 在平仓后收盘时传入
// “收盘后剩余本金 + closePercent=1”,与盘中“平仓前本金 + 实际关闭比例”不是同一语义。
// GetInterests 同时被盘中试算和 EOD 平仓后收盘调用:后者传入的
@@ -686,29 +689,17 @@ namespace YLErp.Modules.SwapModule
// 模式2(合约名义本金规模)的本次结息本金必须始终是实际平仓额,因此无条件覆盖,
// 否则会错误地用剩余 70 结算本次平掉的 30。模式9(标的期初全价)的部分平仓
// 仍保留既有的剩余/复利动态本金承接逻辑;仅最终全平时 posi=0,才覆盖以避免结息本金为 0。
if ((InterestModeEnum)position.InterestMode == InterestModeEnum.
|| ((InterestModeEnum)position.InterestMode == InterestModeEnum.
if (mode == InterestModeEnum.
|| (mode == InterestModeEnum.
&& posiNotionalValue == 0m))
{
closePrincipal = closePosiNotionalValue;
}
if (MarginModes.Contains(position.InterestMode))
{
positionClone.InterestDirection = MarginCalc.FlipDirection(position.InterestDirection);
}
// 获取利率
decimal rate = Math.Round(GetFixedRate(position, unwindDate), InterestCalculationPrecision, MidpointRounding.AwayFromZero); // 做精度调整 原数据有精度误差
decimal floatRate = GetFloatRate(position, preEodPosition, td.StartDate.Value, endDate, interestPeriod, swap, positionClone);
// 根据场景计算利息
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)
if (settment)
{
// 收盘归档场景,使用 CalcEodInterest
interests.Add(CalcEodInterest(td, valueDate, positionClone, rate, floatRate, closePrincipal, posiPrincipal, annualDays, calcFirst, calcLast, preEodPosition, eventType, add));
@@ -722,7 +713,7 @@ namespace YLErp.Modules.SwapModule
: 0m;
interests.Add(CalcUnwindInterest(td, valueDate, endDate, positionClone, rate, floatRate, posiPrincipal,
closePrincipal, newClosePercent, annualDays, preEodPosition, eventType, add, swap, orginPv, calcFirst,
calcLast||newCalcLast, consumedInterest));
calcLast || newCalcLast, consumedInterest));
}
}
//当日有平仓或互换记录时,避免重复结算