diff --git a/UnitTestProject/Modules/SwapModule/SwapFrontendPnlValidateTest.cs b/UnitTestProject/Modules/SwapModule/SwapFrontendPnlValidateTest.cs
new file mode 100644
index 00000000..b861410e
--- /dev/null
+++ b/UnitTestProject/Modules/SwapModule/SwapFrontendPnlValidateTest.cs
@@ -0,0 +1,316 @@
+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 }
+ };
+ }
+ }
+}
diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
index eb960cc4..6b0d5de6 100644
--- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs
+++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs
@@ -1,4 +1,4 @@
-using MoreLinq.Extensions;
+using MoreLinq.Extensions;
using Newtonsoft.Json;
using Qdp.Pricing.Library.Base.Utilities;
using System.Linq.Expressions;
@@ -131,12 +131,14 @@ namespace YLErp.Modules.SwapModule
#region 前端盈亏只读校验(不阻断交易)
+
///
/// 用 FrontendCalcReference 公式重算盈亏,与前端传来的 unwindData 比对,
/// 差异 > 0.01 记 Error 日志。整体 try/catch 吞异常——校验自身错误绝不阻断交易。
///
/// 目的:前端保持快速反馈(用户改输入立即算),后端不替代前端,仅做合理性兜底,
/// 为将来公式统一积累"前后端差异"数据。
+ /// 核心比对逻辑已抽到 SwapFrontendPnlValidator.BuildFrontendValidationDiffs 纯函数,便于单测覆盖。
///
/// 前端算好传入的结算数据
/// true=结息页(income公式),false=平仓页(unwind公式)
@@ -144,51 +146,16 @@ namespace YLErp.Modules.SwapModule
{
try
{
- // 取浮动腿(有 UnderlyingCode 的),与前端 initDeal 取法一致
+ var diffs = SwapFrontendPnlValidator.BuildFrontendValidationDiffs(unwindData, isIncome);
+ if (diffs == null) return;
+
+ // 取浮动腿用于日志上下文(与原实现一致)
var floatLeg = unwindData.FlowEvents?.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
- // PosiGrossPrice 是 [NotMapped],前端可能没传;为空/0 时跳过(避免误报)
- if (floatLeg == null || floatLeg.PosiGrossPrice == 0)
+ foreach (var d in diffs)
{
- return;
+ Logger.Error($"[互换盈亏校验分歧] tradeId={unwindData.SwapTradeId} field={d.Field} frontend={d.FrontendValue} backend={d.BackendValue} diff={d.Delta} " +
+ $"floatLeg=[gross={floatLeg?.PosiGrossPrice} avg={floatLeg?.TradingAmountAvg} qty={floatLeg?.Quantity} payDir={floatLeg?.PayDirection} posType={floatLeg?.PositionType}]");
}
-
- // 用 UnderlyingInstrumentType 推 Multiplier(债券=100,否则1)
- bool isBond = ConsGlobal.InstrumentType.IsBond(floatLeg.UnderlyingInstrumentType);
- int multiplier = isBond ? 100 : 1;
-
- // 分类利息腿/预付金腿(InterestMode 初始预付金/追加预付金→Margin,否则→Interest)
- var input = new UnwindInput
- {
- Multiplier = multiplier,
- PosiGrossPrice = floatLeg.PosiGrossPrice, // EntryDirtyPrice
- TradingAmountAvg = floatLeg.TradingAmountAvg, // ExitDirtyPrice(界面×multiplier形态)
- CloseQty = unwindData.CloseQty,
- PositionQty = unwindData.PositionQty,
- ContractSize = floatLeg.ContractSize,
- CloseNotionalValue = unwindData.CloseNotionalValue,
- PayDirection = floatLeg.PayDirection,
- PositionType = floatLeg.PositionType,
- TradingFee = floatLeg.TradingFee.ToString(),
- TradingFeePending = floatLeg.TradingFeePending.ToString(),
- DividendIn = floatLeg.DividendIn.ToString(),
- };
- foreach (var leg in unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
- {
- var target = (leg.InterestMode == (int)InterestModeEnum.初始预付金
- || leg.InterestMode == (int)InterestModeEnum.追加预付金)
- ? input.MarginLegs : input.InterestLegs;
- target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL });
- }
-
- var recalc = isIncome
- ? FrontendCalcReference.CalcIncome(input)
- : FrontendCalcReference.CalcUnwind(input);
-
- // 逐字段比对,差异 > 0.01 告警
- const decimal threshold = 0.01m;
- CheckDiff(nameof(recalc.SwapRealizedPnL), unwindData.SwapRealizedPnL, recalc.SwapRealizedPnL, threshold, unwindData.SwapTradeId, floatLeg);
- CheckDiff(nameof(recalc.SwapCloseAmount), unwindData.SwapCloseAmount, recalc.SwapCloseAmount, threshold, unwindData.SwapTradeId, floatLeg);
- CheckDiff("MarkClosePnl", floatLeg.MarkClosePnl, recalc.MarkClosePnl, threshold, unwindData.SwapTradeId, floatLeg);
}
catch (Exception ex)
{
@@ -197,16 +164,6 @@ namespace YLErp.Modules.SwapModule
}
}
- private void CheckDiff(string field, decimal frontendVal, decimal backendVal, decimal threshold, int tradeId, swap_flow_event floatLeg)
- {
- decimal diff = frontendVal - backendVal;
- if (Math.Abs(diff) > threshold)
- {
- Logger.Error($"[互换盈亏校验分歧] tradeId={tradeId} field={field} frontend={frontendVal} backend={backendVal} diff={diff} " +
- $"floatLeg=[gross={floatLeg.PosiGrossPrice} avg={floatLeg.TradingAmountAvg} qty={floatLeg.Quantity} payDir={floatLeg.PayDirection} posType={floatLeg.PositionType}]");
- }
- }
-
#endregion
///
@@ -1999,3 +1956,5 @@ namespace YLErp.Modules.SwapModule
}
}
+
+
diff --git a/YLErpDAL/Modules/SwapModule/SwapFrontendPnlValidator.cs b/YLErpDAL/Modules/SwapModule/SwapFrontendPnlValidator.cs
new file mode 100644
index 00000000..db4a1957
--- /dev/null
+++ b/YLErpDAL/Modules/SwapModule/SwapFrontendPnlValidator.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using MoreLinq.Extensions;
+using Newtonsoft.Json;
+using Qdp.Pricing.Library.Base.Utilities;
+using System.Linq.Expressions;
+using YLErp.BLL;
+using YLErp.BLL.Eod;
+using YLErp.DBModels.Enums;
+using YLErp.Helpers;
+using YLErp.Modules.DataProviderModule;
+using YLErp.Modules.EodModule;
+using YLErp.Modules.TradeModule;
+using YLErp.Modules.TradeModule.DealModule;
+using YLErp.QdpModule;
+
+namespace YLErp.Modules.SwapModule
+{
+ ///
+ /// 单条前后端盈亏差异(纯数据,便于单测断言)。
+ /// 原内嵌于 SwapDealService,因 SwapDealService 已属超大文件(2000+ 行),
+ /// 将其与本校验逻辑一并抽离,降低对超大文件的改动面。
+ ///
+ public sealed class FrontendPnlDiff
+ {
+ public string Field { get; init; } = string.Empty;
+ public decimal FrontendValue { get; init; }
+ public decimal BackendValue { get; init; }
+ public decimal Delta => FrontendValue - BackendValue;
+ }
+
+ ///
+ /// 前端盈亏只读校验:用 FrontendCalcReference 公式重算盈亏,与前端传来的 unwindData 逐字段比对。
+ /// 纯函数(无副作用、无 DB/日志依赖),便于无库单测(见 SwapFrontendPnlValidateTest)。
+ /// 返回 null 表示前置条件不满足(无浮动腿或 PosiGrossPrice=0),调用方应跳过。
+ ///
+ /// 从 SwapDealService.ValidateFrontendPnL 抽出,原方法仅保留调用 + 日志。
+ ///
+ public static class SwapFrontendPnlValidator
+ {
+ /// 前端算好传入的结算数据
+ /// true=结息页(income公式),false=平仓页(unwind公式)
+ /// 差异阈值,默认 0.01
+ public static List? BuildFrontendValidationDiffs(
+ UnwindData unwindData, bool isIncome, decimal threshold = 0.01m)
+ {
+ // 取浮动腿(有 UnderlyingCode 的),与前端 initDeal 取法一致
+ var floatLeg = unwindData.FlowEvents?.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode));
+ // PosiGrossPrice 是 [NotMapped],前端可能没传;为空/0 时跳过(避免误报)
+ if (floatLeg == null || floatLeg.PosiGrossPrice == 0)
+ {
+ return null;
+ }
+
+ // 用 UnderlyingInstrumentType 推 Multiplier(债券=100,否则1)
+ bool isBond = ConsGlobal.InstrumentType.IsBond(floatLeg.UnderlyingInstrumentType);
+ int multiplier = isBond ? 100 : 1;
+
+ // 分类利息腿/预付金腿(InterestMode 初始预付金/追加预付金→Margin,否则→Interest)
+ var input = new UnwindInput
+ {
+ Multiplier = multiplier,
+ PosiGrossPrice = floatLeg.PosiGrossPrice, // EntryDirtyPrice
+ TradingAmountAvg = floatLeg.TradingAmountAvg, // ExitDirtyPrice(界面×multiplier形态)
+ CloseQty = unwindData.CloseQty,
+ PositionQty = unwindData.PositionQty,
+ ContractSize = floatLeg.ContractSize,
+ CloseNotionalValue = unwindData.CloseNotionalValue,
+ PayDirection = floatLeg.PayDirection,
+ PositionType = floatLeg.PositionType,
+ TradingFee = floatLeg.TradingFee.ToString(),
+ TradingFeePending = floatLeg.TradingFeePending.ToString(),
+ DividendIn = floatLeg.DividendIn.ToString(),
+ };
+ foreach (var leg in unwindData.FlowEvents.Where(x => string.IsNullOrEmpty(x.UnderlyingCode)))
+ {
+ var target = (leg.InterestMode == (int)InterestModeEnum.初始预付金
+ || leg.InterestMode == (int)InterestModeEnum.追加预付金)
+ ? input.MarginLegs : input.InterestLegs;
+ target.Add(new LegInput { InterestClosePnL = leg.InterestClosePnL });
+ }
+
+ var recalc = isIncome
+ ? FrontendCalcReference.CalcIncome(input)
+ : FrontendCalcReference.CalcUnwind(input);
+
+ var diffs = new List(3);
+ AddDiffIfOverThreshold(diffs, nameof(recalc.SwapRealizedPnL), unwindData.SwapRealizedPnL, recalc.SwapRealizedPnL, threshold);
+ AddDiffIfOverThreshold(diffs, nameof(recalc.SwapCloseAmount), unwindData.SwapCloseAmount, recalc.SwapCloseAmount, threshold);
+ AddDiffIfOverThreshold(diffs, "MarkClosePnl", floatLeg.MarkClosePnl, recalc.MarkClosePnl, threshold);
+ return diffs;
+ }
+
+ private static void AddDiffIfOverThreshold(
+ List diffs, string field, decimal frontendVal, decimal backendVal, decimal threshold)
+ {
+ decimal diff = frontendVal - backendVal;
+ if (Math.Abs(diff) > threshold)
+ {
+ diffs.Add(new FrontendPnlDiff
+ {
+ Field = field,
+ FrontendValue = frontendVal,
+ BackendValue = backendVal
+ });
+ }
+ }
+ }
+}