using YLErp.DBModels;
using YLErp.DBModels.Enums;
using YLErp.Helpers;
namespace YLErp.Modules.SwapModule
{
///
/// SwapFrontendPnlValidator.BuildFrontendValidationDiffs 的回归测试。
/// -----------------------------------------------------------------
/// 守卫提交 41553970 "fix(swap): 修复债券结息价差盈亏计算逻辑"。
///
/// 背景:ValidateFrontendPnL 用 FrontendCalcReference 重算盈亏与前端值比对,
/// 原为 private void + 吞异常,无法单测。拆出 BuildFrontendValidationDiffs 纯函数:
/// - 入参:UnwindData + isIncome
/// - 返回:null(前置条件不满足)或 List<FrontendPnlDiff>(超阈值的差异项)
/// - 副作用:无(日志留在 ValidateFrontendPnL 外层)
///
/// 本测试锁定:
/// 1) 前端值与后端重算一致 → 返回空列表
/// 2) 前端值与后端重算不一致 → 返回对应字段差异
/// 3) 无浮动腿 → 返回 null
/// 4) PosiGrossPrice=0 → 返回 null
/// 5) PositionQty/CloseQty 口径(41553970 修复点)正确传入
///
[TestClass]
public class SwapFrontendPnlValidateTest
{
// ================================================================
// 场景1:前端值与后端重算一致 → 返回空列表
// 用 FC_001 同款输入:债券多头,PosiGrossPrice=1.02, TradingAmountAvg=105,
// CloseQty=1000, PayDirection=1, PositionType=1, TradingFee="20"
// 后端重算:MarkClosePnl=30, FloatPnlSum=50, SwapRealizedPnL=50, SwapCloseAmount=50
// 前端也填这些值 → 无差异
// ================================================================
[TestMethod]
public void 前后端一致_返回空差异列表()
{
var unwindData = BuildBaseUnwindData(
posiGrossPrice: 1.02m, tradingAmountAvg: 105m,
closeQty: 1000, positionQty: 1000,
tradingFee: "20", tradingFeePending: "0", dividendIn: "0",
payDirection: 1, positionType: 1,
swapRealizedPnL: 50m, swapCloseAmount: 50m, markClosePnl: 30m);
var diffs = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindData, isIncome: false);
Assert.IsNotNull(diffs, "前置条件满足应返回列表而非 null");
Assert.AreEqual(0, diffs.Count,
$"前后端一致应无差异,实际 {diffs.Count} 条:{string.Join(",", diffs.Select(d => d.Field))}");
}
// ================================================================
// 场景2:SwapRealizedPnL 前端填错 → 返回该字段差异
// 后端重算 SwapRealizedPnL=50, SwapCloseAmount=50;
// 前端 SwapRealizedPnL 故意填 60(SwapCloseAmount 保持 50 一致)→ 只 SwapRealizedPnL 有差异
// ================================================================
[TestMethod]
public void SwapRealizedPnL前端填错_返回该字段差异()
{
var unwindData = BuildBaseUnwindData(
posiGrossPrice: 1.02m, tradingAmountAvg: 105m,
closeQty: 1000, positionQty: 1000,
tradingFee: "20", tradingFeePending: "0", dividendIn: "0",
payDirection: 1, positionType: 1,
swapRealizedPnL: 60m, // 故意填错(正确=50)
swapCloseAmount: 50m, // 保持一致
markClosePnl: 30m); // 保持一致
var diffs = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindData, isIncome: false);
Assert.IsNotNull(diffs);
CollectionAssert.AreEquivalent(
new[] { "SwapRealizedPnL" },
diffs.Select(d => d.Field).ToArray(),
"应只捕获 SwapRealizedPnL 的差异");
var diff = diffs.Single(d => d.Field == "SwapRealizedPnL");
Assert.AreEqual(60m, diff.FrontendValue, "前端值=60");
Assert.AreEqual(50m, diff.BackendValue, 0.01m, "后端重算=50");
Assert.AreEqual(10m, diff.Delta, 0.01m, "Delta=10");
}
// ================================================================
// 场景3:MarkClosePnl 前端填错 → 返回该字段差异
// 后端重算 MarkClosePnl=30;前端故意填 25(其他保持一致)→ 只 MarkClosePnl 有差异
// ================================================================
[TestMethod]
public void MarkClosePnl前端填错_返回该字段差异()
{
var unwindData = BuildBaseUnwindData(
posiGrossPrice: 1.02m, tradingAmountAvg: 105m,
closeQty: 1000, positionQty: 1000,
tradingFee: "20", tradingFeePending: "0", dividendIn: "0",
payDirection: 1, positionType: 1,
swapRealizedPnL: 50m,
swapCloseAmount: 50m,
markClosePnl: 25m); // 故意填错(正确=30)
var diffs = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindData, isIncome: false);
Assert.IsNotNull(diffs);
CollectionAssert.AreEquivalent(
new[] { "MarkClosePnl" },
diffs.Select(d => d.Field).ToArray(),
"应只捕获 MarkClosePnl 的差异");
var diff = diffs.Single(d => d.Field == "MarkClosePnl");
Assert.AreEqual(25m, diff.FrontendValue);
Assert.AreEqual(30m, diff.BackendValue, 0.01m);
Assert.AreEqual(-5m, diff.Delta, 0.01m);
}
// ================================================================
// 场景4:无浮动腿(FlowEvents 为空)→ 返回 null
// ================================================================
[TestMethod]
public void 无浮动腿_返回null()
{
var unwindData = new UnwindData
{
SwapTradeId = 1,
CloseQty = 1000,
PositionQty = 1000,
FlowEvents = new List() // 完全空
};
var diffs = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindData, isIncome: false);
Assert.IsNull(diffs, "无浮动腿应返回 null(跳过校验)");
}
// ================================================================
// 场景5:浮动腿 PosiGrossPrice=0 → 返回 null(避免误报)
// ================================================================
[TestMethod]
public void 浮动腿PosiGrossPrice为零_返回null()
{
var unwindData = BuildBaseUnwindData(
posiGrossPrice: 0m, // 前端未传 → 0
tradingAmountAvg: 105m,
closeQty: 1000, positionQty: 1000,
tradingFee: "20", tradingFeePending: "0", dividendIn: "0",
payDirection: 1, positionType: 1,
swapRealizedPnL: 50m, swapCloseAmount: 0m, markClosePnl: 30m);
var diffs = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindData, isIncome: false);
Assert.IsNull(diffs, "PosiGrossPrice=0 应返回 null(避免误报)");
}
// ================================================================
// 场景6:41553970 修复点 —— PositionQty 必须正确传入后端重算
// 旧 bug:PositionQty 未传入,导致部分平仓时盈亏口径错误。
// 验证:PositionQty != CloseQty 时,后端重算仍按真实 PositionQty 走
// (本场景构造部分平仓:CloseQty=500, PositionQty=1000)
// 平仓页 unwind 用 CloseQty 算 MarkClosePnl:
// MarkClosePnl = 500×(1.05−1.02)×1×1 = 15
// FloatPnlSum = 15 + 20 + 0 + 0 = 35
// SwapRealizedPnL = SwapCloseAmount = 35
// ================================================================
[TestMethod]
public void 部分平仓_PositionQty正确传入后端重算()
{
var unwindData = BuildBaseUnwindData(
posiGrossPrice: 1.02m, tradingAmountAvg: 105m,
closeQty: 500, positionQty: 1000,
tradingFee: "20", tradingFeePending: "0", dividendIn: "0",
payDirection: 1, positionType: 1,
swapRealizedPnL: 35m, // 与后端重算一致
swapCloseAmount: 35m, // 与后端重算一致
markClosePnl: 15m); // 与后端重算一致
var diffs = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindData, isIncome: false);
Assert.IsNotNull(diffs);
Assert.AreEqual(0, diffs.Count,
$"部分平仓 PositionQty 正确传入应无差异,实际 {diffs.Count} 条:" +
string.Join(",", diffs.Select(d => $"{d.Field}(fe={d.FrontendValue},be={d.BackendValue})")));
}
// ================================================================
// 场景7:阈值边界 —— 差异恰好等于阈值(0.01)不报,超过才报
// 后端重算 SwapRealizedPnL=50, SwapCloseAmount=50;
// 前端 SwapRealizedPnL 填 50.01 → 差异 0.01 不> 0.01 → 不报
// 前端 SwapRealizedPnL 填 50.02 → 差异 0.02 > 0.01 → 报
// (SwapCloseAmount 保持 50 一致,不参与本场景断言)
// ================================================================
[TestMethod]
public void 阈值边界_差异等于阈值不报_超过才报()
{
// 差异 = 0.01,不 > 0.01,不报
var unwindDataEq = BuildBaseUnwindData(
posiGrossPrice: 1.02m, tradingAmountAvg: 105m,
closeQty: 1000, positionQty: 1000,
tradingFee: "20", tradingFeePending: "0", dividendIn: "0",
payDirection: 1, positionType: 1,
swapRealizedPnL: 50.01m, swapCloseAmount: 50m, markClosePnl: 30m);
var diffsEq = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindDataEq, isIncome: false);
Assert.IsNotNull(diffsEq);
Assert.IsFalse(diffsEq.Any(d => d.Field == "SwapRealizedPnL"),
"差异=0.01 不> 阈值,不应报 SwapRealizedPnL");
// 差异 = 0.02 > 0.01,报
var unwindDataOver = BuildBaseUnwindData(
posiGrossPrice: 1.02m, tradingAmountAvg: 105m,
closeQty: 1000, positionQty: 1000,
tradingFee: "20", tradingFeePending: "0", dividendIn: "0",
payDirection: 1, positionType: 1,
swapRealizedPnL: 50.02m, swapCloseAmount: 50m, markClosePnl: 30m);
var diffsOver = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindDataOver, isIncome: false);
Assert.IsNotNull(diffsOver);
Assert.IsTrue(diffsOver.Any(d => d.Field == "SwapRealizedPnL"),
"差异=0.02 > 阈值,应报 SwapRealizedPnL");
}
// ================================================================
// 场景8:isIncome=true 走 CalcIncome 路径 —— 确保分支选择正确
// 结息页公式与平仓页不同,构造一致场景验证不抛异常且返回列表
// ================================================================
[TestMethod]
public void IsIncome为true_走CalcIncome分支_返回列表()
{
// income 页 MarkClosePnl = PositionQty × ContractSize × (ExitPrice×scale − EntryPrice) × floatRatio
// = 1000 × 1 × (105×0.01 − 1.02) × 1 = 30
// FloatPnlSum = 30 + 20 = 50;SwapRealizedPnL = SwapCloseAmount = 50
var unwindData = BuildBaseUnwindData(
posiGrossPrice: 1.02m, tradingAmountAvg: 105m,
closeQty: 1000, positionQty: 1000,
tradingFee: "20", tradingFeePending: "0", dividendIn: "0",
payDirection: 1, positionType: 1,
swapRealizedPnL: 50m, swapCloseAmount: 50m, markClosePnl: 30m);
var diffs = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindData, isIncome: true);
Assert.IsNotNull(diffs, "isIncome=true 也应返回列表(可能为空或有差异)");
// 不锁死具体差异,只验证分支可达、不抛异常
}
// ================================================================
// 场景9:自定义阈值 —— threshold=1.0 时小差异不报
// 后端 SwapRealizedPnL=50, SwapCloseAmount=50;
// 前端 SwapRealizedPnL=50.5(差异 0.5 < 1.0 不报),SwapCloseAmount=50 一致
// ================================================================
[TestMethod]
public void 自定义大阈值_小差异不报()
{
var unwindData = BuildBaseUnwindData(
posiGrossPrice: 1.02m, tradingAmountAvg: 105m,
closeQty: 1000, positionQty: 1000,
tradingFee: "20", tradingFeePending: "0", dividendIn: "0",
payDirection: 1, positionType: 1,
swapRealizedPnL: 50.5m, // 差异 0.5
swapCloseAmount: 50m,
markClosePnl: 30m);
var diffs = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindData, isIncome: false, threshold: 1.0m);
Assert.IsNotNull(diffs);
Assert.IsFalse(diffs.Any(d => d.Field == "SwapRealizedPnL"),
"threshold=1.0 时差异 0.5 不应报");
}
// ================================================================
// Helper:构造带一条浮动腿 + 一条利息腿的 UnwindData
// 默认用债券(UnderlyingInstrumentType 走 IsBond=true → multiplier=100)
// 字段值与 FrontendCalcCharacterizationTest.FC_001 对齐
// ================================================================
private static UnwindData BuildBaseUnwindData(
decimal posiGrossPrice,
decimal tradingAmountAvg,
decimal closeQty,
decimal positionQty,
string tradingFee,
string tradingFeePending,
string dividendIn,
int payDirection,
int positionType,
decimal swapRealizedPnL,
decimal swapCloseAmount,
decimal markClosePnl)
{
// 浮动腿(债券,有 UnderlyingCode)
var floatLeg = new swap_flow_event
{
UnderlyingCode = "511160.SH",
UnderlyingInstrumentType = "Bond",
PosiGrossPrice = posiGrossPrice,
TradingAmountAvg = tradingAmountAvg,
ContractSize = 1m,
PayDirection = payDirection,
PositionType = positionType,
TradingFee = decimal.Parse(tradingFee),
TradingFeePending = decimal.Parse(tradingFeePending),
DividendIn = decimal.Parse(dividendIn),
MarkClosePnl = markClosePnl,
InterestMode = (int)InterestModeEnum.标的期初全价
};
// 利息腿(无 UnderlyingCode)
var interestLeg = new swap_flow_event
{
InterestMode = (int)InterestModeEnum.固定值,
InterestClosePnL = 0m
};
return new UnwindData
{
SwapTradeId = 1,
CloseQty = closeQty,
PositionQty = positionQty,
CloseNotionalValue = closeQty * 100m, // 债券面值 100
SwapRealizedPnL = swapRealizedPnL,
SwapCloseAmount = swapCloseAmount,
FlowEvents = new List { floatLeg, interestLeg }
};
}
}
}