Files
zszq-trs/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs
张名锐 d78d1f48e7 fix(swap): 修复互换价格变动时盈亏计算错误
- 在后端计算逻辑中添加 longRatio 参数用于区分多空头寸类型
- 修改前端 JavaScript 代码中的盈亏计算公式,加入 longRatio 参与计算
- 更新单元测试,增加空头头寸价格上涨时亏损的测试用例
- 补充前端测试用例验证空头头寸计算逻辑的准确性
- 修复了空头头寸在价格上涨时显示盈利而非亏损的计算错误
2026-08-07 17:44:04 +08:00

293 lines
14 KiB
C#
Raw Permalink 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包含分红");
}
[TestMethod]
public void 收取空头_价格上涨_应为亏损()
{
var input = new UnwindInput
{
Multiplier = 100,
PosiGrossPrice = 1.01654321m,
TradingAmountAvg = 101.754321m,
PositionQty = 50000000m,
ContractSize = 1m,
CloseNotionalValue = 50827160.5m,
CloseQty = 0m,
PayDirection = 1,
PositionType = 2,
TradingFee = "0",
TradingFeePending = "0",
DividendIn = "-90400"
};
var result = FrontendCalcReference.CalcIncome(input);
AssertDecimalEqual(-50000m, result.MarkClosePnl, 0.01m, "收取空头价格上涨=盯市亏损");
AssertDecimalEqual(-140400m, result.FloatPnlSum, 0.01m, "盯市亏损加分红");
}
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}");
}
}
}