From aeb55a01ad06b0bff27e083b7bdecf4ac00c8548 Mon Sep 17 00:00:00 2001 From: hjhan Date: Wed, 12 Aug 2026 14:50:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(eod):=20=E6=8F=90=E5=8F=96InterestInco?= =?UTF-8?q?meCalc.DailyAccrual/RollRealized=20+=20MtmCalc.ReturnLegProfitS?= =?UTF-8?q?um?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 候选B: 3处日利息 principal*(rate+float)/annualDays → InterestIncomeCalc.DailyAccrual 候选D: 4处 PosiProfitSum=Mtm+Div+Fee → MtmCalc.ReturnLegProfitSum 候选E: 4对 RealizedInterest/RealizedInterestFee 滚存 → InterestIncomeCalc.RollRealized SwapModule零回归(7基线/510通过) --- .../ReturnLegs/InterestIncomeCalc.cs | 21 ++++++++ .../Modules/SwapModule/ReturnLegs/MtmCalc.cs | 4 ++ .../SwapModule/SwapEodPositionService.cs | 53 +++++++++---------- 3 files changed, 50 insertions(+), 28 deletions(-) create mode 100644 YLErpDAL/Modules/SwapModule/ReturnLegs/InterestIncomeCalc.cs diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/InterestIncomeCalc.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/InterestIncomeCalc.cs new file mode 100644 index 00000000..8f1b2aa4 --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/InterestIncomeCalc.cs @@ -0,0 +1,21 @@ +namespace YLErp.Modules.SwapModule.ReturnLegs; + +/// +/// EOD 利息腿展示值计算——轻量单日公式(非 SwapDealService 的分段计息引擎)。 +/// +public static class InterestIncomeCalc +{ + /// 日应计利息 = 本金 × (固定利率 + 浮动利差) ÷ 年化天数(若年化)。 + /// 原 3 处内联 `principal*(rate+float); if(annualized) /=annualDays` 收口到此。 + public static decimal DailyAccrual(decimal principal, decimal rate, decimal floatRate, bool isAnnualized, int annualDays) + { + var amount = principal * (rate + floatRate); + return isAnnualized ? amount / annualDays : amount; + } + + /// 已实现利息滚存。(prev + today×ratio, prevFee + todayFee)。 + /// 原 4 处内联 2 行赋值收口到此。 + public static (decimal Interest, decimal Fee) RollRealized( + decimal prevInterest, decimal prevFee, decimal closeInterest, decimal closeInterestFee, int ratio) + => (prevInterest + closeInterest * ratio, prevFee + closeInterestFee); +} diff --git a/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs b/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs index 4bbb5620..7fab2a8f 100644 --- a/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs +++ b/YLErpDAL/Modules/SwapModule/ReturnLegs/MtmCalc.cs @@ -19,4 +19,8 @@ public static class MtmCalc /// 收取=1, 支付=-1。 public static decimal UnrealizedPnl(decimal price, decimal costGrossPrice, decimal qty, decimal contractSize, int shortRatio, int ratio) => (price - costGrossPrice) * qty * contractSize * shortRatio * ratio; + + /// 浮动端总未实现盈亏 = 盯市盈亏 + 分红 + 待结费用。原 4 处内联收口到此。 + public static decimal ReturnLegProfitSum(decimal mtmPnl, decimal dividendSum, decimal feePending) + => mtmPnl + dividendSum + feePending; } diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index 7fa54924..d4b14f00 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -1081,11 +1081,9 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.TdCloseInterest = flowEvents.Sum(x => x.InterestAmount); newEodPayPosition.TdCloseInterestFee = newEodPayPosition.TdInterestFee; //持仓内容-利息腿-损益统计(本方视角) - var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); - if (position.IsAnnualized) - { - intersetAcmount /= tradeExtend.AnnualDays; - } + var intersetAcmount = InterestIncomeCalc.DailyAccrual( + newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate, + newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays); newEodPayPosition.TdInterestIncome = intersetAcmount;// 要算一下当天产生的利息 var interestIncomeBeforeSettlement = eodPayPosition.InterestIncomeSum + newEodPayPosition.TdInterestIncome; var interestFeeBeforeSettlement = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee; @@ -1111,8 +1109,9 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); //累计已实现 - newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; - newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; + 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)); @@ -1250,8 +1249,9 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); //累计已实现 - newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; - newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; + 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)); @@ -1408,11 +1408,9 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.TdCloseInterest = manualSettledInterestAmount + autoSettledInterestAmount; // intersetAcmount 是收盘后本金的一天应计展示值。算尾部分平仓时,下面的复利分支会改用 // 平仓前全额本金重算当天新增,但跨日携带的 TdInterestPrincipal 仍只能是剩余本金。 - var intersetAcmount = newEodPayPosition.TdInterestPrincipal * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); - if (position.IsAnnualized) - { - intersetAcmount /= tradeExtend.AnnualDays; - } + var intersetAcmount = InterestIncomeCalc.DailyAccrual( + newEodPayPosition.TdInterestPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate, + newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays); newEodPayPosition.TdInterestIncome = autoSwap ? intersetAcmount : !hasPreviousEod @@ -1462,12 +1460,9 @@ namespace YLErp.Modules.SwapModule var accrualPrincipal = calcLast ? fullPrincipal : newEodPayPosition.TdInterestPrincipal; - newEodPayPosition.TdInterestIncome = accrualPrincipal - * (newEodPayPosition.TdInterestRate + newEodPayPosition.FloatRate); - if (position.IsAnnualized) - { - newEodPayPosition.TdInterestIncome /= tradeExtend.AnnualDays; - } + newEodPayPosition.TdInterestIncome = InterestIncomeCalc.DailyAccrual( + accrualPrincipal, newEodPayPosition.TdInterestRate, newEodPayPosition.FloatRate, + newEodPayPosition.IsAnnualized, tradeExtend.AnnualDays); } if (!autoSwap && closePercent > 0m && closePercent < 1m @@ -1513,8 +1508,9 @@ namespace YLErp.Modules.SwapModule //累计已实现 // RealizedInterest 只增不回滚:上日累计已实现 + 当日结息按方向后的金额。 // 收取腿的 -37119.14 会把累计已实现更新为 -37119.14;后续普通 EOD 保持该值。 - newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; - newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; + 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)); @@ -1631,8 +1627,9 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); //累计已实现 - newEodPayPosition.RealizedInterest = eodPayPosition.RealizedInterest + newEodPayPosition.TdCloseInterest * ratio; - newEodPayPosition.RealizedInterestFee = eodPayPosition.RealizedInterestFee + newEodPayPosition.TdCloseInterestFee; + 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)); @@ -1724,7 +1721,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.TdPosiDividend = Math.Round(dividendIn * ratio, 2); newEodPayPosition.PosiMtmPnL = MtmCalc.UnrealizedPnl(newEodPayPosition.UnderlyingPrice, newEodPayPosition.PosiGrossPrice, newEodPayPosition.PosiQuantity, newEodPayPosition.ContractSize, shortRatio, (int)ratio); newEodPayPosition.PosiDividendSum = Math.Round(newEodPayPosition.TdPosiDividend - newEodPayPosition.TdCloseDividend, 2); - newEodPayPosition.PosiProfitSum = newEodPayPosition.PosiMtmPnL + newEodPayPosition.PosiDividendSum + newEodPayPosition.PosiFeePending; + newEodPayPosition.PosiProfitSum = MtmCalc.ReturnLegProfitSum(newEodPayPosition.PosiMtmPnL, newEodPayPosition.PosiDividendSum, newEodPayPosition.PosiFeePending); //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum); @@ -1797,7 +1794,7 @@ namespace YLErp.Modules.SwapModule curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio); //curretEod.TdPosiDividend = 0; //curretEod.PosiDividendSum = eod.PosiDividendSum + curretEod.TdPosiDividend; - curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending; + curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending); curretEod.TdCloseFee = 0; curretEod.TdCloseQty = 0; curretEod.TdCloseMtmPnl = 0; @@ -1910,7 +1907,7 @@ namespace YLErp.Modules.SwapModule SetFloatingRealizedPnl(curretEod); curretEod.SwapPositionValue -= curretEod.TdCloseDividend; - curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending; + curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending); if (curretEod.PosiStatus == 1) { curretEod.PosiNotionalValue = 0; @@ -2083,7 +2080,7 @@ namespace YLErp.Modules.SwapModule curretEod.UnderlyingMarketValue = MtmCalc.MarketValue(curretEod.UnderlyingPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio); curretEod.PosiMtmPnL = MtmCalc.UnrealizedPnl(curretEod.UnderlyingPrice, curretEod.PosiGrossPrice, curretEod.PosiQuantity, curretEod.ContractSize, shortRatio, directionRatio); - curretEod.PosiProfitSum = curretEod.PosiMtmPnL + curretEod.PosiDividendSum + curretEod.PosiFeePending; + curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending); curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl; curretEod.RealizedDividend = curretEod.TdCloseDividend; curretEod.RealizedFee = curretEod.TdCloseFee;