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 + }); + } + } + } +}