diff --git a/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs b/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs index e06b31a2..4d396b0a 100644 --- a/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs +++ b/UnitTestProject/Modules/SwapModule/SwapInterestScenario3And4FloatingTest.cs @@ -100,6 +100,13 @@ namespace UnitTestProject.Modules.SwapModule private readonly List _eodPositions = new(); private readonly IReadOnlyDictionary _floatRates; + /// + /// 捕获最近一次 CalcSwapInterests 返回的 interests.First().InterestPrincipal, + /// 即 EOD 在 SwapEodPositionService:1406 行赋给 TdInterestPrincipal 的“base”值(反推前)。 + /// 用于测试中精确镜像 mode 2/9 分叉(:1458 反推 / :1465 不反推),避免对复利累计利息做人工猜测。 + /// + public decimal LastBaseInterestPrincipal { get; private set; } + public E2EEodService(trade td, List positions, trade_extend extend, IReadOnlyDictionary floatRates) : base(nameof(SwapInterestScenario3And4FloatingTest)) @@ -123,11 +130,40 @@ namespace UnitTestProject.Modules.SwapModule protected override List FindSwapPositions(int swapTradeId) => _positions.Where(x => x.SwapTradeId == swapTradeId && !x.IsInitial).ToList(); + // --- 真实交易要素:标的与付息数据(替代原过度简化 stub)--- + // 本用例 = FR007 浮动利率互换,真实要素:标的是利率指数(非债券),增值税率 0,无债券付息事件。 + // 这些值与生产一致(利率指数 VAT 免、不进付息路径),因此不改变任何计息结果,只是不再写死魔法值。 + private static readonly IReadOnlyDictionary _realUnderlyings = + new Dictionary + { + ["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; } /// 对指定日期做真实日终收盘(无平仓),构建/累积 eod 链。 @@ -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 等于计息器返回的 base(interests.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(), partialCloseNotional, partialCloseNotional); var partialEod = _eod.ExecuteClose(position, new DateTime(2026, 5, 11), - Notional - partialCloseNotional, 0m, new List { partialFlow }, partialCloseNotional, prevEodPartial); + remainingNotional, 0m, new List { 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 直取 base(GLMS-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; diff --git a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs index aeedd67e..33704017 100644 --- a/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs +++ b/YLErpDAL/BLL/EodSettlement/RealTimeClientBanlanceService.cs @@ -20,6 +20,7 @@ using YLErp.Modules; using YLErp.Modules.DataProviderModule; using YLErp.Modules.EodModule; using YLErp.Modules.EodModule.QueryModule; +using YLErp.Modules.SwapModule.Margin; using YLErp.Modules.UnderlyingModule; namespace YLErp.BLL.Eod @@ -1294,7 +1295,7 @@ namespace YLErp.BLL.Eod var balance = _clientBalanceDic[item.Key]; var clientTradeIds = item.Select(s => s.id).ToList(); var clientPositions = positions.Where(x => clientTradeIds.Contains(x.SwapTradeId)).ToList(); - var marginList = clientPositions.Where(x =>ConsTrade.InterestMarginModels.Contains(x.InterestMode)).Sum(s=>s.InterestPrincipalFix * (s.InterestDirection == 1 ? -1 : 1)); + var marginList = clientPositions.Where(x => MarginModes.Contains(x.InterestMode)).Sum(s=>s.InterestPrincipalFix * (s.InterestDirection == 1 ? -1 : 1)); balance.SwapMargin =Convert.ToDouble(marginList); balance.PositionCount= clientTradeIds.Count(); // balance.PositionNotionalPrincipal = Convert.ToDouble(positions.Sum(s=>s.PosiNotionalValue));//实时 diff --git a/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalc.cs b/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalc.cs index f89b7689..94337c80 100644 --- a/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalc.cs +++ b/YLErpDAL/Modules/EodModule/SettlementModule/EodClientBalanceCalc.cs @@ -15,6 +15,7 @@ using YLErp.Model.Enum; using YLErp.Models; using YLErp.Modules.CalculationModule; using YLErp.Modules.EodModule.QueryModule; +using YLErp.Modules.SwapModule.Margin; using YLErp.Modules.TradeRiskCalcModule; namespace YLErp.Modules.EodModule.SettlementModule @@ -131,7 +132,7 @@ namespace YLErp.Modules.EodModule.SettlementModule var positionList = DbContext.trade.Where(t => (ConsTrade.TradeStatusAfterConfirmed.Contains(t.TradeStatus)||t.UnWindDate> balanceDate) && t.ValidState != "InValid"&&t.TradeType=="收益互换"); var marignQuery = from t in positionList join s in DbContext.eod_swap_position on t.id equals s.SwapTradeId - where ConsTrade.InterestMarginModels.Contains(s.InterestMode) && !s.Invalid && s.HappenDate <= balanceDate &&s.ValueDate==balanceDate + where MarginModes.ForLinq.Contains(s.InterestMode) && !s.Invalid && s.HappenDate <= balanceDate &&s.ValueDate==balanceDate select new { ClientId = t.ClientId, diff --git a/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs b/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs index d653e114..7232022f 100644 --- a/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs +++ b/YLErpDAL/Modules/SwapModule/Margin/MarginModes.cs @@ -29,6 +29,16 @@ public static class MarginModes (int)InterestModeEnum.追加预付金, }; - /// 判断 mode 是否属于保证金。 + /// + /// List 形态,供 EF Core LINQ 表达式用(HashSet.Contains 无法翻译成 SQL)。 + /// 替代 ConsTrade.InterestMarginModels。 + /// + public static readonly List ForLinq = new() + { + (int)InterestModeEnum.初始预付金, + (int)InterestModeEnum.追加预付金, + }; + + /// 判断 mode 是否属于保证金(非 LINQ 场景用)。 public static bool Contains(int interestMode) => All.Contains(interestMode); } diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 17d62d18..776b88c4 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -2314,7 +2314,7 @@ namespace YLErp.Modules.SwapModule public static decimal CalculateSwapRealizedPnl(eod_swap_position position) { var interestRatio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m; - if (ConsTrade.InterestMarginModels.Contains(position.InterestMode)) + if (MarginModes.Contains(position.InterestMode)) { interestRatio = -interestRatio; } @@ -2338,7 +2338,7 @@ namespace YLErp.Modules.SwapModule if (position.InterestDirection <= 0) return; var interestRatio = position.InterestDirection == (int)SwapDirectionEnum.收取 ? 1m : -1m; - if (ConsTrade.InterestMarginModels.Contains(position.InterestMode)) + if (MarginModes.Contains(position.InterestMode)) { interestRatio = -interestRatio; }