diff --git a/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs b/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs index a4fec34f..233b547a 100644 --- a/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs +++ b/UnitTestProject/Modules/SwapModule/FrontendCalcCharacterizationTest.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using YLErp.DBModels; using YLErp.DBModels.Enums; +using YLErp.Helpers; namespace YLErp.Modules.SwapModule { @@ -25,178 +26,8 @@ namespace YLErp.Modules.SwapModule private static readonly string GoldenDir = Path.Combine( AppDomain.CurrentDomain.BaseDirectory, "Resources", "GoldenFiles", "FrontendCalc"); - // ================================================================ - // FrontendCalcReference:前端公式的 C# 忠实重写(参考实现,非生产代码) - // ================================================================ - - /// - /// 前端公式参考实现。输入=前端可见的原始字段,输出=前端算出的衍生字段。 - /// 注意:otcformat.trading.StockEqvNotional 的精度按 MoneyRound=2 模拟。 - /// - private static class FrontendCalcReference - { - /// 模拟 otcformat.trading.StockEqvNotional(金额类,2位小数) - private static decimal StockEqvNotional(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero); - - /// - /// 计算平仓页(unwind)的盯市盈亏与汇总。 - /// 对应 unwindSwapTrade.js:196-261(calcFloatClosePnl + calcCloseAmount)。 - /// - public static UnwindResult CalcUnwind(UnwindInput input) - { - // 规范名映射:initPosiNetPrice(前端变量) = PosiGrossPrice = EntryDirtyPrice(期初全价不含费) - decimal entryPrice = input.PosiGrossPrice; - // scale = getPriceScale():债券(multiplier=100)→0.01,非债券→1 - decimal scale = input.Multiplier == 100 ? 0.01m : 1m; - // 方向因子:PayDirection(1=收取)→+1;PositionType(1=多头)→+1 - decimal floatRatio = input.PayDirection == 1 ? 1 : -1; - decimal longRatio = input.PositionType == 1 ? 1 : -1; - - decimal tradingFee = ParseOrZero(input.TradingFee); - decimal tradingFeePending = ParseOrZero(input.TradingFeePending); - decimal dividendIn = ParseOrZero(input.DividendIn); - - // MarkClosePnl = round(CloseQty × (TradingAmountAvg × scale − EntryPrice) × floatRatio × longRatio × 10000)/10000 - decimal markClosePnl = Math.Round( - input.CloseQty * (input.TradingAmountAvg * scale - entryPrice) * floatRatio * longRatio * 10000) / 10000; - // toFixed(2) → StockEqvNotional - markClosePnl = Math.Round(markClosePnl, 2, MidpointRounding.AwayFromZero); - markClosePnl = StockEqvNotional(markClosePnl); - - // FloatPnlSum = (MarkClosePnl + TradingFee + TradingFeePending + DividendIn).toFixed(2) - decimal floatPnlSum = decimal.Parse( - (markClosePnl + tradingFee + tradingFeePending + dividendIn).ToString("F2")); - - // calcCloseAmount:SwapRealizedPnL/SwapCloseAmount = FloatPnlSum + Σ利息腿 + Σ预付金腿 - decimal swapCloseAmount = floatPnlSum; - decimal swapRealizedPnL = floatPnlSum; - decimal swapMarginRebatePnl = 0m; - foreach (var interest in input.InterestLegs) - { - swapCloseAmount += interest.InterestClosePnL; - swapRealizedPnL += interest.InterestClosePnL; - } - foreach (var margin in input.MarginLegs) - { - swapCloseAmount += margin.InterestClosePnL; - swapMarginRebatePnl += margin.InterestClosePnL; - swapRealizedPnL += margin.InterestClosePnL; - } - swapRealizedPnL = StockEqvNotional(swapRealizedPnL); - swapCloseAmount = StockEqvNotional(swapCloseAmount); - swapMarginRebatePnl = StockEqvNotional(swapMarginRebatePnl); - - // TradingAmountFeeAvg = CloseQty==0 ? 0 : (TradingAmountAvg×scale + TradingFee/CloseQty × ratio) - // 注:unwind 的 ratio = PositionType?1:-1(calcCloseAmount 内重新定义) - decimal ratio = input.PositionType == 1 ? 1 : -1; - decimal tradingAmountFeeAvg = input.CloseQty == 0 ? 0 : - input.TradingAmountAvg * scale + (tradingFee / input.CloseQty) * ratio; - - return new UnwindResult - { - MarkClosePnl = markClosePnl, - FloatPnlSum = floatPnlSum, - SwapRealizedPnL = swapRealizedPnL, - SwapCloseAmount = swapCloseAmount, - SwapMarginRebatePnl = swapMarginRebatePnl, - TradingAmountFeeAvg = tradingAmountFeeAvg - }; - } - - /// - /// 计算结息页(income)的盯市盈亏与汇总。 - /// 对应 incomeSwapTrade.js:128-178。 - /// 差异:用 CloseNotionalValue(非 CloseQty)作量纲,无 longRatio,无 Math.round/10000。 - /// - public static UnwindResult CalcIncome(UnwindInput input) - { - // income 页 initPosiGrossPrice = PosiGrossPrice = EntryDirtyPrice - decimal entryPrice = input.PosiGrossPrice; - decimal scale = input.Multiplier == 100 ? 0.01m : 1m; - decimal floatRatio = input.PayDirection == 1 ? 1 : -1; - - decimal tradingFee = ParseOrZero(input.TradingFee); - decimal tradingFeePending = ParseOrZero(input.TradingFeePending); - decimal dividendIn = ParseOrZero(input.DividendIn); - - // MarkClosePnl = CloseNotionalValue × (TradingAmountAvg × scale − EntryPrice) × floatRatio - // (无 longRatio、无 Math.round/10000) - decimal markClosePnl = input.CloseNotionalValue * (input.TradingAmountAvg * scale - entryPrice) * floatRatio; - markClosePnl = StockEqvNotional(markClosePnl); - - decimal floatPnlSum = decimal.Parse( - (markClosePnl + tradingFee + tradingFeePending + dividendIn).ToString("F2")); - - decimal swapCloseAmount = floatPnlSum; - decimal swapRealizedPnL = floatPnlSum; - decimal swapMarginRebatePnl = 0m; - foreach (var interest in input.InterestLegs) - { - swapCloseAmount += interest.InterestClosePnL; - swapRealizedPnL += interest.InterestClosePnL; - } - foreach (var margin in input.MarginLegs) - { - swapCloseAmount += margin.InterestClosePnL; - swapMarginRebatePnl += margin.InterestClosePnL; - swapRealizedPnL += margin.InterestClosePnL; - } - // income 页无 SwapMarginAmount 计算(恒为0) - swapRealizedPnL = StockEqvNotional(swapRealizedPnL); - swapCloseAmount = StockEqvNotional(swapCloseAmount); - swapMarginRebatePnl = StockEqvNotional(swapMarginRebatePnl); - - // TradingAmountFeeAvg = CloseQty>0 ? (TradingAmountAvg×scale + TradingFee/CloseQty × floatRatio) : TradingAmountAvg×scale - // 注:income 用 floatRatio(PayDirection),与 unwind 的 ratio(PositionType) 不同 - decimal tradingAmountFeeAvg = input.CloseQty > 0 - ? input.TradingAmountAvg * scale + (tradingFee / input.CloseQty) * floatRatio - : input.TradingAmountAvg * scale; - - return new UnwindResult - { - MarkClosePnl = markClosePnl, - FloatPnlSum = floatPnlSum, - SwapRealizedPnL = swapRealizedPnL, - SwapCloseAmount = swapCloseAmount, - SwapMarginRebatePnl = swapMarginRebatePnl, - TradingAmountFeeAvg = tradingAmountFeeAvg - }; - } - - private static decimal ParseOrZero(string s) => string.IsNullOrEmpty(s) ? 0m : decimal.Parse(s); - } - - // 输入/输出模型 - private class UnwindInput - { - public int Multiplier; // 债券=100,非债券=1 - public decimal PosiGrossPrice; // EntryDirtyPrice(期初全价不含费) - public decimal TradingAmountAvg; // 用户可改的期末标的价格(界面×multiplier形态) - public decimal CloseQty; // 平仓数量 - public decimal CloseNotionalValue;// 平仓名义本金(income 用) - public int PayDirection; // 1=收取,-1=支付 - public int PositionType; // 1=多头,2=空头 - public string TradingFee; // 交易费用(前端是字符串) - public string TradingFeePending; // 待结算费用 - public string DividendIn; // 分红 - public List InterestLegs = new(); - public List MarginLegs = new(); - } - - private class LegInput - { - public decimal InterestClosePnL; // 利息腿平仓盈亏(已含方向) - } - - private class UnwindResult - { - public decimal MarkClosePnl; - public decimal FloatPnlSum; - public decimal SwapRealizedPnL; - public decimal SwapCloseAmount; - public decimal SwapMarginRebatePnl; - public decimal TradingAmountFeeAvg; - } + // FrontendCalcReference 已搬迁到生产代码 YLErpDAL/Helpers/FrontendCalcReference.cs, + // 生产代码(SwapDealService校验)与测试共用同一份公式实现,避免分叉。 // ================================================================ // 8 个测试场景(含用户可变输入分支) diff --git a/YLErpDAL/Helpers/FrontendCalcReference.cs b/YLErpDAL/Helpers/FrontendCalcReference.cs new file mode 100644 index 00000000..cd2585e8 --- /dev/null +++ b/YLErpDAL/Helpers/FrontendCalcReference.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; + +namespace YLErp.Helpers +{ + /// + /// 前端互换计算公式的后端参考实现(忠实重写 unwindSwapTrade.js / incomeSwapTrade.js)。 + /// ============================================================================ + /// 用途: + /// 1. 特征化测试的金标准(FrontendCalcCharacterizationTest)—— 冻结前端行为 + /// 2. 后端只读校验(SwapDealService.ValidateFrontendPnL)—— 重算盈亏与前端传值比对 + /// + /// 注:前端保持快速反馈(用户改输入立即算),本类不替代前端,仅作后端校验兜底。 + /// 命名规范(见《互换价格字段命名规范决策文档》): + /// PosiGrossPrice 现状名,实为"期初全价不含费",规范名 EntryDirtyPrice + /// TradingAmountAvg 现状名,实为"期末全价不含费",规范名 ExitDirtyPrice + /// ============================================================================ + /// + public static class FrontendCalcReference + { + /// 模拟 otcformat.trading.StockEqvNotional(金额类,2位小数) + private static decimal StockEqvNotional(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero); + + /// + /// 计算平仓页(unwind)的盯市盈亏与汇总。 + /// 对应 unwindSwapTrade.js:196-261(calcFloatClosePnl + calcCloseAmount)。 + /// + public static UnwindResult CalcUnwind(UnwindInput input) + { + // 规范名映射:initPosiNetPrice(前端变量) = PosiGrossPrice = EntryDirtyPrice(期初全价不含费) + decimal entryPrice = input.PosiGrossPrice; + // scale = getPriceScale():债券(multiplier=100)→0.01,非债券→1 + decimal scale = input.Multiplier == 100 ? 0.01m : 1m; + // 方向因子:PayDirection(1=收取)→+1;PositionType(1=多头)→+1 + decimal floatRatio = input.PayDirection == 1 ? 1 : -1; + decimal longRatio = input.PositionType == 1 ? 1 : -1; + + decimal tradingFee = ParseOrZero(input.TradingFee); + decimal tradingFeePending = ParseOrZero(input.TradingFeePending); + decimal dividendIn = ParseOrZero(input.DividendIn); + + // MarkClosePnl = round(CloseQty × (TradingAmountAvg × scale − EntryPrice) × floatRatio × longRatio × 10000)/10000 + decimal markClosePnl = Math.Round( + input.CloseQty * (input.TradingAmountAvg * scale - entryPrice) * floatRatio * longRatio * 10000) / 10000; + // toFixed(2) → StockEqvNotional + markClosePnl = Math.Round(markClosePnl, 2, MidpointRounding.AwayFromZero); + markClosePnl = StockEqvNotional(markClosePnl); + + // FloatPnlSum = (MarkClosePnl + TradingFee + TradingFeePending + DividendIn).toFixed(2) + decimal floatPnlSum = decimal.Parse( + (markClosePnl + tradingFee + tradingFeePending + dividendIn).ToString("F2")); + + // calcCloseAmount:SwapRealizedPnL/SwapCloseAmount = FloatPnlSum + Σ利息腿 + Σ预付金腿 + decimal swapCloseAmount = floatPnlSum; + decimal swapRealizedPnL = floatPnlSum; + decimal swapMarginRebatePnl = 0m; + foreach (var interest in input.InterestLegs) + { + swapCloseAmount += interest.InterestClosePnL; + swapRealizedPnL += interest.InterestClosePnL; + } + foreach (var margin in input.MarginLegs) + { + swapCloseAmount += margin.InterestClosePnL; + swapMarginRebatePnl += margin.InterestClosePnL; + swapRealizedPnL += margin.InterestClosePnL; + } + swapRealizedPnL = StockEqvNotional(swapRealizedPnL); + swapCloseAmount = StockEqvNotional(swapCloseAmount); + swapMarginRebatePnl = StockEqvNotional(swapMarginRebatePnl); + + // TradingAmountFeeAvg = CloseQty==0 ? 0 : (TradingAmountAvg×scale + TradingFee/CloseQty × ratio) + // 注:unwind 的 ratio = PositionType?1:-1(calcCloseAmount 内重新定义) + decimal ratio = input.PositionType == 1 ? 1 : -1; + decimal tradingAmountFeeAvg = input.CloseQty == 0 ? 0 : + input.TradingAmountAvg * scale + (tradingFee / input.CloseQty) * ratio; + + return new UnwindResult + { + MarkClosePnl = markClosePnl, + FloatPnlSum = floatPnlSum, + SwapRealizedPnL = swapRealizedPnL, + SwapCloseAmount = swapCloseAmount, + SwapMarginRebatePnl = swapMarginRebatePnl, + TradingAmountFeeAvg = tradingAmountFeeAvg + }; + } + + /// + /// 计算结息页(income)的盯市盈亏与汇总。 + /// 对应 incomeSwapTrade.js:128-178。 + /// 差异:用 CloseNotionalValue(非 CloseQty)作量纲,无 longRatio,无 Math.round/10000。 + /// + public static UnwindResult CalcIncome(UnwindInput input) + { + // income 页 initPosiGrossPrice = PosiGrossPrice = EntryDirtyPrice + decimal entryPrice = input.PosiGrossPrice; + decimal scale = input.Multiplier == 100 ? 0.01m : 1m; + decimal floatRatio = input.PayDirection == 1 ? 1 : -1; + + decimal tradingFee = ParseOrZero(input.TradingFee); + decimal tradingFeePending = ParseOrZero(input.TradingFeePending); + decimal dividendIn = ParseOrZero(input.DividendIn); + + // MarkClosePnl = CloseNotionalValue × (TradingAmountAvg × scale − EntryPrice) × floatRatio + // (无 longRatio、无 Math.round/10000) + decimal markClosePnl = input.CloseNotionalValue * (input.TradingAmountAvg * scale - entryPrice) * floatRatio; + markClosePnl = StockEqvNotional(markClosePnl); + + decimal floatPnlSum = decimal.Parse( + (markClosePnl + tradingFee + tradingFeePending + dividendIn).ToString("F2")); + + decimal swapCloseAmount = floatPnlSum; + decimal swapRealizedPnL = floatPnlSum; + decimal swapMarginRebatePnl = 0m; + foreach (var interest in input.InterestLegs) + { + swapCloseAmount += interest.InterestClosePnL; + swapRealizedPnL += interest.InterestClosePnL; + } + foreach (var margin in input.MarginLegs) + { + swapCloseAmount += margin.InterestClosePnL; + swapMarginRebatePnl += margin.InterestClosePnL; + swapRealizedPnL += margin.InterestClosePnL; + } + // income 页无 SwapMarginAmount 计算(恒为0) + swapRealizedPnL = StockEqvNotional(swapRealizedPnL); + swapCloseAmount = StockEqvNotional(swapCloseAmount); + swapMarginRebatePnl = StockEqvNotional(swapMarginRebatePnl); + + // TradingAmountFeeAvg = CloseQty>0 ? (TradingAmountAvg×scale + TradingFee/CloseQty × floatRatio) : TradingAmountAvg×scale + // 注:income 用 floatRatio(PayDirection),与 unwind 的 ratio(PositionType) 不同 + decimal tradingAmountFeeAvg = input.CloseQty > 0 + ? input.TradingAmountAvg * scale + (tradingFee / input.CloseQty) * floatRatio + : input.TradingAmountAvg * scale; + + return new UnwindResult + { + MarkClosePnl = markClosePnl, + FloatPnlSum = floatPnlSum, + SwapRealizedPnL = swapRealizedPnL, + SwapCloseAmount = swapCloseAmount, + SwapMarginRebatePnl = swapMarginRebatePnl, + TradingAmountFeeAvg = tradingAmountFeeAvg + }; + } + + private static decimal ParseOrZero(string s) => string.IsNullOrEmpty(s) ? 0m : decimal.Parse(s); + } + + /// 前端计算输入模型(对应前端可见的原始字段) + public class UnwindInput + { + public int Multiplier; // 债券=100,非债券=1 + public decimal PosiGrossPrice; // EntryDirtyPrice(期初全价不含费) + public decimal TradingAmountAvg; // 用户可改的期末标的价格(界面×multiplier形态) + public decimal CloseQty; // 平仓数量 + public decimal CloseNotionalValue;// 平仓名义本金(income 用) + public int PayDirection; // 1=收取,-1=支付 + public int PositionType; // 1=多头,2=空头 + public string TradingFee; // 交易费用(前端是字符串) + public string TradingFeePending; // 待结算费用 + public string DividendIn; // 分红 + public List InterestLegs = new(); + public List MarginLegs = new(); + } + + /// 利息腿/预付金腿输入 + public class LegInput + { + public decimal InterestClosePnL; // 利息腿平仓盈亏(已含方向) + } + + /// 前端计算输出模型(前端算出的衍生字段) + public class UnwindResult + { + public decimal MarkClosePnl; + public decimal FloatPnlSum; + public decimal SwapRealizedPnL; + public decimal SwapCloseAmount; + public decimal SwapMarginRebatePnl; + public decimal TradingAmountFeeAvg; + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapDealService.cs b/YLErpDAL/Modules/SwapModule/SwapDealService.cs index 0fd4892f..ef772d90 100644 --- a/YLErpDAL/Modules/SwapModule/SwapDealService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapDealService.cs @@ -5,6 +5,7 @@ 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; @@ -15,6 +16,7 @@ namespace YLErp.Modules.SwapModule { public class SwapDealService : SwapTradeBaseService { + private static readonly IYcLogger Logger = LogFactory.GetLogger(nameof(SwapDealService)); protected virtual bool TryGetFloatRate(DateTime valueDate, string underlyingCode, out double rate) { return EodPriceQueryService.TryGetPrice(valueDate, underlyingCode, out rate); @@ -125,6 +127,85 @@ namespace YLErp.Modules.SwapModule { } + + #region 前端盈亏只读校验(不阻断交易) + + /// + /// 用 FrontendCalcReference 公式重算盈亏,与前端传来的 unwindData 比对, + /// 差异 > 0.01 记 Error 日志。整体 try/catch 吞异常——校验自身错误绝不阻断交易。 + /// + /// 目的:前端保持快速反馈(用户改输入立即算),后端不替代前端,仅做合理性兜底, + /// 为将来公式统一积累"前后端差异"数据。 + /// + /// 前端算好传入的结算数据 + /// true=结息页(income公式),false=平仓页(unwind公式) + private void ValidateFrontendPnL(UnwindData unwindData, bool isIncome) + { + try + { + // 取浮动腿(有 UnderlyingCode 的),与前端 initDeal 取法一致 + var floatLeg = unwindData.FlowEvents?.FirstOrDefault(x => !string.IsNullOrEmpty(x.UnderlyingCode)); + // PosiGrossPrice 是 [NotMapped],前端可能没传;为空/0 时跳过(避免误报) + if (floatLeg == null || floatLeg.PosiGrossPrice == 0) + { + return; + } + + // 用 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, + 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) + { + // 校验自身错误绝不阻断交易 + Logger.Error($"[互换盈亏校验异常] tradeId={unwindData.SwapTradeId} isIncome={isIncome}", ex); + } + } + + 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 + /// /// 平仓初始化 /// @@ -1093,6 +1174,7 @@ namespace YLErp.Modules.SwapModule throw new ServiceException("未找到交易信息"); } //CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制 + ValidateFrontendPnL(unwindData, isIncome: false); // 只读校验告警,不阻断交易 bool cofirm = false; ExecuteInTransaction(() => { @@ -1568,6 +1650,7 @@ namespace YLErp.Modules.SwapModule throw new ServiceException("未找到交易信息"); } //CheckLastEod(unwindData.ValueDate, td.StartDate.Value, unwindData.SwapTradeId); //去掉平仓收盘限制 + ValidateFrontendPnL(unwindData, isIncome: true); // 只读校验告警,不阻断交易 ExecuteInTransaction(() => { int clientCashId = AddClientCash(td, Convert.ToDouble(-unwindData.SwapRealizedPnL), ClientCashInCashOut.系统操作_互换, unwindData.ValueDate);