- 将价差盈亏计算从使用CloseNotionalValue改为使用PositionQty和ContractSize - 更新前端JavaScript代码中的计算公式,按持仓数量和合约乘数计算价差盈亏 - 在UnwindInput模型中添加PositionQty和ContractSize字段 - 修正后台计算服务中的数据映射逻辑 - 添加FC_009测试用例验证债券价差按数量计算的正确性 - 更新现有测试用例的输入参数以匹配新的计算方式
188 lines
9.4 KiB
C#
188 lines
9.4 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
|
||
namespace YLErp.Helpers
|
||
{
|
||
/// <summary>
|
||
/// 前端互换计算公式的后端参考实现(忠实重写 unwindSwapTrade.js / incomeSwapTrade.js)。
|
||
/// ============================================================================
|
||
/// 用途:
|
||
/// 1. 特征化测试的金标准(FrontendCalcCharacterizationTest)—— 冻结前端行为
|
||
/// 2. 后端只读校验(SwapDealService.ValidateFrontendPnL)—— 重算盈亏与前端传值比对
|
||
///
|
||
/// 注:前端保持快速反馈(用户改输入立即算),本类不替代前端,仅作后端校验兜底。
|
||
/// 命名规范(见《互换价格字段命名规范决策文档》):
|
||
/// PosiGrossPrice 现状名,实为"期初全价不含费",规范名 EntryDirtyPrice
|
||
/// TradingAmountAvg 现状名,实为"期末全价不含费",规范名 ExitDirtyPrice
|
||
/// ============================================================================
|
||
/// </summary>
|
||
public static class FrontendCalcReference
|
||
{
|
||
/// <summary>模拟 otcformat.trading.StockEqvNotional(金额类,2位小数)</summary>
|
||
private static decimal StockEqvNotional(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero);
|
||
|
||
/// <summary>
|
||
/// 计算平仓页(unwind)的盯市盈亏与汇总。
|
||
/// 对应 unwindSwapTrade.js:196-261(calcFloatClosePnl + calcCloseAmount)。
|
||
/// </summary>
|
||
public static UnwindResult CalcUnwind(UnwindInput input)
|
||
{
|
||
// 规范名映射:initPosiNetPrice(前端变量) = PosiGrossPrice = EntryDirtyPrice(期初全价不含费)
|
||
decimal entryPrice = input.PosiGrossPrice;
|
||
// scale = getPriceScale():债券(multiplier=100)→0.01,非债券→1
|
||
decimal scale = input.Multiplier == 100 ? 0.01m : 1m;
|
||
// 方向因子:PayDirection(1=收取)→+1;PositionType(1=多头)→+1
|
||
decimal floatRatio = input.PayDirection == 1 ? 1 : -1;
|
||
decimal longRatio = input.PositionType == 1 ? 1 : -1;
|
||
|
||
decimal tradingFee = ParseOrZero(input.TradingFee);
|
||
decimal tradingFeePending = ParseOrZero(input.TradingFeePending);
|
||
decimal dividendIn = ParseOrZero(input.DividendIn);
|
||
|
||
// MarkClosePnl = round(CloseQty × (TradingAmountAvg × scale − EntryPrice) × floatRatio × longRatio × 10000)/10000
|
||
decimal markClosePnl = Math.Round(
|
||
input.CloseQty * (input.TradingAmountAvg * scale - entryPrice) * floatRatio * longRatio * 10000) / 10000;
|
||
// toFixed(2) → StockEqvNotional
|
||
markClosePnl = Math.Round(markClosePnl, 2, MidpointRounding.AwayFromZero);
|
||
markClosePnl = StockEqvNotional(markClosePnl);
|
||
|
||
// FloatPnlSum = (MarkClosePnl + TradingFee + TradingFeePending + DividendIn).toFixed(2)
|
||
decimal floatPnlSum = decimal.Parse(
|
||
(markClosePnl + tradingFee + tradingFeePending + dividendIn).ToString("F2"));
|
||
|
||
// calcCloseAmount:SwapRealizedPnL/SwapCloseAmount = FloatPnlSum + Σ利息腿 + Σ预付金腿
|
||
decimal swapCloseAmount = floatPnlSum;
|
||
decimal swapRealizedPnL = floatPnlSum;
|
||
decimal swapMarginRebatePnl = 0m;
|
||
foreach (var interest in input.InterestLegs)
|
||
{
|
||
swapCloseAmount += interest.InterestClosePnL;
|
||
swapRealizedPnL += interest.InterestClosePnL;
|
||
}
|
||
foreach (var margin in input.MarginLegs)
|
||
{
|
||
swapCloseAmount += margin.InterestClosePnL;
|
||
swapMarginRebatePnl += margin.InterestClosePnL;
|
||
swapRealizedPnL += margin.InterestClosePnL;
|
||
}
|
||
swapRealizedPnL = StockEqvNotional(swapRealizedPnL);
|
||
swapCloseAmount = StockEqvNotional(swapCloseAmount);
|
||
swapMarginRebatePnl = StockEqvNotional(swapMarginRebatePnl);
|
||
|
||
// TradingAmountFeeAvg = CloseQty==0 ? 0 : (TradingAmountAvg×scale + TradingFee/CloseQty × ratio)
|
||
// 注:unwind 的 ratio = PositionType?1:-1(calcCloseAmount 内重新定义)
|
||
decimal ratio = input.PositionType == 1 ? 1 : -1;
|
||
decimal tradingAmountFeeAvg = input.CloseQty == 0 ? 0 :
|
||
input.TradingAmountAvg * scale + (tradingFee / input.CloseQty) * ratio;
|
||
|
||
return new UnwindResult
|
||
{
|
||
MarkClosePnl = markClosePnl,
|
||
FloatPnlSum = floatPnlSum,
|
||
SwapRealizedPnL = swapRealizedPnL,
|
||
SwapCloseAmount = swapCloseAmount,
|
||
SwapMarginRebatePnl = swapMarginRebatePnl,
|
||
TradingAmountFeeAvg = tradingAmountFeeAvg
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算结息页(income)的盯市盈亏与汇总。
|
||
/// 对应 incomeSwapTrade.js:128-178。
|
||
/// 差异:用剩余持仓数量和合约乘数作量纲,无 longRatio,无 Math.round/10000。
|
||
/// </summary>
|
||
public static UnwindResult CalcIncome(UnwindInput input)
|
||
{
|
||
// income 页 initPosiGrossPrice = PosiGrossPrice = EntryDirtyPrice
|
||
decimal entryPrice = input.PosiGrossPrice;
|
||
decimal scale = input.Multiplier == 100 ? 0.01m : 1m;
|
||
decimal floatRatio = input.PayDirection == 1 ? 1 : -1;
|
||
|
||
decimal tradingFee = ParseOrZero(input.TradingFee);
|
||
decimal tradingFeePending = ParseOrZero(input.TradingFeePending);
|
||
decimal dividendIn = ParseOrZero(input.DividendIn);
|
||
|
||
// MarkClosePnl = PositionQty × ContractSize × (TradingAmountAvg × scale − EntryPrice) × floatRatio
|
||
// (无 longRatio、无 Math.round/10000)
|
||
decimal markClosePnl = input.PositionQty * input.ContractSize * (input.TradingAmountAvg * scale - entryPrice) * floatRatio;
|
||
markClosePnl = StockEqvNotional(markClosePnl);
|
||
|
||
decimal floatPnlSum = decimal.Parse(
|
||
(markClosePnl + tradingFee + tradingFeePending + dividendIn).ToString("F2"));
|
||
|
||
decimal swapCloseAmount = floatPnlSum;
|
||
decimal swapRealizedPnL = floatPnlSum;
|
||
decimal swapMarginRebatePnl = 0m;
|
||
foreach (var interest in input.InterestLegs)
|
||
{
|
||
swapCloseAmount += interest.InterestClosePnL;
|
||
swapRealizedPnL += interest.InterestClosePnL;
|
||
}
|
||
foreach (var margin in input.MarginLegs)
|
||
{
|
||
swapCloseAmount += margin.InterestClosePnL;
|
||
swapMarginRebatePnl += margin.InterestClosePnL;
|
||
swapRealizedPnL += margin.InterestClosePnL;
|
||
}
|
||
// income 页无 SwapMarginAmount 计算(恒为0)
|
||
swapRealizedPnL = StockEqvNotional(swapRealizedPnL);
|
||
swapCloseAmount = StockEqvNotional(swapCloseAmount);
|
||
swapMarginRebatePnl = StockEqvNotional(swapMarginRebatePnl);
|
||
|
||
// TradingAmountFeeAvg = CloseQty>0 ? (TradingAmountAvg×scale + TradingFee/CloseQty × floatRatio) : TradingAmountAvg×scale
|
||
// 注:income 用 floatRatio(PayDirection),与 unwind 的 ratio(PositionType) 不同
|
||
decimal tradingAmountFeeAvg = input.CloseQty > 0
|
||
? input.TradingAmountAvg * scale + (tradingFee / input.CloseQty) * floatRatio
|
||
: input.TradingAmountAvg * scale;
|
||
|
||
return new UnwindResult
|
||
{
|
||
MarkClosePnl = markClosePnl,
|
||
FloatPnlSum = floatPnlSum,
|
||
SwapRealizedPnL = swapRealizedPnL,
|
||
SwapCloseAmount = swapCloseAmount,
|
||
SwapMarginRebatePnl = swapMarginRebatePnl,
|
||
TradingAmountFeeAvg = tradingAmountFeeAvg
|
||
};
|
||
}
|
||
|
||
private static decimal ParseOrZero(string s) => string.IsNullOrEmpty(s) ? 0m : decimal.Parse(s);
|
||
}
|
||
|
||
/// <summary>前端计算输入模型(对应前端可见的原始字段)</summary>
|
||
public class UnwindInput
|
||
{
|
||
public int Multiplier; // 债券=100,非债券=1
|
||
public decimal PosiGrossPrice; // EntryDirtyPrice(期初全价不含费)
|
||
public decimal TradingAmountAvg; // 用户可改的期末标的价格(界面×multiplier形态)
|
||
public decimal CloseQty; // 平仓数量
|
||
public decimal PositionQty; // 结息时的剩余持仓数量
|
||
public decimal ContractSize = 1m; // 合约乘数
|
||
public decimal CloseNotionalValue;// 平仓名义本金
|
||
public int PayDirection; // 1=收取,-1=支付
|
||
public int PositionType; // 1=多头,2=空头
|
||
public string TradingFee; // 交易费用(前端是字符串)
|
||
public string TradingFeePending; // 待结算费用
|
||
public string DividendIn; // 分红
|
||
public List<LegInput> InterestLegs = new();
|
||
public List<LegInput> MarginLegs = new();
|
||
}
|
||
|
||
/// <summary>利息腿/预付金腿输入</summary>
|
||
public class LegInput
|
||
{
|
||
public decimal InterestClosePnL; // 利息腿平仓盈亏(已含方向)
|
||
}
|
||
|
||
/// <summary>前端计算输出模型(前端算出的衍生字段)</summary>
|
||
public class UnwindResult
|
||
{
|
||
public decimal MarkClosePnl;
|
||
public decimal FloatPnlSum;
|
||
public decimal SwapRealizedPnL;
|
||
public decimal SwapCloseAmount;
|
||
public decimal SwapMarginRebatePnl;
|
||
public decimal TradingAmountFeeAvg;
|
||
}
|
||
}
|