fix(swap): 修正掉期产品保证金利息计算逻辑

- 修正FloatingUnrealizedPnl字段定义,包含未结交易费用
- 更新MarginInterestGain和MarginInterestLoss字段的文档说明
- 添加CalculateSwapRealizedPnl方法处理保证金腿利息现金流方向
- 修正RealizedPnL计算逻辑,避免保证金利息重复计算
- 调整保证金本金和利息计算口径的一致性处理
- 修正保证金利息现金流方向与本金方向的相反关系
This commit is contained in:
张名锐
2026-07-16 19:55:44 +08:00
parent 98122ef509
commit 6676b62573
2 changed files with 61 additions and 9 deletions
+5 -3
View File
@@ -186,7 +186,7 @@ namespace YLErp.DBModels
public decimal PeriodAmount { get; set; }
/// <summary>
/// 合约浮动端待实现收益,取浮动腿盯市收益,
/// 合约浮动端待实现收益,取浮动腿盯市收益及未结交易费用
/// 不包含期间付息/分红,避免与 <see cref="PeriodAmount"/> 重复。
/// </summary>
public decimal FloatingUnrealizedPnl { get; set; }
@@ -209,12 +209,14 @@ namespace YLErp.DBModels
public decimal? PeriodPaymentValuation { get; set; }
/// <summary>
/// 我方收取的保证金利息累计额,按保证金腿方向归集并取绝对值展示。
/// 我方收取的保证金利息累计额保证金腿原始“支付”方向表示
/// 对手方向我方支付保证金,利息现金流方向与保证金本金方向相反。
/// </summary>
public decimal MarginInterestGain { get; set; }
/// <summary>
/// 我方支付的保证金利息累计额,按保证金腿方向归集并取绝对值展示。
/// 我方支付的保证金利息累计额保证金腿原始“收取”方向表示
/// 我方收取对手方保证金,应向对手方支付利息;支付金额以负数展示。
/// </summary>
public decimal MarginInterestLoss { get; set; }
}
@@ -1964,7 +1964,10 @@ namespace YLErp.Modules.SwapModule
});
eod_Swap.InterestPnL = interestPnL;
eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue);
eod_Swap.RealizedPnL = eodSwapPositions.Sum(s => s.RealizedPnl);
// 保证金腿的利息现金流方向与保证金本金方向相反。
// 不能直接汇总 RealizedPnl,否则“收取客户保证金”的腿会把应支付给客户的
// 利息作为收益相加。逐腿按利息方向转换后再生成框架合约已实现收益。
eod_Swap.RealizedPnL = eodSwapPositions.Sum(CalculateSwapRealizedPnl);
eod_Swap.TdRealizedPnL = eod_Swap.RealizedPnL - (preEodSwap?.RealizedPnL ?? 0);
eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
var initMargin = Convert.ToDecimal(tradeSpan?.InitialMargin ?? 0);
@@ -2037,7 +2040,7 @@ namespace YLErp.Modules.SwapModule
eod_Swap.TdRealizedPnL += x.TdCloseMtmPnl + x.TdCloseDividend + x.TdCloseFee + x.TdCloseInterest * ratio + x.TdCloseInterestFee;
});
eod_Swap.PostionValue = eodSwapPositions.Sum(s => s.SwapPositionValue);
eod_Swap.RealizedPnL = eodSwapPositions.Sum(s => s.RealizedMtmPnL + s.RealizedDividend + s.RealizedFee + s.RealizedInterest + s.RealizedInterestFee);
eod_Swap.RealizedPnL = eodSwapPositions.Sum(CalculateSwapRealizedPnl);
eod_Swap.TdCloseQty = positions.Sum(s => s.TdCloseQty);
var tradeInitMarginObj = DbContext.trade_initial_margin.FirstOrDefault(x => x.TradeId == td.id);
var initMarginList = interestPositions.Where(x => x.InterestMode == (int)InterestModeEnum. && x.HappenDate == settleDate).ToList();
@@ -2050,6 +2053,27 @@ namespace YLErp.Modules.SwapModule
DbContext.SaveChanges();
}
/// <summary>
/// 汇总单条日终腿的我方已实现收益。
/// 浮动腿及普通利息腿维持数据库记录的方向;初始/追加预付金腿的利息
/// 则与保证金本金方向相反。这样“收取对手方保证金”产生的利息会作为
/// 我方支付给对手方的成本计入,而不会错误增加框架合约已实现收益。
/// </summary>
private decimal CalculateSwapRealizedPnl(eod_swap_position position)
{
var interestRatio = position.InterestDirection == (int)SwapDirectionEnum. ? 1m : -1m;
if (marginTypes.Contains(position.InterestMode))
{
interestRatio = -interestRatio;
}
return position.RealizedMtmPnL
+ position.RealizedDividend
+ position.RealizedFee
+ position.RealizedInterest * interestRatio
+ position.RealizedInterestFee;
}
/// <summary>
/// 获取多空组合 平仓详细
/// </summary>
@@ -2241,7 +2265,11 @@ namespace YLErp.Modules.SwapModule
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct());
item.PeriodAmount = floatingLegs.Sum(x => x.RealizedDividend + x.PosiDividendSum);
item.FloatingUnrealizedPnl = floatingLegs.Sum(x => x.PosiMtmPnL);
// PosiProfitSum = 标的盯市收益 + 未结交易费用 + 待实现付息/分红。
// 风险页的“合约浮动端待实现收益”需要保留未结交易费用,
// 但期间付息/分红由 PeriodAmount 单列展示并参与对应估值口径,
// 因此仅扣除 PosiDividendSum,不能直接使用 PosiMtmPnL。
item.FloatingUnrealizedPnl = floatingLegs.Sum(x => x.PosiProfitSum - x.PosiDividendSum);
item.InterestPaymentMethod = dividendPayDate == 0 ? "到期轧差" : "派息日支付";
if (dividendPayDate == 0)
{
@@ -2251,12 +2279,34 @@ namespace YLErp.Modules.SwapModule
{
item.PeriodPaymentValuation = item.FloatingUnrealizedPnl + item.position.InterestPnL;
}
// eod_swap 的保证金本金来自 trade_span;缺少 span 数据时会被保存为 0。
// 本风险页改按日终保证金腿展示,且该页面保证金本金采用原始本金的 1/10 口径。
// 利息仍使用原始本金累积值,不能同步缩放,否则会破坏保证金利息金额。
item.position.InitMarginGain = marginLegs
.Where(x => x.InterestMode == (int)InterestModeEnum.
&& x.InterestDirection == (int)SwapDirectionEnum.)
.Sum(x => Math.Abs(x.InterestPrincipalFix)) / 10m;
item.position.InitMarginLoss = marginLegs
.Where(x => x.InterestMode == (int)InterestModeEnum.
&& x.InterestDirection == (int)SwapDirectionEnum.)
.Sum(x => Math.Abs(x.InterestPrincipalFix)) / 10m;
item.position.PostionMarginGain = marginLegs
.Where(x => x.InterestMode == (int)InterestModeEnum.
&& x.InterestDirection == (int)SwapDirectionEnum.)
.Sum(x => Math.Abs(x.InterestPrincipalFix)) / 10m;
item.position.PostionMarginLoss = marginLegs
.Where(x => x.InterestMode == (int)InterestModeEnum.
&& x.InterestDirection == (int)SwapDirectionEnum.)
.Sum(x => Math.Abs(x.InterestPrincipalFix)) / 10m;
// 保证金本金方向与我方的利息现金流方向相反:原始“收取”保证金
// 表示我方占用客户资金,应向客户支付利息;支付金额按负数展示。
item.MarginInterestGain = marginLegs
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.)
.Sum(x => Math.Abs(x.InterestIncomeSum));
item.MarginInterestLoss = marginLegs
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.)
.Sum(x => Math.Abs(x.InterestIncomeSum));
item.MarginInterestLoss = marginLegs
.Where(x => x.InterestDirection == (int)SwapDirectionEnum.)
.Sum(x => -Math.Abs(x.InterestIncomeSum));
}
var dv01 = query.Sum(O => O.position.dv01??0);