Files
zszq-trs/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs
T
张名锐 415539704d fix(swap): 修复债券结息价差盈亏计算逻辑
- 将价差盈亏计算从使用CloseNotionalValue改为使用PositionQty和ContractSize
- 更新前端JavaScript代码中的计算公式,按持仓数量和合约乘数计算价差盈亏
- 在UnwindInput模型中添加PositionQty和ContractSize字段
- 修正后台计算服务中的数据映射逻辑
- 添加FC_009测试用例验证债券价差按数量计算的正确性
- 更新现有测试用例的输入参数以匹配新的计算方式
2026-07-17 10:58:31 +08:00

268 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Helpers;
namespace YLErp.Modules.SwapModule
{
/// <summary>
/// 前端计算逻辑特征化测试(Characterization Test
/// ============================================================================
/// 目的:用 golden 冻结前端 JS 的计算行为(含用户可变输入分支),
/// 作为下一轮"计算下沉后端"的金标准——后端结果必须匹配这些 golden。
///
/// 背景:前端 unwindSwapTrade.js / incomeSwapTrade.js 是实时响应式计算器,
/// 用户改标的价格/平仓数量/交易费用/利息金额时,前端立刻重算 MarkClosePnl/
/// SwapRealizedPnL/SwapCloseAmount,后端拿到"前端算好的最终结果"直接记账。
/// 本测试用 C# 忠实重写前端公式作参考实现,手算真实输入的期望值存 golden。
///
/// 命名规范(见命名决策文档):参考实现内部用规范名(EntryPrice/ExitPrice/
/// floatRatio/longRatio),注释标明对应前端字段与规范语义。
/// ============================================================================
[TestClass]
public class FrontendCalcCharacterizationTest
{
private static readonly string GoldenDir = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "FrontendCalc");
// FrontendCalcReference 已搬迁到生产代码 YLErpDAL/Helpers/FrontendCalcReference.cs
// 生产代码(SwapDealService校验)与测试共用同一份公式实现,避免分叉。
// ================================================================
// 8 个测试场景(含用户可变输入分支)
// ================================================================
// ---- 平仓页(unwind)场景 ----
/// <summary>
/// [FC_001] 平仓-债券多头-默认值(基线)
/// EntryDirtyPrice(PosiGrossPrice)=1.02, ExitPrice(TradingAmountAvg,×100形态)=105,
/// CloseQty=1000, PayDirection=1(收取), PositionType=1(多头), TradingFee="20"
/// scale=0.01, floatRatio=1, longRatio=1
/// MarkClosePnl = round(1000×(105×0.011.02)×1×1×10000)/10000 = round(1000×0.03×10000)/10000 = 30
/// </summary>
[TestMethod]
public void FC_001_平仓_债券多头_默认值()
{
var input = new UnwindInput
{
Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m,
CloseQty = 1000, PayDirection = 1, PositionType = 1,
TradingFee = "20", TradingFeePending = "0", DividendIn = "0"
};
var result = FrontendCalcReference.CalcUnwind(input);
// MarkClosePnl = 1000×(1.051.02)×1×1 = 30
AssertDecimalEqual(30m, result.MarkClosePnl, 0.01m, "MarkClosePnl");
// FloatPnlSum = 30 + 20 + 0 + 0 = 50
AssertDecimalEqual(50m, result.FloatPnlSum, 0.01m, "FloatPnlSum");
// SwapRealizedPnL = FloatPnlSum(50)
AssertDecimalEqual(50m, result.SwapRealizedPnL, 0.01m, "SwapRealizedPnL");
Console.WriteLine($"FC_001: MarkClosePnl={result.MarkClosePnl}, FloatPnlSum={result.FloatPnlSum} ✅");
}
/// <summary>
/// [FC_002] 平仓-用户改标的价格(TradingAmountAvg 100→110
/// MarkClosePnl = round(1000×(110×0.011.02)×10000)/10000 = round(1000×0.08×10000)/10000 = 80
/// </summary>
[TestMethod]
public void FC_002_平仓_用户改标的价格()
{
var input = new UnwindInput
{
Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 110m, // 改成110
CloseQty = 1000, PayDirection = 1, PositionType = 1,
TradingFee = "20", TradingFeePending = "0", DividendIn = "0"
};
var result = FrontendCalcReference.CalcUnwind(input);
AssertDecimalEqual(80m, result.MarkClosePnl, 0.01m, "改价格后 MarkClosePnl");
AssertDecimalEqual(100m, result.FloatPnlSum, 0.01m, "改价格后 FloatPnlSum");
Console.WriteLine($"FC_002: 改标的价格后 MarkClosePnl={result.MarkClosePnl} ✅");
}
/// <summary>
/// [FC_003] 平仓-用户改平仓数量(CloseQty 1000→500TradingFeePending 随比例变)
/// MarkClosePnl = round(500×(105×0.011.02)×10000)/10000 = round(500×0.03×10000)/10000 = 15
/// TradingFeePending 按比例=BeforeCloseFee×ClosePercent(0.5),假设=10
/// </summary>
[TestMethod]
public void FC_003_平仓_用户改平仓数量()
{
var input = new UnwindInput
{
Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m,
CloseQty = 500, // 改成500(原1000
PayDirection = 1, PositionType = 1,
TradingFee = "20", TradingFeePending = "10", DividendIn = "0"
};
var result = FrontendCalcReference.CalcUnwind(input);
// MarkClosePnl = 500×0.03 = 15
AssertDecimalEqual(15m, result.MarkClosePnl, 0.01m, "改数量后 MarkClosePnl");
// FloatPnlSum = 15 + 20 + 10 + 0 = 45
AssertDecimalEqual(45m, result.FloatPnlSum, 0.01m, "改数量后 FloatPnlSum");
Console.WriteLine($"FC_003: 改平仓数量后 MarkClosePnl={result.MarkClosePnl} ✅");
}
/// <summary>
/// [FC_004] 平仓-用户改利息金额(InterestClosePnL=100
/// SwapRealizedPnL = FloatPnlSum(50) + InterestClosePnL(100) = 150
/// </summary>
[TestMethod]
public void FC_004_平仓_用户改利息金额()
{
var input = new UnwindInput
{
Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m,
CloseQty = 1000, PayDirection = 1, PositionType = 1,
TradingFee = "20", TradingFeePending = "0", DividendIn = "0"
};
input.InterestLegs.Add(new LegInput { InterestClosePnL = 100m });
var result = FrontendCalcReference.CalcUnwind(input);
AssertDecimalEqual(30m, result.MarkClosePnl, 0.01m, "MarkClosePnl 不受利息影响");
// SwapRealizedPnL = 50 + 100 = 150
AssertDecimalEqual(150m, result.SwapRealizedPnL, 0.01m, "含利息的 SwapRealizedPnL");
Console.WriteLine($"FC_004: 改利息后 SwapRealizedPnL={result.SwapRealizedPnL} ✅");
}
/// <summary>
/// [FC_005] 平仓-非债券空头(PositionType=Short=2, multiplier=1
/// floatRatio=1(收取), longRatio=-1(空头)
/// MarkClosePnl = round(1000×(100×1100)×1×(1)×10000)/10000 = 0(价格不变时空头盈亏=0)
/// 改成价格涨:TradingAmountAvg=105, MarkClosePnl=round(1000×(105100)×1×(1)×10000)/10000=50000
/// 空头价格涨=亏损
/// </summary>
[TestMethod]
public void FC_005_平仓_非债券空头_方向因子()
{
var input = new UnwindInput
{
Multiplier = 1, PosiGrossPrice = 100m, TradingAmountAvg = 105m, // 涨了5
CloseQty = 1000, PayDirection = 1, PositionType = 2, // 空头
TradingFee = "0", TradingFeePending = "0", DividendIn = "0"
};
var result = FrontendCalcReference.CalcUnwind(input);
// 空头价格涨=亏损:1000×(105100)×1×(1) = 5000
AssertDecimalEqual(-5000m, result.MarkClosePnl, 0.01m, "空头价格涨=亏损");
Console.WriteLine($"FC_005: 空头方向因子 MarkClosePnl={result.MarkClosePnl} ✅");
}
// ---- 结息页(income)场景 ----
/// <summary>
/// [FC_006] 结息-债券多头-全量结算(基线)
/// income 使用持仓数量和合约乘数,无 longRatio
/// EntryPrice=1.02, TradingAmountAvg=105(×100形态), PositionQty=10000, ContractSize=1
/// MarkClosePnl = 10000×1×(105×0.011.02)×1 = 10000×0.03 = 300
/// </summary>
[TestMethod]
public void FC_006_结息_债券多头_全量结算()
{
var input = new UnwindInput
{
Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m,
PositionQty = 10000,
ContractSize = 1,
CloseNotionalValue = 10200, // 与数量刻意不同,守卫 income 不再误用名义本金
CloseQty = 0, // income 不用数量
PayDirection = 1, PositionType = 1,
TradingFee = "0", TradingFeePending = "0", DividendIn = "0"
};
var result = FrontendCalcReference.CalcIncome(input);
AssertDecimalEqual(300m, result.MarkClosePnl, 0.01m, "income MarkClosePnl");
AssertDecimalEqual(300m, result.SwapRealizedPnL, 0.01m, "income SwapRealizedPnL");
Console.WriteLine($"FC_006: income MarkClosePnl={result.MarkClosePnl} ✅");
}
/// <summary>
/// [FC_007] 结息-用户改标的价格(TradingAmountAvg 105→110
/// MarkClosePnl = 10000×(110×0.011.02) = 10000×0.08 = 800
/// </summary>
[TestMethod]
public void FC_007_结息_用户改标的价格()
{
var input = new UnwindInput
{
Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 110m,
PositionQty = 10000, ContractSize = 1,
CloseNotionalValue = 10200, CloseQty = 0,
PayDirection = 1, PositionType = 1,
TradingFee = "0", TradingFeePending = "0", DividendIn = "0"
};
var result = FrontendCalcReference.CalcIncome(input);
AssertDecimalEqual(800m, result.MarkClosePnl, 0.01m, "改价格后 income MarkClosePnl");
Console.WriteLine($"FC_007: 改价格后 income MarkClosePnl={result.MarkClosePnl} ✅");
}
/// <summary>
/// [FC_008] 结息-含利息腿与预付金腿(InterestClosePnL + margin InterestClosePnL
/// SwapRealizedPnL = FloatPnlSum(300) + 利息腿(100) + 预付金腿(50) = 450
/// SwapMarginRebatePnl = 预付金腿(50)
/// </summary>
[TestMethod]
public void FC_008_结息_含利息腿与预付金腿_总额()
{
var input = new UnwindInput
{
Multiplier = 100, PosiGrossPrice = 1.02m, TradingAmountAvg = 105m,
PositionQty = 10000, ContractSize = 1,
CloseNotionalValue = 10200, CloseQty = 0,
PayDirection = 1, PositionType = 1,
TradingFee = "0", TradingFeePending = "0", DividendIn = "0"
};
input.InterestLegs.Add(new LegInput { InterestClosePnL = 100m });
input.MarginLegs.Add(new LegInput { InterestClosePnL = 50m });
var result = FrontendCalcReference.CalcIncome(input);
// SwapRealizedPnL = 300 + 100 + 50 = 450
AssertDecimalEqual(450m, result.SwapRealizedPnL, 0.01m, "含利息+预付金的 SwapRealizedPnL");
// SwapMarginRebatePnl = 50
AssertDecimalEqual(50m, result.SwapMarginRebatePnl, 0.01m, "SwapMarginRebatePnl");
Console.WriteLine($"FC_008: SwapRealizedPnL={result.SwapRealizedPnL}, SwapMarginRebatePnl={result.SwapMarginRebatePnl} ✅");
}
/// <summary>
/// [FC_009] 结息-债券支付端:价差盈亏必须按数量计算,不能按期初名义本金计算。
/// 纯价差 = 30000000×1×(80%98%)×(1) = 5400000;加分红-45000后合计5355000。
/// </summary>
[TestMethod]
public void FC_009_结息_债券价差按数量计算()
{
var input = new UnwindInput
{
Multiplier = 100,
PosiGrossPrice = 0.98m,
TradingAmountAvg = 80m,
PositionQty = 30000000m,
ContractSize = 1m,
CloseNotionalValue = 29400000m,
CloseQty = 0m,
PayDirection = 2,
PositionType = 1,
TradingFee = "0",
TradingFeePending = "0",
DividendIn = "-45000"
};
var result = FrontendCalcReference.CalcIncome(input);
AssertDecimalEqual(5400000m, result.MarkClosePnl, 0.01m, "income MarkClosePnl按数量计算");
AssertDecimalEqual(5355000m, result.FloatPnlSum, 0.01m, "income FloatPnlSum包含分红");
}
private static void AssertDecimalEqual(decimal expected, decimal actual, decimal tolerance, string message = "")
{
Assert.IsTrue(Math.Abs(expected - actual) <= tolerance,
$"{message} Expected: {expected}, Actual: {actual}, Diff: {expected - actual}");
}
}
}