using System; using System.Collections.Generic; using System.Linq; using YLErp; using YLErp.DBModels; using YLErp.DBModels.Enums; using YLErp.Modules.SwapModule.Margin; 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; } /// /// 计算 EQD-7084 新“框架合约”Tab 的纯展示口径。 /// 浮动腿盯市收益、开平仓费用和普通利息分别计算;保证金腿的利息 /// 仅作为估值组成项保留一次,不混入新 Tab 的普通利息列。 /// public static EodSwapRiskNewFields CalculateEodSwapRiskNewFields( IEnumerable floatingLegs, IEnumerable interestLegs, string structureType, decimal notionalValue, DateTime? startDate, DateTime? ExerciseDate, decimal periodAmount, int dividendPayDate) { // 日终明细以 UnderlyingCode 是否存在区分浮动腿和利息腿;调用方即使传入混合集合, // 这里也会重新过滤,避免保证金/利息数据被带入浮动端新口径。 var floating = (floatingLegs ?? Enumerable.Empty()) .Where(x => x != null && !string.IsNullOrEmpty(x.UnderlyingCode)) .ToList(); var interests = (interestLegs ?? Enumerable.Empty()) .Where(x => x != null && string.IsNullOrEmpty(x.UnderlyingCode)) .ToList(); // MarginModes 覆盖初始/维持保证金相关腿。它们的利息不属于需求中的“利息端待实现收益”, // 但必须单独保留,以使两个合约估值与旧口径总额保持一致。 var ordinaryInterests = interests.Where(x => !MarginModes.Contains(x.InterestMode)).ToList(); var marginInterests = interests.Where(x => MarginModes.Contains(x.InterestMode)).ToList(); var firstFloating = floating.FirstOrDefault(); // PosiGrossPrice 已是 EOD 归档口径的期初全价;债券价格不可在报表接口再次乘 100。 var initialPrice = firstFloating?.PosiGrossPrice; // PosiFeePending 是日终归一后的我方损益方向:支付费用为负、收取费用为正。 // 本列独立展示它,下面的 valuation 再加回一次,不能因展示拆列而改变合约估值。 var openingClosingFee = floating.Sum(x => x.PosiFeePending); // PosiMtmPnL 已排除分红和费用,避免从 PosiProfitSum 重复拆分历史费用。 var floatingUnrealizedPnl = floating.Sum(x => x.PosiMtmPnL); var ordinaryInterestPnl = ordinaryInterests.Sum(x => x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode)); var marginInterestAmount = marginInterests.Sum(x => x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode)); // 新口径估值 = 去费用浮动收益 + 开平仓费用 + 普通利息 + 保证金利息。 // “浮动端待实现收益”列不包含费用,而合约估值仍沿用旧总额,故费用只能在此加一次。 var valuation = floatingUnrealizedPnl + openingClosingFee + ordinaryInterestPnl + marginInterestAmount; var result = new EodSwapRiskNewFields { UnderlyingInstrumentType = firstFloating?.UnderlyingInstrumentType, UnderlyingDirection = string.Join(",", floating .Select(x => x.PositionType == (int)PositionTypeFlag.Long ? "多头" : x.PositionType == (int)PositionTypeFlag.Short ? "空头" : "") .Where(x => !string.IsNullOrEmpty(x)) .Distinct()), UnderlyingCode = string.Join(",", floating .Select(x => x.UnderlyingCode) .Where(x => !string.IsNullOrEmpty(x)) .Distinct()), InitialPrice = initialPrice, NotionalQuantity = notionalValue, ContractStartDate = startDate, ContractMaturityDate = ExerciseDate, // 只要普通利息腿存在 FR007,即按需求显示 FR007;保证金腿不影响该展示基准。 InterestBenchmark = ordinaryInterests.Any(x => !string.IsNullOrWhiteSpace(x.FloatRateUnderlyingCode) && x.FloatRateUnderlyingCode.IndexOf("FR007", StringComparison.OrdinalIgnoreCase) >= 0) ? "FR007" : "固定利率", // 使用日终当日实际适用的 TdInterestRate 合计,而非合同初始利率或利差字段。 InterestRatePrice = ordinaryInterests.Sum(x => x.TdInterestRate), OpeningClosingFee = -openingClosingFee, // 合约浮动端待实现收益 FloatingUnrealizedPnl = floatingUnrealizedPnl, // 合约利息端待实现收益 OrdinaryInterestPnl = ordinaryInterestPnl, MarginInterestAmount = marginInterestAmount, MarginInterestGain = marginInterests .Where(x => x.InterestDirection == (int)SwapDirectionEnum.支付) .Sum(x => Math.Abs(x.InterestIncomeSum)), MarginInterestLoss = marginInterests .Where(x => x.InterestDirection == (int)SwapDirectionEnum.收取) .Sum(x => -Math.Abs(x.InterestIncomeSum)) }; // DividendPayDate=0 表示到期才与本金轧差,期间付息/分红需要加进该口径; // 其余支付方式则由现金支付承担期间金额,估值字段不再包含 periodAmount。 if (dividendPayDate == 0) { result.MaturityNettingValuation = valuation + periodAmount; } else { result.PeriodPaymentValuation = valuation; } return result; } } }