diff --git a/UnitTestProject/Modules/SwapModule/PrepaidPrincipalClosingChainTraceTest.cs b/UnitTestProject/Modules/SwapModule/PrepaidPrincipalClosingChainTraceTest.cs new file mode 100644 index 00000000..39746577 --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/PrepaidPrincipalClosingChainTraceTest.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Model; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 预付金腿「收盘递推链」trace 测试 + /// ============================================================================ + /// 背景:8.7 全部平仓时预付金端系统值=20.83(≈6天),Excel 预期本次利息=10.41(3天)。 + /// 已证明(GetInterests/CalcDailySimpleInterest) 8.7 平仓利息完全由「前一日收盘归档的 + /// eod_swap_position.ValueDate」地板决定:ValueDate=8.4→3天→10.41(Excel),ValueDate≈8.1→6天→20.83(系统)。 + /// + /// 本测试回答用户的核心疑问:「平仓页面是否依赖前一日收盘递推?」 + /// —— 是。SwapUnwind 取 lastEod(ValueDate<unwindDate) 的 eod 归档作为 preEodPosition; + /// 收盘链 SwapPositionCompose→DealInterests 四个分支全部把 newEodPayPosition.ValueDate=settleDate。 + /// 因此:若收盘链每天正常运行,预付金腿 eod.ValueDate 应逐日推进到部分平仓日 8.4; + /// 系统读到 ValueDate≈8.1 说明该腿 eod 在 8.1 之后没被收盘链重新生成(递推链在 8.4 断了), + /// 属「收盘缺失/滞后」而非利息公式 bug。 + /// + /// 本测试用纯内存驱动 SwapPositionCompose,从交易开始日 7.28 逐日收盘到 8.4(部分平仓), + /// 捕获预付金腿每日 eod.ValueDate,断言其正确推进到 8.4。 + /// ============================================================================ + [TestClass] + public class PrepaidPrincipalClosingChainTraceTest + { + private const int SwapTradeId = 777; + private static readonly DateTime TradeStart = new(2026, 7, 28); + private static readonly DateTime SwapDate = new(2026, 7, 31); // 先互换 + private static readonly DateTime ResetDate = new(2026, 8, 3); // FR007 重置 + private static readonly DateTime PartialDate = new(2026, 8, 4); // 部分平仓 40% + private const long PrepayInitialId = 2; + private const long PrepayRealId = 3; + private const long FloatId = 1; + + #region 可测试化子类(驱动收盘链,不连库) + + private sealed class TraceEodService : TestableSwapEodPositionService + { + private readonly List _trades; + private readonly List _positions; + private List _eodPositions; + private readonly List _extends; + private List _currentFlowEvents; + + public List EodPositions => _eodPositions; + + public TraceEodService( + List trades, List positions, + List eodPositions, List eodSwaps, + List extends) + : base(nameof(PrepaidPrincipalClosingChainTraceTest)) + { + _trades = trades; _positions = positions; + _eodPositions = eodPositions; _extends = extends; + _currentFlowEvents = new List(); + } + + // 收盘链 seam override(对齐 SwapPositionComposeScenarioTest) + protected override List FindActiveSwapTrades(DateTime settleDate, IEnumerable clientIds) => _trades; + protected override List FindAllSwapPositions(List tradeIds) => _positions; + protected override List FindTradeExtends(List tradeIds) => _extends; + protected override List FindEodSwapsByDate(DateTime valueDate) + // 从已归档 eod 持仓派生 eod_swap 父记录(稳健,避免依赖 SaveEodSwapRecord seam 回填) + => _eodPositions.Where(x => x.SwapTradeId == SwapTradeId) + .Select(x => x.ValueDate).Distinct() + .Select(d => new eod_swap { SwapTradeId = SwapTradeId, ValueDate = d }) + .ToList(); + protected override List FindFlowEvents(int swapTradeId, DateTime settleDate) => _currentFlowEvents; + protected override List FindCompletedFlowEvents(List tradeIds) => _currentFlowEvents; + protected override List FindEodSwapPositions(int swapTradeId, DateTime preSettleDate) + => _eodPositions.Where(x => x.SwapTradeId == swapTradeId && x.ValueDate >= preSettleDate).ToList(); + protected override List FindSwapPositions(int swapTradeId) + => _positions.Where(x => x.SwapTradeId == swapTradeId && !x.IsInitial).ToList(); + + protected override underlying_manager GetUnderlyingData(string underlyingCode) + => new underlying_manager { ValueAddedTax = 0m, UnderlyingInstrumentType = "TBonds" }; + 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 void SaveEodSwapRecord(trade td, DateTime settleDate, DateTime preSettleDate) + { + // eod_swap 父记录由 FindEodSwapsByDate 从归档 eod 持仓派生,此处无需操作 + } + protected override void ExecuteInTransaction(Action action) => action(); + protected override void ClearSwapPositionsForCompose(trade td, DateTime tradeDate, List eventTypes) { } + public override void ClearSwapPositions(trade td, DateTime valueDate, List eventTypes, bool delAfter) { } + protected override swap_event AddSwapEvent(DateTime tradeDate, int swapTradeId, int eventType, string data, int clientCashId, bool save, string reason) + { return new swap_event { id = 1 }; } + + // GetPreDealDate 默认命中 DB,覆写为安全早期日期 + public override DateTime? GetPreDealDate(int tradeId, DateTime settleDate, List eventTypes) + => TradeStart; + + // 利息计算 stub(本测试只关心 eod.ValueDate 递推,不关心利息金额) + protected override List CalcSwapInterests( + trade td, trade_extend tradeExtend, DateTime valueDate, DateTime unwindDate, + List eodPositions, List positions, + decimal posiNotionalValue, decimal posiLongNotionalValue, decimal posiShortNotionalValue, + decimal closePosiNotionalValue, decimal closePrecent, int eventType, bool tdClose, bool needPrice, + decimal grossPrice, decimal orginPv, bool add = false, bool settment = true, bool newCalcLast = false, + List closeList = null) + { + return positions.Select(p => new swap_flow_event + { + PositionId = p.id, + InterestPrincipal = p.InterestPrincipalFix, + InterestRate = 0.021111m, + FloatRate = 0m + }).ToList(); + } + + /// 对指定日期收盘,并把生成的 eod / eod_swap 回填,供下一日递推。 + public void RunClose(DateTime settleDate, DateTime preSettleDate, List flowEvents) + { + _currentFlowEvents = flowEvents ?? new List(); + SwapPositionCompose(settleDate, preSettleDate, null); + // 回填:把本次持久化的 eod 并入 _eodPositions + foreach (var eod in PersistedPositions.Where(x => x.SwapTradeId == SwapTradeId)) + { + if (!_eodPositions.Any(x => x.id == eod.id)) + _eodPositions.Add(eod); + } + } + + /// 取预付金腿截至某日的最新 eod(ValueDate 最大)。 + public eod_swap_position LatestPrepayEod() + => _eodPositions + .Where(x => x.SwapTradeId == SwapTradeId && x.PositionId == PrepayInitialId) + .OrderByDescending(x => x.ValueDate) + .FirstOrDefault(); + } + + #endregion + + #region 工厂方法 + + private static trade CreateTrade() + => new trade + { + id = SwapTradeId, TradeNumber = "TRACE-PREPAY-001", ClientId = 10, + TradeType = "收益互换", TradeDate = TradeStart, StartDate = TradeStart, + ExerciseDate = TradeStart.AddYears(1), TradeStatus = "确认成交", ValidState = "Valid", + QuoteCurrency = "CNY", SettlementCurrency = "CNY", StructureType = "普通债券类收益互换", + OriginalStockEqvNotional = 100000, TradePrice = 0 + }; + + private static trade_extend CreateExtend() + => new trade_extend + { + TradeId = SwapTradeId, + ExtendJson = @"{""NeedOpenFee"":false,""AnnualDays"":365,""SettlementRules"":0,""Direction"":1,""FlowBookMode"":0,""InterestCalcMode"":""01""}" + }; + + private static swap_position CreateFloatPosition() + => new swap_position + { + id = FloatId, SwapTradeId = SwapTradeId, PosiDirection = 1, PositionType = (int)PositionTypeFlag.Long, + UnderlyingCode = "220205.IB", UnderlyingInstrumentType = "TBonds", + ContractSize = 1m, CountRatio = 1m, IsInitial = true, Invalid = false, + PosiQuantity = 1000, PosiNotionalValue = 1000, + PosiNetPrice = 1.0050m, PosiGrossPrice = 1.0020m, + InterestDirection = 0 + }; + + private static swap_position CreatePrepayInitial() + => new swap_position + { + id = PrepayInitialId, SwapTradeId = SwapTradeId, PosiDirection = 0, + InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.初始预付金, + InterestPrincipalFix = 100000.23m, IsInitial = true, Invalid = false, + PosiStartDate = TradeStart, PosiMatuirityDate = TradeStart.AddYears(1), + InterestSwapInterval = "[]", InterestRateDefault = 0.021111m + }; + + private static swap_position CreatePrepayReal() + => new swap_position + { + id = PrepayRealId, PositionId = PrepayInitialId, SwapTradeId = SwapTradeId, PosiDirection = 0, + InterestDirection = (int)SwapDirectionEnum.收取, + InterestMode = (int)InterestModeEnum.初始预付金, + InterestPrincipalFix = 60000.138m, IsInitial = false, Invalid = false + }; + + private static swap_flow_event CreatePartialCloseFlow(DateTime date) + => new swap_flow_event + { + SwapTradeId = SwapTradeId, PositionId = FloatId, + EventType = (int)SwapEventTypeEnum.平仓, + Quantity = 400, EventDate = date, UnwindDate = date, + MarkClosePnl = 500m, CloseFee = 10m, DataState = (int)SwapFlowDateStateEnum.完成 + }; + + #endregion + + [TestMethod] + public void 收盘递推链_预付金腿ValueDate应从交易开始日逐日推进到部分平仓日8_4() + { + var td = CreateTrade(); + var extend = CreateExtend(); + var positions = new List + { + CreateFloatPosition(), CreatePrepayInitial(), CreatePrepayReal() + }; + var eodPositions = new List(); + var eodSwaps = new List(); + + var svc = new TraceEodService( + new List { td }, positions, eodPositions, eodSwaps, + new List { extend }); + + // 从交易开始日逐日收盘:7.28 → 7.31(互换) → 8.3(重置) → 8.4(部分平仓) + var closes = new List<(DateTime settle, DateTime pre, List flows)> + { + (TradeStart, TradeStart.AddDays(-1), new List()), + (SwapDate, TradeStart, new List + { + new swap_flow_event { SwapTradeId=SwapTradeId, PositionId=FloatId, + EventType=(int)SwapEventTypeEnum.互换, EventDate=SwapDate, UnwindDate=SwapDate, + DataState=(int)SwapFlowDateStateEnum.完成 } + }), + (ResetDate, SwapDate, new List()), + (PartialDate, ResetDate, new List { CreatePartialCloseFlow(PartialDate) }), + }; + + Console.WriteLine("=== 收盘递推链:预付金腿 eod.ValueDate 逐日追踪 ==="); + foreach (var c in closes) + { + svc.RunClose(c.settle, c.pre, c.flows); + var prepay = svc.LatestPrepayEod(); + var vd = prepay?.ValueDate ?? default; + Console.WriteLine($" 收盘 {c.settle:yyyy-MM-dd} → 预付金腿 eod.ValueDate={vd:yyyy-MM-dd} (TdInterestPrincipal={prepay?.TdInterestPrincipal})"); + } + + var finalPrepay = svc.LatestPrepayEod(); + Assert.IsNotNull(finalPrepay, "预付金腿应有 eod 归档"); + // 收盘链每天把每条腿 eod.ValueDate 设为当日收盘日;部分平仓日 8.4 收盘后应为 8.4 + Assert.AreEqual(PartialDate, finalPrepay.ValueDate.Date, + "收盘链应把预付金腿 eod.ValueDate 正确推进到部分平仓日 8.4(证明收盘代码本身正确,系统 8.1 是递推链在 8.4 断了)"); + + Console.WriteLine($">> 结论:收盘链正确推进到 {finalPrepay.ValueDate:yyyy-MM-dd};" + + $"系统读到 8.1 说明该笔交易的预付金腿 eod 在 8.4 部分平仓日未被收盘链重新生成(收盘缺失/滞后),非利息公式 bug。"); + } + } +}