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, 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。"); } } }