using Newtonsoft.Json; namespace YLErp.Modules.SwapModule { /// /// L1(类内去重)前置安全网:DealInterests 四分支中无 golden 语料的三格 /// (AutoSettle / AutoSettleWithClose / CloseOnly)尾部滚存字段特征化快照。 /// /// - ManualSwap / RollForward 两格已由 DealInterestsGoldenReplayTest 语料钉住 /// (字段集见 GoldenReplayFramework.EodPositionToJson)。 /// - 本测试钉"现状行为":L1 抽共享助手(腿字段拷贝段 + 滚存收尾段)前后, /// 以下字段必须逐字段不变。变化=去重改了口径。 /// - 同时断言接 seam 指纹(哪个计息接缝 + eventType)与 autoInterests 收集行为, /// 兼作 L2(按腿拆类)的路由验收。 /// - 计息金额由受控 CalcResult 注入(不连库、不依赖真实计息引擎)。 /// [TestClass] public class InterestEodTailSnapshotTest { private const decimal Principal = 10000m; private const decimal Rate = 0.03m; private static readonly DateTime StartDate = new(2026, 4, 27); private static readonly DateTime SettleDate = StartDate.AddDays(10); // 第10天收盘 private const decimal Accrued10d = 8.22m; // 受控:10天理论应结 private const decimal DailyNew = 0.82m; // 受控:当日新增 private const decimal ManualSettled = 3.5m; // 受控:盘中平仓已结 private const decimal Remaining = 7000m; // 平仓后剩余本金 private const decimal ClosedNotional = 3000m; // 本次平掉本金 private sealed class TailStubService : TestableSwapEodPositionService { public TailStubService() : base(nameof(InterestEodTailSnapshotTest)) { } /// 受控计息结果:两个计息 seam 均返回它 public List CalcResult { get; set; } = new(); public string LastCalcSeam { get; private set; } = ""; public List CalcEventTypes { get; } = new(); protected override List CalcSwapInterests( trade td, trade_extend tradeExtend, DateTime valueDate, DateTime unwindDate, List eodPositions, List positions, decimal posiNotionalValue, decimal closePosiNotionalValue, decimal closePrecent, int eventType, bool tdClose, decimal orginPv, bool add = false, bool settment = true, bool newCalcLast = false, List closeList = null) { LastCalcSeam = nameof(CalcSwapInterests); CalcEventTypes.Add(eventType); return CalcResult; } protected override List CalcEodPostCloseSettleInterests(InterestCalcRequest req) { LastCalcSeam = nameof(CalcEodPostCloseSettleInterests); CalcEventTypes.Add(req.EventType); return CalcResult; } /// 持仓延续腿重置日再定盘接缝:计数并返回受控新定盘(不连库) public decimal RefixResult { get; set; } public int RefixCalls { get; private set; } protected override decimal ResolveOngoingResetFixing(swap_position position, DateTime valueDate) { RefixCalls++; return RefixResult; } public List ExecuteDealInterests( List interestList, List eodPositions, DateTime settleDate, trade td, List flowEvents, decimal posiTotalNotional, decimal closeNational, decimal grossPrice, decimal orginPv) { var autoInterests = new List(); DealInterests(interestList, eodPositions, new List(), settleDate, td, flowEvents, autoInterests, null, posiTotalNotional, closeNational, grossPrice, orginPv); return autoInterests; } } private static trade CreateTrade() => new() { id = 1, TradeNumber = "TAIL-SNAP-001", ClientId = 999998, TradeType = "收益互换", TradeDate = StartDate, StartDate = StartDate, ExerciseDate = new DateTime(2027, 4, 27), TradeStatus = "确认成交", ValidState = "Valid", StructureType = "单标的", QuoteCurrency = "CNY", SettlementCurrency = "CNY", trade_extend = new trade_extend { TradeId = 1, ExtendJson = JsonConvert.SerializeObject(new TradeExtendJson { AnnualDays = 365, InterestCalcMode = "10", SettlementRules = 0 }) } }; /// true=当日观察日(Settlement=1);false=观察日在别日 private static swap_position CreateInterestPosition(bool observationDay) { var interval = observationDay ? new IntervalModel { Date = SettleDate, Rate = Rate, Settlement = 1 } : new IntervalModel { Date = StartDate, Rate = Rate, Settlement = 1 }; return new swap_position { id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate, InterestPrincipalFix = Principal, PosiStartDate = StartDate, PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1, interest_rule = 0, InterestSwapInterval = JsonConvert.SerializeObject(new List { interval }) }; } private static eod_swap_position CreatePreEod(decimal accumulated) => new() { id = 100, PositionId = 1001, ValueDate = SettleDate.AddDays(-1), InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, InterestIncomeSum = accumulated, InterestProfitSum = accumulated, InterestRateDefault = Rate, TdInterestPrincipal = Principal, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 1 }; private static swap_flow_event CreateCalcResult() => new() { EventType = (int)SwapEventTypeEnum.自动互换, PositionId = 1001, InterestAmount = Accrued10d, TdInterestAmount = DailyNew, InterestClosePnL = Accrued10d, InterestPrincipal = Principal, InterestRate = Rate, InterestDirection = (int)SwapDirectionEnum.收取 }; private static swap_flow_event CreateCloseEvent() => new() { EventType = (int)SwapEventTypeEnum.平仓, PositionId = 1001, InterestAmount = ManualSettled, InterestClosePnL = ManualSettled, InterestRate = Rate, InterestFee = 0m, InterestPrincipal = ClosedNotional, InterestDirection = (int)SwapDirectionEnum.收取, DataState = (int)SwapFlowDateStateEnum.完成 }; /// AutoSettle 格:观察日无平仓 → SaveAutoEodInterestPosition,返回值收集进 autoInterests [TestMethod] public void AutoSettle_观察日无平仓_尾部快照() { var service = new TailStubService { CalcResult = new List { CreateCalcResult() } }; var autoInterests = service.ExecuteDealInterests( new List { CreateInterestPosition(observationDay: true) }, new List { CreatePreEod(Accrued10d) }, SettleDate, CreateTrade(), new List(), Principal, 0m, 100m, Principal); Assert.AreEqual("CalcSwapInterests", service.LastCalcSeam, "观察日无平仓应走 CalcSwapInterests seam"); Assert.AreEqual((int)SwapEventTypeEnum.自动互换, service.CalcEventTypes.Single(), "eventType 应为自动互换"); Assert.AreEqual(1, autoInterests.Count, "观察日分支应收集返回值进 autoInterests(→资金记录)"); var p = service.PersistedPositions.Single(); // 钉值于 2026-08-17 现状行为(受控输入:应结8.22/新增0.82/本金10000) Assert.AreEqual(8.22m, p.TdCloseInterest); Assert.AreEqual(0.82m, p.TdInterestIncome); Assert.AreEqual(10000m, p.TdInterestPrincipal); Assert.AreEqual(0.03m, p.TdInterestRate); Assert.AreEqual(0.00m, p.InterestIncomeSum, "应结=结算,待实现清零"); Assert.AreEqual(0m, p.InterestFeeSum); Assert.AreEqual(0m, p.InterestProfitSum); Assert.AreEqual(8.22m, p.RealizedInterest); Assert.AreEqual(0m, p.RealizedInterestFee); Assert.AreEqual(0m, p.SwapPositionValue); Assert.AreEqual(1.0m, p.TdCurrency); } /// AutoSettleWithClose 格(TEST-MATRIX §6 最弱格):观察日+平仓 → SaveAutoEodWithCloseInterestPosition(autoSwap:true),补结差额=恒1全额−盘中已结 [TestMethod] public void AutoSettleWithClose_观察日加平仓_尾部快照() { var service = new TailStubService { CalcResult = new List { CreateCalcResult() } }; var autoInterests = service.ExecuteDealInterests( new List { CreateInterestPosition(observationDay: true) }, new List { CreatePreEod(Accrued10d) }, SettleDate, CreateTrade(), new List { CreateCloseEvent() }, Remaining, ClosedNotional, 100m, Remaining); Assert.AreEqual("CalcEodPostCloseSettleInterests", service.LastCalcSeam, "观察日+平仓应走 EodPostCloseSettle seam"); Assert.AreEqual((int)SwapEventTypeEnum.自动互换, service.CalcEventTypes.Single(), "autoSwap=true → eventType=自动互换"); Assert.AreEqual(1, autoInterests.Count, "观察日分支应收集返回值进 autoInterests"); Assert.AreEqual(Accrued10d - ManualSettled, autoInterests[0].InterestAmount, "补结差额=恒1全额8.22−盘中已结3.50"); var p = service.PersistedPositions.Single(); // 钉值于 2026-08-17 现状行为(受控输入:恒1全额8.22/盘中已结3.5/剩余7000/平掉3000) Assert.AreEqual(8.22m, p.TdCloseInterest, "TdCloseInterest=盘中已结3.50+补结4.72"); Assert.AreEqual(0.5753424657534246575342465753m, p.TdInterestIncome, "autoSwap 重算展示应计=剩余7000×3%/365"); Assert.AreEqual(7000m, p.TdInterestPrincipal, "单利部分平仓:跨日本金=剩余"); Assert.AreEqual(0.03m, p.TdInterestRate); Assert.AreEqual(0.00m, p.InterestIncomeSum, "恒1口径:理论应结8.22−结算8.22=0"); Assert.AreEqual(0m, p.InterestFeeSum); Assert.AreEqual(0m, p.InterestProfitSum); Assert.AreEqual(8.22m, p.RealizedInterest); Assert.AreEqual(0m, p.RealizedInterestFee); Assert.AreEqual(0m, p.SwapPositionValue); Assert.AreEqual(1.0m, p.TdCurrency); } /// CloseOnly 格:非观察日平仓 → SaveAutoEodWithCloseInterestPosition(autoSwap:false),返回值不收集,TdCloseInterest=盘中已结 [TestMethod] public void CloseOnly_非观察日平仓_尾部快照() { var service = new TailStubService { CalcResult = new List { CreateCalcResult() } }; var autoInterests = service.ExecuteDealInterests( new List { CreateInterestPosition(observationDay: false) }, new List { CreatePreEod(Accrued10d) }, SettleDate, CreateTrade(), new List { CreateCloseEvent() }, Remaining, ClosedNotional, 100m, Remaining); Assert.AreEqual("CalcEodPostCloseSettleInterests", service.LastCalcSeam, "纯平仓应走 EodPostCloseSettle seam"); Assert.AreEqual((int)SwapEventTypeEnum.平仓, service.CalcEventTypes.Single(), "autoSwap=false → eventType=平仓"); Assert.AreEqual(0, autoInterests.Count, "纯平仓分支不收集返回值(结算已在盘中流水定格)"); var p = service.PersistedPositions.Single(); // 钉值于 2026-08-17 现状行为(受控输入:恒1重算8.22/盘中已结3.5/剩余7000/平掉3000) Assert.AreEqual(ManualSettled, p.TdCloseInterest, "TdCloseInterest 应仅为盘中已结3.50,不叠加恒1重算值"); Assert.AreEqual(0.5753424657534246575342465753m, p.TdInterestIncome, "不算尾路径:剩余7000×3%/365"); Assert.AreEqual(7000m, p.TdInterestPrincipal, "单利部分平仓:跨日本金=剩余"); Assert.AreEqual(0.03m, p.TdInterestRate, "非观察日:利率取平仓流水 InterestRate"); Assert.AreEqual(5.295342465753m, p.InterestIncomeSum, "尾差递推:上日8.22+新增0.575342−已结3.50"); Assert.AreEqual(0m, p.InterestFeeSum); Assert.AreEqual(5.295342465753m, p.InterestProfitSum); Assert.AreEqual(3.5m, p.RealizedInterest); Assert.AreEqual(0m, p.RealizedInterestFee); Assert.AreEqual(5.295342465753m, p.SwapPositionValue); Assert.AreEqual(1.0m, p.TdCurrency); } #region 持仓延续腿重置日再定盘(EQD-6968 自洽化:快照利率载体) private const decimal OldFloat = 0.01425m; private const decimal NewFloat = 0.0143m; /// 4/27+14:7 天周期的重置日平仓 private static readonly DateTime ResetSettle = StartDate.AddDays(14); /// 4/27+10:非重置日平仓(10%7≠0) private static readonly DateTime NonResetSettle = StartDate.AddDays(10); private static swap_position CreateFloatLegPosition() => new() { id = 1001, SwapTradeId = 1, InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, InterestRateDefault = Rate, InterestPrincipalFix = Principal, PosiStartDate = StartDate, PosiMatuirityDate = new DateTime(2027, 4, 27), IsInitial = true, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 7, interest_rule = 0, FloatRateUnderlyingCode = "FR007", InterestSwapInterval = JsonConvert.SerializeObject(new List { new IntervalModel { Date = StartDate, Rate = Rate, Settlement = 1 } }) }; private static eod_swap_position CreatePreEodBefore(DateTime settle, decimal accumulated) => new() { id = 100, PositionId = 1001, ValueDate = settle.AddDays(-1), InterestDirection = (int)SwapDirectionEnum.收取, InterestMode = (int)InterestModeEnum.标的期初全价, InterestIncomeSum = accumulated, InterestProfitSum = accumulated, InterestRateDefault = Rate, TdInterestPrincipal = Principal, InterestType = (int)InterestTypeEnum.单利, IsAnnualized = true, interest_rest_days = 7 }; private static swap_flow_event CalcResultWithFloat(decimal floatRate) { var e = CreateCalcResult(); e.FloatRate = floatRate; return e; } /// /// 平仓日恰为重置日且剩余持仓>0:快照 FloatRate 必须显式再定盘为当日新定盘—— /// 它是后续非重置日(ByEod 沿用 preEod.FloatRate)与当日应计(intersetAcmount)的利率载体。 /// 排除日"纯跳过"后事件利率=末段已消费利率(OldFloat),载体职责与本步骤显式分离。 /// [TestMethod] public void CloseOnly_平仓日为重置日_剩余持仓快照再定盘() { var service = new TailStubService { CalcResult = new List { CalcResultWithFloat(OldFloat) }, RefixResult = NewFloat, }; service.ExecuteDealInterests( new List { CreateFloatLegPosition() }, new List { CreatePreEodBefore(ResetSettle, Accrued10d) }, ResetSettle, CreateTrade(), new List { CreateCloseEvent() }, Remaining, ClosedNotional, 100m, Remaining); Assert.AreEqual(1, service.RefixCalls, "不算尾+平仓日=重置日+剩余>0:应恰好显式再定盘一次"); Assert.AreEqual(NewFloat, service.PersistedPositions.Single().FloatRate, "剩余持仓快照利率=当日新定盘(非事件末段旧利率)"); } [TestMethod] public void CloseOnly_平仓日非重置日_不再定盘_快照沿用事件利率() { var service = new TailStubService { CalcResult = new List { CalcResultWithFloat(OldFloat) }, RefixResult = NewFloat, }; service.ExecuteDealInterests( new List { CreateFloatLegPosition() }, new List { CreatePreEodBefore(NonResetSettle, Accrued10d) }, NonResetSettle, CreateTrade(), new List { CreateCloseEvent() }, Remaining, ClosedNotional, 100m, Remaining); Assert.AreEqual(0, service.RefixCalls, "非重置日平仓:无需再定盘"); Assert.AreEqual(OldFloat, service.PersistedPositions.Single().FloatRate, "快照沿用事件末段已消费利率(周期未切换)"); } #endregion } }