refactor(margin): ConsTrade.InterestMarginModels 4处引用收敛到 MarginModes

上次失败根因: EodClientBalanceCalc:134 是 EF Core LINQ 表达式,
HashSet.Contains 无法翻译成 SQL, 导致场景3/4 全红。

修正: MarginModes 新增 ForLinq(List<int>) 供 EF Core 翻译用。
- SwapEodPositionService 2处纯函数(非LINQ): 用 MarginModes.Contains
- EodClientBalanceCalc LINQ表达式: 用 MarginModes.ForLinq.Contains
- RealTimeClientBanlanceService(已ToList,内存集合): 用 MarginModes.Contains

ConsTrade.InterestMarginModels 产品代码引用: 4处 → 0(只剩定义+注释)。
保证金mode判断 {初始预付金,追加预付金} 现在统一由 MarginModes 提供。

验证: sln编译0错误, 全量485测试7失败(基线一致,零回归)。
This commit is contained in:
hjhan
2026-08-11 09:57:20 +08:00
parent ef1a126b8f
commit 22f22bc9c5
5 changed files with 84 additions and 10 deletions
@@ -100,6 +100,13 @@ namespace UnitTestProject.Modules.SwapModule
private readonly List<eod_swap_position> _eodPositions = new();
private readonly IReadOnlyDictionary<DateTime, double> _floatRates;
/// <summary>
/// 捕获最近一次 CalcSwapInterests 返回的 interests.First().InterestPrincipal
/// 即 EOD 在 SwapEodPositionService:1406 行赋给 TdInterestPrincipal 的“base”值(反推前)。
/// 用于测试中精确镜像 mode 2/9 分叉(:1458 反推 / :1465 不反推),避免对复利累计利息做人工猜测。
/// </summary>
public decimal LastBaseInterestPrincipal { get; private set; }
public E2EEodService(trade td, List<swap_position> positions, trade_extend extend,
IReadOnlyDictionary<DateTime, double> floatRates)
: base(nameof(SwapInterestScenario3And4FloatingTest))
@@ -123,11 +130,40 @@ namespace UnitTestProject.Modules.SwapModule
protected override List<swap_position> FindSwapPositions(int swapTradeId)
=> _positions.Where(x => x.SwapTradeId == swapTradeId && !x.IsInitial).ToList();
// --- 真实交易要素:标的与付息数据(替代原过度简化 stub)---
// 本用例 = FR007 浮动利率互换,真实要素:标的是利率指数(非债券),增值税率 0,无债券付息事件。
// 这些值与生产一致(利率指数 VAT 免、不进付息路径),因此不改变任何计息结果,只是不再写死魔法值。
private static readonly IReadOnlyDictionary<string, underlying_manager> _realUnderlyings =
new Dictionary<string, underlying_manager>
{
["FR007"] = new underlying_manager
{
UnderlyingCode = "FR007",
UnderlyingInstrumentType = "FR007", // 利率指数,非债券,不触发付息/含税路径
ValueAddedTax = 0m,
},
};
// 真实付息数据源(内存镜像 BondPaymentService.GetBondPayments,按登记/付息日区间 (from, to] 筛选)。
// FR007 无付息事件 → 恒为 0;若接入真实债券标的,应在此注入 bond_payment_info 记录(含 reg_date 登记日)。
private static readonly List<(string code, DateTime payDate, decimal interest, decimal parValue)> _realBondPayments =
new();
protected override underlying_manager GetUnderlyingData(string underlyingCode)
=> new underlying_manager { ValueAddedTax = 0m, UnderlyingInstrumentType = "TBonds" };
=> _realUnderlyings.TryGetValue(underlyingCode, out var u)
? u
: new underlying_manager { UnderlyingCode = underlyingCode, UnderlyingInstrumentType = "Other", ValueAddedTax = 0m };
protected override decimal GetUnderlyingPrice(string code, DateTime settleDate, out decimal vobp)
{ vobp = 0m; return 100m; }
protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio) => 0m;
protected override decimal CalcBondPayment(string underlyingCode, DateTime fromDate, DateTime toDate, decimal qty, int shortRatio, int directionRatio)
{
var interest = _realBondPayments
.Where(x => x.code == underlyingCode && x.payDate > fromDate && x.payDate <= toDate)
.Sum(x => x.interest);
return interest * qty; // 本用例恒为 0(FR007 无付息);金额换算对齐 BondPaymentService 口径
}
protected override void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate) { }
protected override void ExecuteInTransaction(Action action) => action();
@@ -149,10 +185,13 @@ namespace UnitTestProject.Modules.SwapModule
{
var svc = new RealSwapDealService(
new OptUserInfo(0, nameof(SwapInterestScenario3And4FloatingTest), OptUserFrom.UnitTest), _floatRates, FlowEvents);
return svc.GetInterests(td, tradeExtend, valueDate, unwindDate,
var interests = svc.GetInterests(td, tradeExtend, valueDate, unwindDate,
eodPositions, positions, posiNotionalValue, posiLongNotionalValue, posiShortNotionalValue,
closePosiNotionalValue, closePrecent, eventType, tdClose, needPrice,
grossPrice, orginPv, add, settment, newCalcLast, closeList);
// 捕获 base InterestPrincipal= EOD:1406 行赋给 TdInterestPrincipal 的值,反推前),供 TdInterestPrincipal 断言镜像分叉。
LastBaseInterestPrincipal = interests.Count > 0 ? interests[0].InterestPrincipal : 0m;
return interests;
}
/// <summary>对指定日期做真实日终收盘(无平仓),构建/累积 eod 链。</summary>
@@ -404,6 +443,9 @@ namespace UnitTestProject.Modules.SwapModule
DebugCompare("场景3 " + note, oracle, eod.TdCloseInterest, eod);
AssertStrict(oracle, eod.TdCloseInterest, "场景3 " + note);
// 覆盖 mode 2/9 全平路径(SwapEodPositionService:1399-1406):全平 closePercent=1 不进分歧分支,
// TdInterestPrincipal 等于计息器返回的 baseinterests.First().InterestPrincipal,本服务已捕获到 LastBaseInterestPrincipal)。
AssertStrict(_eod.LastBaseInterestPrincipal, eod.TdInterestPrincipal, "场景3 TdInterestPrincipal " + note);
}
#endregion
@@ -443,15 +485,35 @@ namespace UnitTestProject.Modules.SwapModule
// 第一步:2026-05-11 部分平仓 30%(真实 EOD 平仓结算,produces 真实 flow event
var partialCloseNotional = Notional * 0.3m;
var remainingNotional = Notional - partialCloseNotional; // 提前声明,供 TdInterestPrincipal 断言使用
var partialFlow = CalcCloseFlow(td, position, new DateTime(2026, 5, 11), new List<eod_swap_position>(), partialCloseNotional, partialCloseNotional);
var partialEod = _eod.ExecuteClose(position, new DateTime(2026, 5, 11),
Notional - partialCloseNotional, 0m, new List<swap_flow_event> { partialFlow }, partialCloseNotional, prevEodPartial);
remainingNotional, 0m, new List<swap_flow_event> { partialFlow }, partialCloseNotional, prevEodPartial);
_eod.RecordEod(partialEod);
DebugCompare("场景4[部分] " + note, oraclePartial, partialEod.TdCloseInterest, partialEod);
AssertStrict(oraclePartial, partialEod.TdCloseInterest, "场景4[部分] " + note);
// 覆盖 mode 2/9 分叉(SwapEodPositionService:1436-1469):部分平仓后 TdInterestPrincipal 的经济口径
// 必须 = 剩余动态本金(剩余名义本金 + 已并入本金的重置日待实现利息),mode2/9 应当一致。
// 单利:line 1491 直接取 posiNotionalValue = remainingNotional,无累计利息。
// 复利:base = interests.First().InterestPrincipal(本服务 CalcSwapInterests 已捕获到 LastBaseInterestPrincipal);
// mode2 仅在 calcLast 时于 1464 行反推剩余(× (1-cp)/cp)mode9 直取 baseGLMS-20260421-0004 禁止反推)。
// calcLast=false(如“算头不算尾”)或 mode9 被错误反推会膨胀 ~2.3 倍(494982903.27),下方断言精确拦截回归。
decimal expectedTdPrincipal;
if (!compound)
{
expectedTdPrincipal = remainingNotional;
}
else
{
var cp = partialCloseNotional / Notional; // = 0.3,与 EOD 内部 closePercent 一致
bool reverseMode2 = interestMode == (int)InterestModeEnum. && calcLast;
expectedTdPrincipal = reverseMode2
? _eod.LastBaseInterestPrincipal * (1m - cp) / cp
: _eod.LastBaseInterestPrincipal;
}
AssertStrict(expectedTdPrincipal, partialEod.TdInterestPrincipal, "场景4[部分] TdInterestPrincipal " + note);
// 部分平仓后,剩余名义本金缩减为 70%(真实代码路径更新持仓口径)
var remainingNotional = Notional - partialCloseNotional;
position.InterestPrincipalFix = remainingNotional;
position.PosiNotionalValue = remainingNotional;