diff --git a/YLErpDAL/Modules/SwapModule/EodPnlCalculator.cs b/YLErpDAL/Modules/SwapModule/EodPnlCalculator.cs new file mode 100644 index 00000000..3532be45 --- /dev/null +++ b/YLErpDAL/Modules/SwapModule/EodPnlCalculator.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using YLErp; +using YLErp.DBModels; +using YLErp.DBModels.Enums; +using YLErp.Modules.SwapModule.ReturnLegs; + +namespace YLErp.Modules.SwapModule +{ + /// + /// 互换日终盈亏/精度计算纯函数集合。 + /// 自 SwapEodPositionService 抽出,支持无库单测;同类内部调用无需前缀。 + /// + public static class EodPnlCalculator + { + // 日终利息待实现需跨日累计,按表设计保留 12 位;已实现结算仍按金额两位处理。 + private const int EodInterestStoragePrecision = 12; + + internal static decimal RoundMoney(decimal value) + { + return Math.Round(value, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + } + + internal static decimal RoundEodInterest(decimal value) + { + return Math.Round(value, EodInterestStoragePrecision, MidpointRounding.AwayFromZero); + } + + /// + /// 仅在写入 eod_swap_position 前统一快照精度。 + /// 浮动腿收益最终以金额两位展示和存储;利息腿的待实现、计息基数及利率保留 12 位, + /// 使部分结算后的尾差可继续参与后续计息。 + /// + internal static void NormalizeEodPositionForStorage(eod_swap_position position) + { + if (string.IsNullOrEmpty(position.UnderlyingCode)) + { + // 利息腿没有标的代码:待实现字段保留高精度,已实现结算字段收敛到金额两位。 + position.InterestPrincipalFix = RoundEodInterest(position.InterestPrincipalFix); + position.InterestRateDefault = RoundEodInterest(position.InterestRateDefault); + position.InterestFeePending = RoundEodInterest(position.InterestFeePending); + position.TdInterestPrincipal = RoundEodInterest(position.TdInterestPrincipal); + position.TdInterestRate = RoundEodInterest(position.TdInterestRate); + position.TdInterestIncome = RoundEodInterest(position.TdInterestIncome); + position.TdInterestFee = RoundEodInterest(position.TdInterestFee); + position.InterestIncomeSum = RoundEodInterest(position.InterestIncomeSum); + position.InterestFeeSum = RoundEodInterest(position.InterestFeeSum); + position.InterestProfitSum = RoundEodInterest(position.InterestProfitSum); + position.FloatRate = RoundEodInterest(position.FloatRate); + position.SwapPositionValue = RoundEodInterest(position.SwapPositionValue); + position.TdCloseInterest = RoundMoney(position.TdCloseInterest); + position.TdCloseInterestFee = RoundMoney(position.TdCloseInterestFee); + position.RealizedInterest = RoundMoney(position.RealizedInterest); + position.RealizedInterestFee = RoundMoney(position.RealizedInterestFee); + } + else + { + // 浮动腿有标的代码:其损益作为金额结果落库,统一按两位四舍五入。 + position.TdPosiDividend = RoundMoney(position.TdPosiDividend); + position.PosiMtmPnL = RoundMoney(position.PosiMtmPnL); + position.PosiDividendSum = RoundMoney(position.PosiDividendSum); + position.PosiFeePending = RoundMoney(position.PosiFeePending); + position.PosiProfitSum = RoundMoney(position.PosiProfitSum); + position.TdCloseMtmPnl = RoundMoney(position.TdCloseMtmPnl); + position.TdCloseDividend = RoundMoney(position.TdCloseDividend); + position.TdCloseFee = RoundMoney(position.TdCloseFee); + position.RealizedMtmPnL = RoundMoney(position.RealizedMtmPnL); + position.RealizedDividend = RoundMoney(position.RealizedDividend); + position.RealizedFee = RoundMoney(position.RealizedFee); + position.SwapPositionValue = RoundMoney(position.SwapPositionValue); + } + position.RealizedPnl = RoundMoney(position.RealizedPnl); + } + + /// + /// 浮动腿累计已实现盈亏由盯市、分红和费用三个已实现组成项汇总。 + /// 各组成项已经按本方视角落库,此处不再额外转换方向。 + /// + internal static void SetFloatingRealizedPnl(eod_swap_position position) + { + position.RealizedPnl = position.RealizedMtmPnL + + position.RealizedDividend + + position.RealizedFee; + } + + /// + /// 汇总单条日终腿的我方已实现收益。 + /// 浮动腿及普通利息腿维持数据库记录的方向;初始/追加预付金腿的利息 + /// 则与保证金本金方向相反。这样“收取对手方保证金”产生的利息会作为 + /// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。 + /// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。 + /// + public static decimal CalculateSwapRealizedPnl(eod_swap_position position) + { + var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); + + return position.RealizedMtmPnL + + position.RealizedDividend + + position.RealizedFee + + position.RealizedInterest * interestRatio + + position.RealizedInterestFee; + } + + /// 填充框架合约的持仓腿汇总字段(多空名义本金/市值/浮动盈亏/dv01/平仓量)。 + /// SaveEodSwap 与 UpdateEodSwap 共用,消除 ~10 行重复。 + internal static void FillPositionLegSummary(eod_swap eod_Swap, List positions) + { + eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); + eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue); + eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue); + eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum); + eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0); + eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty); + } + + /// 利息腿 PnL 汇总(按方向比例 + 保证金翻转)。原 SaveEodSwap/UpdateEodSwap 各一段 ForEach。 + internal static decimal SumInterestPnL(List interestPositions) + { + decimal interestPnL = 0; + foreach (var x in interestPositions) + interestPnL += x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode); + return interestPnL; + } + + /// + /// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest + /// 统一按"绝对金额 × 业务方向"重写。普通利息腿收取为正、支付为负; + /// 预付金腿利息方向与保证金本金方向相反。随后重算 RealizedPnl。 + /// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。 + /// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。 + /// + public static void NormalizeInterestSignForReport(eod_swap_position position) + { + if (position.InterestDirection <= 0) return; + + if (position.InterestMode == (int)InterestModeEnum.标的期初全价) + { + return; + } + var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); + position.TdCloseInterest = Math.Abs(position.TdCloseInterest) * interestRatio; + position.RealizedInterest = Math.Abs(position.RealizedInterest) * interestRatio; + // 兼容修复前已落库的利息腿:当时只累计了明细字段,未同步写入 RealizedPnl。 + position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee; + } + + /// + /// 计算预付金利率。多条初始/追加预付金腿按本金绝对值加权, + /// 不按收付方向轧差,避免相反方向本金抵消后放大利率。 + /// + internal static decimal CalculateWeightedMarginRate(IEnumerable margins) + { + var marginList = margins.ToList(); + var totalWeight = marginList.Sum(x => Math.Abs(x.InterestPrincipalFix)); + return totalWeight == 0 + ? 0 + : marginList.Sum(x => x.InterestRateDefault * Math.Abs(x.InterestPrincipalFix)) / totalWeight; + } + + /// + /// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额, + /// 按收取为正、支付为负直接轧差求和,不做本金加权。 + /// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。 + /// + public static decimal CalculateWeightedMarginInterest(IEnumerable margins) + { + return margins.Sum(x => + x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection)); + } + + /// + /// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。 + /// 4 处 SaveAutoEodInterestPosition/SaveEodInterestPosition 路径口径一致, + /// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest), + /// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。 + /// + public static void SetFixedLegRealizedPnl(eod_swap_position position) + { + position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee; + } + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs index e445a357..d42fc903 100644 --- a/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapEodPositionService.cs @@ -48,72 +48,13 @@ namespace YLErp.Modules.SwapModule : ConsGlobal.SwapDeliveryPriceRound; } - // 日终利息待实现需跨日累计,按表设计保留 12 位;已实现结算仍按金额两位处理。 - private const int EodInterestStoragePrecision = 12; - - private static decimal RoundMoney(decimal value) - { - return Math.Round(value, ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); - } - - private static decimal RoundEodInterest(decimal value) - { - return Math.Round(value, EodInterestStoragePrecision, MidpointRounding.AwayFromZero); - } - - /// - /// 仅在写入 eod_swap_position 前统一快照精度。 - /// 浮动腿收益最终以金额两位展示和存储;利息腿的待实现、计息基数及利率保留 12 位, - /// 使部分结算后的尾差可继续参与后续计息。 - /// - private static void NormalizeEodPositionForStorage(eod_swap_position position) - { - if (string.IsNullOrEmpty(position.UnderlyingCode)) - { - // 利息腿没有标的代码:待实现字段保留高精度,已实现结算字段收敛到金额两位。 - position.InterestPrincipalFix = RoundEodInterest(position.InterestPrincipalFix); - position.InterestRateDefault = RoundEodInterest(position.InterestRateDefault); - position.InterestFeePending = RoundEodInterest(position.InterestFeePending); - position.TdInterestPrincipal = RoundEodInterest(position.TdInterestPrincipal); - position.TdInterestRate = RoundEodInterest(position.TdInterestRate); - position.TdInterestIncome = RoundEodInterest(position.TdInterestIncome); - position.TdInterestFee = RoundEodInterest(position.TdInterestFee); - position.InterestIncomeSum = RoundEodInterest(position.InterestIncomeSum); - position.InterestFeeSum = RoundEodInterest(position.InterestFeeSum); - position.InterestProfitSum = RoundEodInterest(position.InterestProfitSum); - position.FloatRate = RoundEodInterest(position.FloatRate); - position.SwapPositionValue = RoundEodInterest(position.SwapPositionValue); - position.TdCloseInterest = RoundMoney(position.TdCloseInterest); - position.TdCloseInterestFee = RoundMoney(position.TdCloseInterestFee); - position.RealizedInterest = RoundMoney(position.RealizedInterest); - position.RealizedInterestFee = RoundMoney(position.RealizedInterestFee); - } - else - { - // 浮动腿有标的代码:其损益作为金额结果落库,统一按两位四舍五入。 - position.TdPosiDividend = RoundMoney(position.TdPosiDividend); - position.PosiMtmPnL = RoundMoney(position.PosiMtmPnL); - position.PosiDividendSum = RoundMoney(position.PosiDividendSum); - position.PosiFeePending = RoundMoney(position.PosiFeePending); - position.PosiProfitSum = RoundMoney(position.PosiProfitSum); - position.TdCloseMtmPnl = RoundMoney(position.TdCloseMtmPnl); - position.TdCloseDividend = RoundMoney(position.TdCloseDividend); - position.TdCloseFee = RoundMoney(position.TdCloseFee); - position.RealizedMtmPnL = RoundMoney(position.RealizedMtmPnL); - position.RealizedDividend = RoundMoney(position.RealizedDividend); - position.RealizedFee = RoundMoney(position.RealizedFee); - position.SwapPositionValue = RoundMoney(position.SwapPositionValue); - } - position.RealizedPnl = RoundMoney(position.RealizedPnl); - } - #region 可测试化接缝(Seams)——override 这些虚方法可在测试中替换 DB/外部调用,生产代码行为不变 /// 持久化 eod 持仓记录(生产: DbContext.Add;测试: 收集到列表) protected virtual void PersistEodSwapPosition(eod_swap_position position) { // 所有新增或更新的日终持仓都经过此入口,避免不同日终分支出现精度差异。 - NormalizeEodPositionForStorage(position); + EodPnlCalculator.NormalizeEodPositionForStorage(position); var storagePriceRound = GetStorageDeliveryPriceRound(position.UnderlyingInstrumentType, position.UnderlyingCode); position.PosiGrossPrice = Math.Round(position.PosiGrossPrice, storagePriceRound, MidpointRounding.AwayFromZero); position.UnderlyingPrice = Math.Round(position.UnderlyingPrice, storagePriceRound, MidpointRounding.AwayFromZero); @@ -1089,8 +1030,8 @@ namespace YLErp.Modules.SwapModule var interestFeeBeforeSettlement = eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee; var isMaturityFinalSettlement = valueDate.Date >= td.ExerciseDate.Value.Date && flowEvents.Any() - && RoundMoney(interestIncomeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterest) - && RoundMoney(interestFeeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterestFee); + && EodPnlCalculator.RoundMoney(interestIncomeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest) + && EodPnlCalculator.RoundMoney(interestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee); if (isMaturityFinalSettlement) { @@ -1101,8 +1042,8 @@ namespace YLErp.Modules.SwapModule } else { - newEodPayPosition.InterestIncomeSum = RoundEodInterest(interestIncomeBeforeSettlement - newEodPayPosition.TdCloseInterest); - newEodPayPosition.InterestFeeSum = RoundEodInterest(interestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee); + newEodPayPosition.InterestIncomeSum = EodPnlCalculator.RoundEodInterest(interestIncomeBeforeSettlement - newEodPayPosition.TdCloseInterest); + newEodPayPosition.InterestFeeSum = EodPnlCalculator.RoundEodInterest(interestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee); } newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum; //持仓价值 @@ -1202,8 +1143,8 @@ namespace YLErp.Modules.SwapModule // 日终快照仍使用上面的高精度应结金额计算待实现尾差,避免把舍入差提前丢掉。 interests.ForEach(x => { - x.InterestAmount = RoundMoney(x.InterestAmount); - x.InterestClosePnL = RoundMoney(x.InterestClosePnL); + x.InterestAmount = EodPnlCalculator.RoundMoney(x.InterestAmount); + x.InterestClosePnL = EodPnlCalculator.RoundMoney(x.InterestClosePnL); }); decimal settledInterestAmount = interests.Sum(x => x.InterestAmount); @@ -1240,10 +1181,10 @@ namespace YLErp.Modules.SwapModule // 到期自动互换是最后一次自动结算:两位实际金额已落流水/资金,待实现不再滚入下一日。 newEodPayPosition.InterestIncomeSum = isMaturityFinalAutoSettlement ? 0 - : RoundEodInterest(interestAmountBeforeSettlement - settledInterestAmount); + : EodPnlCalculator.RoundEodInterest(interestAmountBeforeSettlement - settledInterestAmount); newEodPayPosition.InterestFeeSum = isMaturityFinalAutoSettlement ? 0 - : RoundEodInterest(eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee); + : EodPnlCalculator.RoundEodInterest(eodPayPosition.InterestFeeSum + newEodPayPosition.TdInterestFee - newEodPayPosition.TdCloseInterestFee); newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum; //持仓价值 newEodPayPosition.SwapPositionValue = PositionValueCalc.Calc(newEodPayPosition.InterestProfitSum, newEodPayPosition.PosiProfitSum, (int)ratio); @@ -1365,7 +1306,7 @@ namespace YLErp.Modules.SwapModule decimal autoSettledInterestAmount = 0m; if (autoSwap && interests.Count > 0) { - autoSettledInterestAmount = RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount); + autoSettledInterestAmount = EodPnlCalculator.RoundMoney(interestAmountBeforeSettlement - manualSettledInterestAmount); var autoInterest = interests[0]; autoInterest.InterestAmount = autoSettledInterestAmount; autoInterest.InterestClosePnL = autoSettledInterestAmount @@ -1495,13 +1436,13 @@ namespace YLErp.Modules.SwapModule // InterestIncomeSum 是收盘后仍未结算的尾差/剩余利息。 // 部分平仓:扣款前待实现 - TdCloseInterest;最终全平且两位金额已覆盖时直接清零。 newEodPayPosition.InterestIncomeSum = closePercent == 1 - && RoundMoney(pendingInterestBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterest) + && EodPnlCalculator.RoundMoney(pendingInterestBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterest) ? 0m - : RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest); + : EodPnlCalculator.RoundEodInterest(pendingInterestBeforeSettlement - newEodPayPosition.TdCloseInterest); newEodPayPosition.InterestFeeSum = closePercent == 1 - && RoundMoney(pendingInterestFeeBeforeSettlement) == RoundMoney(newEodPayPosition.TdCloseInterestFee) + && EodPnlCalculator.RoundMoney(pendingInterestFeeBeforeSettlement) == EodPnlCalculator.RoundMoney(newEodPayPosition.TdCloseInterestFee) ? 0m - : RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee); + : EodPnlCalculator.RoundEodInterest(pendingInterestFeeBeforeSettlement - newEodPayPosition.TdCloseInterestFee); //持仓内容-利息腿-损益统计(本方视角) // InterestProfitSum 是利息腿待实现总额,包含利息和费用;无费用时等于 InterestIncomeSum。 newEodPayPosition.InterestProfitSum = newEodPayPosition.InterestIncomeSum + newEodPayPosition.InterestFeeSum; @@ -1736,7 +1677,7 @@ namespace YLErp.Modules.SwapModule newEodPayPosition.RealizedFee = closeFee; newEodPayPosition.RealizedMtmPnL = newEodPayPosition.TdCloseMtmPnl; newEodPayPosition.RealizedDividend = newEodPayPosition.TdCloseDividend; - SetFloatingRealizedPnl(newEodPayPosition); + EodPnlCalculator.SetFloatingRealizedPnl(newEodPayPosition); newEodPayPosition.PosiStatus = payQty == 0 ? 1 : 0; UpdateDbOption(newEodPayPosition); @@ -1810,7 +1751,7 @@ namespace YLErp.Modules.SwapModule curretEod.RealizedMtmPnL = eod.RealizedMtmPnL + curretEod.TdCloseMtmPnl; curretEod.RealizedDividend = eod.RealizedDividend + curretEod.TdCloseDividend; curretEod.RealizedFee = eod.RealizedFee + curretEod.TdCloseFee; - SetFloatingRealizedPnl(curretEod); + EodPnlCalculator.SetFloatingRealizedPnl(curretEod); var currencyRate = new EodCurrencyRateService(UserInfo).GetCurrencyRate(td.QuoteCurrency, td.SettlementCurrency, td.StartDate.Value , seekPreday: true, currencyRateType: DirectionRatio.RateType(curretEod.PosiDirection)); curretEod.TdCurrency = Convert.ToDecimal(currencyRate); @@ -1825,17 +1766,6 @@ namespace YLErp.Modules.SwapModule return curretEod; } - /// - /// 浮动腿累计已实现盈亏由盯市、分红和费用三个已实现组成项汇总。 - /// 各组成项已经按本方视角落库,此处不再额外转换方向。 - /// - private static void SetFloatingRealizedPnl(eod_swap_position position) - { - position.RealizedPnl = position.RealizedMtmPnL - + position.RealizedDividend - + position.RealizedFee; - } - /// /// 更新虚拟交易费用 /// @@ -1911,7 +1841,7 @@ namespace YLErp.Modules.SwapModule { curretEod.PosiDividendSum = 0; } - SetFloatingRealizedPnl(curretEod); + EodPnlCalculator.SetFloatingRealizedPnl(curretEod); curretEod.SwapPositionValue -= curretEod.TdCloseDividend; curretEod.PosiProfitSum = MtmCalc.ReturnLegProfitSum(curretEod.PosiMtmPnL, curretEod.PosiDividendSum, curretEod.PosiFeePending); @@ -2091,7 +2021,7 @@ namespace YLErp.Modules.SwapModule curretEod.RealizedMtmPnL = curretEod.TdCloseMtmPnl; curretEod.RealizedDividend = curretEod.TdCloseDividend; curretEod.RealizedFee = curretEod.TdCloseFee; - SetFloatingRealizedPnl(curretEod); + EodPnlCalculator.SetFloatingRealizedPnl(curretEod); curretEod.PosiStatus = curretEod.PosiQuantity == 0 ? 1 : 0; if (curretEod.PosiStatus == 1) { @@ -2182,8 +2112,8 @@ namespace YLErp.Modules.SwapModule eod_Swap.BookId = td.AssetId; eod_Swap.ValueDate = settleDate; eod_Swap.StructureType = td.StructureType; - FillPositionLegSummary(eod_Swap, positions); - eod_Swap.InterestPnL = SumInterestPnL(interestPositions); + EodPnlCalculator.FillPositionLegSummary(eod_Swap, positions); + eod_Swap.InterestPnL = EodPnlCalculator.SumInterestPnL(interestPositions); eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue); // 保证金腿的利息现金流方向与保证金本金方向相反。 // 不能直接汇总 RealizedPnl,否则“收取客户保证金”的腿会把应支付给客户的 @@ -2236,8 +2166,8 @@ namespace YLErp.Modules.SwapModule var interestPositions = eodSwapPositions.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//利息腿 var positions = eodSwapPositions.Where(x => !string.IsNullOrEmpty(x.UnderlyingCode)).ToList();//持仓腿 eod_Swap.NotionalValue = Math.Round(Convert.ToDecimal(td.OriginalStockEqvNotional ?? td.StockEqvNotional), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); - FillPositionLegSummary(eod_Swap, positions); - eod_Swap.InterestPnL += SumInterestPnL(interestPositions); + EodPnlCalculator.FillPositionLegSummary(eod_Swap, positions); + eod_Swap.InterestPnL += EodPnlCalculator.SumInterestPnL(interestPositions); eodSwapPositions.ForEach(x => { var ratio = DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode); @@ -2263,38 +2193,7 @@ namespace YLErp.Modules.SwapModule /// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。 /// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。 /// - public static decimal CalculateSwapRealizedPnl(eod_swap_position position) - { - var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); - - return position.RealizedMtmPnL - + position.RealizedDividend - + position.RealizedFee - + position.RealizedInterest * interestRatio - + position.RealizedInterestFee; - } - - /// 填充框架合约的持仓腿汇总字段(多空名义本金/市值/浮动盈亏/dv01/平仓量)。 - /// SaveEodSwap 与 UpdateEodSwap 共用,消除 ~10 行重复。 - private static void FillPositionLegSummary(eod_swap eod_Swap, List positions) - { - eod_Swap.NotionalValueLong = Math.Round(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.PosiNotionalValue), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); - eod_Swap.NotionalValueShort = Math.Round(-Math.Abs(positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.PosiNotionalValue)), ConsGlobal.MoneyRound, MidpointRounding.AwayFromZero); - eod_Swap.MarketValueLong = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Long).Sum(s => s.UnderlyingMarketValue); - eod_Swap.MarketValueShort = positions.Where(x => x.PositionType == (int)PositionTypeFlag.Short).Sum(s => s.UnderlyingMarketValue); - eod_Swap.FloatingPnL = positions.Sum(s => s.PosiProfitSum); - eod_Swap.dv01 = positions.Sum(s => s.dv01 ?? 0); - eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty); - } - - /// 利息腿 PnL 汇总(按方向比例 + 保证金翻转)。原 SaveEodSwap/UpdateEodSwap 各一段 ForEach。 - private static decimal SumInterestPnL(List interestPositions) - { - decimal interestPnL = 0; - foreach (var x in interestPositions) - interestPnL += x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode); - return interestPnL; - } + public static decimal CalculateSwapRealizedPnl(eod_swap_position position) => EodPnlCalculator.CalculateSwapRealizedPnl(position); /// /// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest @@ -2303,20 +2202,7 @@ namespace YLErp.Modules.SwapModule /// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。 /// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。 /// - public static void NormalizeInterestSignForReport(eod_swap_position position) - { - if (position.InterestDirection <= 0) return; - - if (position.InterestMode == (int)InterestModeEnum.标的期初全价) - { - return; - } - var interestRatio = DirectionRatio.InterestLegPnl(position.InterestDirection, position.InterestMode); - position.TdCloseInterest = Math.Abs(position.TdCloseInterest) * interestRatio; - position.RealizedInterest = Math.Abs(position.RealizedInterest) * interestRatio; - // 兼容修复前已落库的利息腿:当时只累计了明细字段,未同步写入 RealizedPnl。 - position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee; - } + public static void NormalizeInterestSignForReport(eod_swap_position position) => EodPnlCalculator.NormalizeInterestSignForReport(position); /// /// 获取多空组合 平仓详细 @@ -2806,7 +2692,7 @@ namespace YLErp.Modules.SwapModule item.position.FloatRateUnderlyingCode = floatRateInterest?.FloatRateUnderlyingCode; item.position.FloatRate = floatRateInterest?.FloatRate ?? 0; item.OpenMarginAmount = initialMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection)); - item.OpenMarginRate = CalculateWeightedMarginRate(tradeMargins); + item.OpenMarginRate = EodPnlCalculator.CalculateWeightedMarginRate(tradeMargins); item.AdditionalMarginAmount = additionalMargins.Sum(s => s.InterestPrincipalFix * DirectionRatio.ReceivePay(s.InterestDirection)); item.MarginInterestAmount = CalculateWeightedMarginInterest(eodMargins); item.InterestAmount = eodInterests.Sum(s => s.InterestIncomeSum * (-DirectionRatio.ReceivePay(s.InterestDirection))); @@ -2831,29 +2717,12 @@ namespace YLErp.Modules.SwapModule return retListResult; } - /// - /// 计算预付金利率。多条初始/追加预付金腿按本金绝对值加权, - /// 不按收付方向轧差,避免相反方向本金抵消后放大利率。 - /// - private static decimal CalculateWeightedMarginRate(IEnumerable margins) - { - var marginList = margins.ToList(); - var totalWeight = marginList.Sum(x => Math.Abs(x.InterestPrincipalFix)); - return totalWeight == 0 - ? 0 - : marginList.Sum(x => x.InterestRateDefault * Math.Abs(x.InterestPrincipalFix)) / totalWeight; - } - /// /// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额, /// 按收取为正、支付为负直接轧差求和,不做本金加权。 /// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。 /// - public static decimal CalculateWeightedMarginInterest(IEnumerable margins) - { - return margins.Sum(x => - x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection)); - } + public static decimal CalculateWeightedMarginInterest(IEnumerable margins) => EodPnlCalculator.CalculateWeightedMarginInterest(margins); /// /// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。 @@ -2861,10 +2730,7 @@ namespace YLErp.Modules.SwapModule /// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest), /// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。 /// - public static void SetFixedLegRealizedPnl(eod_swap_position position) - { - position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee; - } + public static void SetFixedLegRealizedPnl(eod_swap_position position) => EodPnlCalculator.SetFixedLegRealizedPnl(position); /// /// 将数据库中以公司/交易簿记方向保存的日终字段转换为客户视角。 /// 该转换必须在拆分浮动收益、费用和期间付息/分红之前完成,