From 09fa96a93faaef2610f5ce6364ac3088a8dcf7af Mon Sep 17 00:00:00 2001 From: hjhan Date: Mon, 17 Aug 2026 18:15:09 +0800 Subject: [PATCH] =?UTF-8?q?refactor(swap-eod):=20L1=20=E7=B1=BB=E5=86=85?= =?UTF-8?q?=E5=8E=BB=E9=87=8D=E2=80=94=E2=80=94CopyInterestLegFields/Final?= =?UTF-8?q?izeInterestEodRoll=20=E6=94=B6=E5=8F=A3=E5=9B=9B=20Save*=20?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E6=AE=B5=EF=BC=88=E8=A1=8C=E4=B8=BA=E4=BF=9D?= =?UTF-8?q?=E7=9C=9F=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 腿字段拷贝 3 处×14 行 → 入参化 helper(FloatRate 来源随场景传入,勿统一) - 滚存收尾 4 处 → FinalizeInterestEodRoll(方向源参数化:三处 position、Copy 一处 eodPayPosition,勿统一;Persist 与日志留调用点保持原序) - 差异保真:WithClose 不设 InterestFeePending;勿缩放注释保留升级 - 安全网:InterestEodTailSnapshotTest 三格尾部快照(AutoSettle/AutoSettleWithClose/CloseOnly,钉值特征化)+ golden 两格 + DI_EXCEL 家族 - 验证:快照 3/3 逐字段一致;全量 SwapModule 576/566/10/0 与去重前基线完全相同 - 注:本提交为工作区被还原后按原编辑重放,验证结果与首次执行一致 --- .../SwapModule/InterestEodTailSnapshotTest.cs | 227 ++++++++++++++++++ .../SwapModule/SwapEodPositionService.cs | 133 +++++----- 2 files changed, 282 insertions(+), 78 deletions(-) create mode 100644 UnitTestProject/Modules/SwapModule/InterestEodTailSnapshotTest.cs diff --git a/UnitTestProject/Modules/SwapModule/InterestEodTailSnapshotTest.cs b/UnitTestProject/Modules/SwapModule/InterestEodTailSnapshotTest.cs new file mode 100644 index 00000000..01cec7cb --- /dev/null +++ b/UnitTestProject/Modules/SwapModule/InterestEodTailSnapshotTest.cs @@ -0,0 +1,227 @@ +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 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); + } + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index a7992ee2..e29a7b81 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -980,6 +980,46 @@ namespace YLErp.Modules.SwapModule trans?.Dispose(); } } + /// + /// 利息腿字段拷贝(SaveEodInterestPosition / SaveAutoEodInterestPosition / SaveAutoEodWithCloseInterestPosition 共用)。 + /// FloatRate 来源随场景不同(手工互换=当日流水;自动互换/平仓=计息结果),由调用方算好传入,勿在本方法内统一。 + /// 场景差异字段(PosiStatus / InterestFeePending / TdInterestPrincipal / TdInterestRate)留在各调用点。 + /// + private static void CopyInterestLegFields(eod_swap_position newEodPayPosition, swap_position position, decimal floatRate) + { + newEodPayPosition.InterestDirection = position.InterestDirection; + newEodPayPosition.InterestMode = position.InterestMode; + newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix; + newEodPayPosition.InterestRateDefault = position.InterestRateDefault; + newEodPayPosition.InterestSwapInterval = position.InterestSwapInterval; + newEodPayPosition.IsAnnualized = position.IsAnnualized; + newEodPayPosition.HappenDate = position.HappenDate; + newEodPayPosition.Currency = position.Currency; + newEodPayPosition.InterestType = position.InterestType; + newEodPayPosition.interest_rest_days = position.interest_rest_days; + newEodPayPosition.interest_rule = position.interest_rule; + newEodPayPosition.FloatRate = floatRate; + newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode; + } + + /// + /// 利息腿日终滚存收尾(四个 Save* 共用):RollRealized 滚累计已实现 → SetFixedLegRealizedPnl → 汇率 → TdCurrency。 + /// RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。 + /// interestDirection 是 RateType 的方向来源——三个方法取 position.InterestDirection, + /// SaveEodInterestPositionCopy 取 eodPayPosition.InterestDirection(现状差异,勿统一)。 + /// PersistEodSwapPosition 与各自日志留在调用点(持久化边界 + 日志顺序各不相同)。 + /// + private void FinalizeInterestEodRoll(eod_swap_position eodPayPosition, eod_swap_position newEodPayPosition, int ratio, trade td, DateTime valueDate, int interestDirection) + { + var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio); + newEodPayPosition.RealizedInterest = rolled.Interest; + newEodPayPosition.RealizedInterestFee = rolled.Fee; + SetFixedLegRealizedPnl(newEodPayPosition); + var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, + DirectionRatio.RateType(interestDirection)); + newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); + } + /// /// 产生互换用 /// @@ -1023,20 +1063,8 @@ namespace YLErp.Modules.SwapModule UpdateDbOption(newEodPayPosition); newEodPayPosition.PosiStatus = 0; newEodPayPosition.Invalid = false; - //持仓内容-利息腿 - newEodPayPosition.InterestDirection = position.InterestDirection; - newEodPayPosition.InterestMode = position.InterestMode; - newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix; - newEodPayPosition.InterestRateDefault = position.InterestRateDefault; - newEodPayPosition.InterestSwapInterval = position.InterestSwapInterval; - newEodPayPosition.IsAnnualized = position.IsAnnualized; - newEodPayPosition.HappenDate = position.HappenDate; - newEodPayPosition.Currency = position.Currency; - newEodPayPosition.InterestType = position.InterestType; - newEodPayPosition.interest_rest_days = position.interest_rest_days; - newEodPayPosition.interest_rule = position.interest_rule; - newEodPayPosition.FloatRate = flowEvents.FirstOrDefault()?.FloatRate ?? 0; - newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode; + //持仓内容-利息腿(FloatRate 取当日互换/平仓流水) + CopyInterestLegFields(newEodPayPosition, position, flowEvents.FirstOrDefault()?.FloatRate ?? 0); newEodPayPosition.InterestFeePending = 0; //利息端估值用信息 newEodPayPosition.TdInterestPrincipal = flowEvents.FirstOrDefault()?.InterestPrincipal ?? 0; @@ -1074,14 +1102,8 @@ namespace YLErp.Modules.SwapModule //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); - //累计已实现 - var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio); - newEodPayPosition.RealizedInterest = rolled.Interest; - newEodPayPosition.RealizedInterestFee = rolled.Fee; - SetFixedLegRealizedPnl(newEodPayPosition); - var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, - DirectionRatio.RateType(position.InterestDirection)); - newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); + //累计已实现(滚存收尾见 FinalizeInterestEodRoll) + FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection); PersistEodSwapPosition(newEodPayPosition); } /// @@ -1179,20 +1201,8 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.PositionId = position.id; UpdateDbOption(newEodPayPosition); newEodPayPosition.Invalid = false; - //持仓内容-利息腿 - newEodPayPosition.InterestDirection = position.InterestDirection; - newEodPayPosition.InterestMode = position.InterestMode; - newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix; - newEodPayPosition.InterestRateDefault = position.InterestRateDefault; - newEodPayPosition.InterestSwapInterval = position.InterestSwapInterval; - newEodPayPosition.IsAnnualized = position.IsAnnualized; - newEodPayPosition.HappenDate = position.HappenDate; - newEodPayPosition.Currency = position.Currency; - newEodPayPosition.InterestType = position.InterestType; - newEodPayPosition.interest_rest_days = position.interest_rest_days; - newEodPayPosition.interest_rule = position.interest_rule; - newEodPayPosition.FloatRate = interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0; - newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode; + //持仓内容-利息腿(FloatRate 取计息结果) + CopyInterestLegFields(newEodPayPosition, position, interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0); newEodPayPosition.InterestFeePending = 0; //利息端估值用信息 newEodPayPosition.TdInterestPrincipal = interestModes.Contains(position.InterestMode) ? eodPayPosition.InterestPrincipalFix : posiNotionalValue; @@ -1216,14 +1226,8 @@ namespace YLErp.Modules.SwapModule //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); - //累计已实现 - var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio); - newEodPayPosition.RealizedInterest = rolled.Interest; - newEodPayPosition.RealizedInterestFee = rolled.Fee; - SetFixedLegRealizedPnl(newEodPayPosition); - var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, - DirectionRatio.RateType(position.InterestDirection)); - newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); + //累计已实现(滚存收尾见 FinalizeInterestEodRoll) + FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection); PersistEodSwapPosition(newEodPayPosition); Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); return interests; @@ -1354,22 +1358,9 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.PositionId = position.id; UpdateDbOption(newEodPayPosition); newEodPayPosition.Invalid = false; - //持仓内容-利息腿 - newEodPayPosition.InterestDirection = position.InterestDirection; - newEodPayPosition.InterestMode = position.InterestMode; - // ResolveInterestLegPositions 已提供平仓后的实时剩余本金,日终不再重复扣减。 - newEodPayPosition.InterestPrincipalFix = position.InterestPrincipalFix; - // newEodPayPosition.InterestPrincipalFix *= (1 - closePercent); - newEodPayPosition.InterestRateDefault = position.InterestRateDefault; - newEodPayPosition.InterestSwapInterval = position.InterestSwapInterval; - newEodPayPosition.IsAnnualized = position.IsAnnualized; - newEodPayPosition.HappenDate = position.HappenDate; - newEodPayPosition.Currency = position.Currency; - newEodPayPosition.InterestType = position.InterestType; - newEodPayPosition.FloatRate = interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0; - newEodPayPosition.FloatRateUnderlyingCode = position.FloatRateUnderlyingCode; - newEodPayPosition.interest_rest_days = position.interest_rest_days; - newEodPayPosition.interest_rule = position.interest_rule; + //持仓内容-利息腿(FloatRate 取计息结果)。InterestPrincipalFix 保持腿现值: + // ResolveInterestLegPositions 已提供平仓后的实时剩余本金,日终不再重复扣减(勿恢复 *(1-closePercent))。 + CopyInterestLegFields(newEodPayPosition, position, interests.Count > 0 ? interests.First().FloatRate ?? 0 : 0); //利息端估值用信息 // TdInterestPrincipal 是“下一日继续计息的收盘后本金”,不是原始合同规模,也不是本次平仓本金。 // 模式9单利直接取剩余名义本金;复利还要保留重置时已经并入本金的待实现利息。 @@ -1491,16 +1482,8 @@ namespace YLErp.Modules.SwapModule $",TdCloseInterest is {newEodPayPosition.TdCloseInterest}"); Log.Info($"InterestFeeSum is {eodPayPosition.InterestFeeSum},TdInterestFee is {newEodPayPosition.TdInterestFee}" + $",TdCloseInterestFee is {newEodPayPosition.TdCloseInterestFee}"); - //累计已实现 - // RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。 - // 收取腿的 -37119.14 会把累计已实现更新为 -37119.14;后续普通 EOD 保持该值。 - var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio); - newEodPayPosition.RealizedInterest = rolled.Interest; - newEodPayPosition.RealizedInterestFee = rolled.Fee; - SetFixedLegRealizedPnl(newEodPayPosition); - var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, - DirectionRatio.RateType(position.InterestDirection)); - newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); + //累计已实现(滚存语义见 FinalizeInterestEodRoll:只增不回滚) + FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, position.InterestDirection); Log.Info($"即将插入数据库的 newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); PersistEodSwapPosition(newEodPayPosition); return interests; @@ -1614,14 +1597,8 @@ namespace YLErp.Modules.SwapModule //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); - //累计已实现 - var rolled = InterestIncomeCalc.RollRealized(eodPayPosition.RealizedInterest, eodPayPosition.RealizedInterestFee, newEodPayPosition.TdCloseInterest, newEodPayPosition.TdCloseInterestFee, ratio); - newEodPayPosition.RealizedInterest = rolled.Interest; - newEodPayPosition.RealizedInterestFee = rolled.Fee; - SetFixedLegRealizedPnl(newEodPayPosition); - var currencyRate = GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, valueDate, true, - DirectionRatio.RateType(eodPayPosition.InterestDirection)); - newEodPayPosition.TdCurrency = Convert.ToDecimal(currencyRate); + //累计已实现(滚存收尾见 FinalizeInterestEodRoll;方向源=eodPayPosition,与其他三方法不同,勿统一) + FinalizeInterestEodRoll(eodPayPosition, newEodPayPosition, ratio, td, valueDate, eodPayPosition.InterestDirection); Log.Info($"the last newEodPayPosition is {JsonHelper.Serialize(newEodPayPosition, false)}"); PersistEodSwapPosition(newEodPayPosition);