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; } /// /// 计算预付金利率。多条初始/追加预付金腿按本金绝对值加权, /// 不按收付方向轧差,避免相反方向本金抵消后放大利率。 /// 对外公开以便结算单与每日估值复用同一计算口径。 /// public 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; } } }