Files
zszq-trs/YLErpDAL/Modules/SwapModule/EodPnlCalculator.cs
T
张名锐 147bcf4bf7 Merge branch 'glms/feature/0828_EQD-7084' into 'glms/feature/1.4.2'
feat(eodswapposition): 新增框架合约新口径展示功能

See merge request otc-dev/zszq-trs!14
2026-08-25 07:40:56 +00:00

283 lines
16 KiB
C#

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
{
/// <summary>
/// 互换日终盈亏/精度计算纯函数集合。
/// 自 SwapEodPositionService 抽出,支持无库单测;同类内部调用无需前缀。
/// </summary>
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);
}
/// <summary>
/// 仅在写入 eod_swap_position 前统一快照精度。
/// 浮动腿收益最终以金额两位展示和存储;利息腿的待实现、计息基数及利率保留 12 位,
/// 使部分结算后的尾差可继续参与后续计息。
/// </summary>
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);
}
/// <summary>
/// 浮动腿累计已实现盈亏由盯市、分红和费用三个已实现组成项汇总。
/// 各组成项已经按本方视角落库,此处不再额外转换方向。
/// </summary>
internal static void SetFloatingRealizedPnl(eod_swap_position position)
{
position.RealizedPnl = position.RealizedMtmPnL
+ position.RealizedDividend
+ position.RealizedFee;
}
/// <summary>
/// 汇总单条日终腿的我方已实现收益。
/// 浮动腿及普通利息腿维持数据库记录的方向;初始/追加预付金腿的利息
/// 则与保证金本金方向相反。这样“收取对手方保证金”产生的利息会作为
/// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。
/// 抽为静态纯函数以支持无库单测(marginTypes 等价于 ConsTrade.InterestMarginModels)。
/// </summary>
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;
}
/// <summary>填充框架合约的持仓腿汇总字段(多空名义本金/市值/浮动盈亏/dv01/平仓量)。
/// SaveEodSwap 与 UpdateEodSwap 共用,消除 ~10 行重复。</summary>
internal static void FillPositionLegSummary(eod_swap eod_Swap, List<eod_swap_position> 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);
}
/// <summary>利息腿 PnL 汇总(按方向比例 + 保证金翻转)。原 SaveEodSwap/UpdateEodSwap 各一段 ForEach。</summary>
internal static decimal SumInterestPnL(List<eod_swap_position> interestPositions)
{
decimal interestPnL = 0;
foreach (var x in interestPositions)
interestPnL += x.InterestProfitSum * DirectionRatio.InterestLegPnl(x.InterestDirection, x.InterestMode);
return interestPnL;
}
/// <summary>
/// 风险报表符号归一化:把历史两种符号口径的 TdCloseInterest/RealizedInterest
/// 统一按"绝对金额 × 业务方向"重写。普通利息腿收取为正、支付为负;
/// 预付金腿利息方向与保证金本金方向相反。随后重算 RealizedPnl。
/// 抽为 public static 纯函数以支持无库单测(见 SwapReportInterestSignNormalizeTest)。
/// 仅当 InterestDirection > 0 时执行(与原内联逻辑等价)。
/// </summary>
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;
}
/// <summary>
/// 计算预付金利率。多条初始/追加预付金腿按本金绝对值加权,
/// 不按收付方向轧差,避免相反方向本金抵消后放大利率。
/// 对外公开以便结算单与每日估值复用同一计算口径。
/// </summary>
public static decimal CalculateWeightedMarginRate(IEnumerable<swap_position> 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;
}
/// <summary>
/// 计算预付金利息金额。InterestIncomeSum 已是各腿利息金额,
/// 按收取为正、支付为负直接轧差求和,不做本金加权。
/// 抽为 public static 纯函数以支持无库单测(见 SwapWeightedMarginInterestTest)。
/// </summary>
public static decimal CalculateWeightedMarginInterest(IEnumerable<eod_swap_position> margins)
{
return margins.Sum(x =>
x.InterestIncomeSum * DirectionRatio.ReceivePay(x.InterestDirection));
}
/// <summary>
/// 固定利息腿的累计已实现盈亏 = 累计已实现利息 + 累计已实现利息费用。
/// 4 处 SaveAutoEodInterestPosition/SaveEodInterestPosition 路径口径一致,
/// 抽为 public static 纯函数以支持无库单测(见 SwapFixedLegRealizedPnlTest),
/// 并消除复制粘贴带来的笔误风险(如 L1296 历史双分号)。
/// </summary>
public static void SetFixedLegRealizedPnl(eod_swap_position position)
{
position.RealizedPnl = position.RealizedInterest + position.RealizedInterestFee;
}
/// <summary>
/// 计算 EQD-7084 新“框架合约”Tab 的纯展示口径。
/// 浮动腿盯市收益、开平仓费用和普通利息分别计算;保证金腿的利息
/// 仅作为估值组成项保留一次,不混入新 Tab 的普通利息列。
/// </summary>
public static EodSwapRiskNewFields CalculateEodSwapRiskNewFields(
IEnumerable<eod_swap_position> floatingLegs,
IEnumerable<eod_swap_position> interestLegs,
string structureType,
decimal notionalValue,
DateTime? startDate,
DateTime? maturityDate,
decimal periodAmount,
int dividendPayDate)
{
// 日终明细以 UnderlyingCode 是否存在区分浮动腿和利息腿;调用方即使传入混合集合,
// 这里也会重新过滤,避免保证金/利息数据被带入浮动端新口径。
var floating = (floatingLegs ?? Enumerable.Empty<eod_swap_position>())
.Where(x => x != null && !string.IsNullOrEmpty(x.UnderlyingCode))
.ToList();
var interests = (interestLegs ?? Enumerable.Empty<eod_swap_position>())
.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 = maturityDate,
// 只要普通利息腿存在 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;
}
}
}