feat(swap): 后端只读校验告警-搬FrontendCalcReference+SwapIncome/SwapUnwind插校验

前端保持快速反馈(用户改输入立即算),后端不替代前端,仅做合理性兜底校验。

1. 搬FrontendCalcReference到YLErpDAL/Helpers(测试与生产共用同一份公式,避免分叉)
   - 从FrontendCalcCharacterizationTest提取,改public,8个FC测试搬迁后全绿验证一致

2. SwapDealService新增ValidateFrontendPnL只读校验:
   - 从unwindData.FlowEvents取浮动腿,PosiGrossPrice==0时跳过(避免误报)
   - 调FrontendCalcReference重算,与前端传值逐字段比对
   - 差异>0.01记Logger.Error(带输入快照便于排查)
   - 整体try/catch吞异常,校验自身错误绝不阻断交易
   - SwapUnwind/SwapIncome各插一处(FindTrade后、ExecuteInTransaction前)

不碰NLog配置(告警进Console,生产由运维采集)。SwapModule 157测试全绿。
This commit is contained in:
hjhan
2026-07-03 09:04:57 +08:00
parent 129e4bed0d
commit e4a92c93ee
3 changed files with 271 additions and 172 deletions
@@ -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# 忠实重写(参考实现,非生产代码)
// ================================================================
/// <summary>
/// 前端公式参考实现。输入=前端可见的原始字段,输出=前端算出的衍生字段。
/// 注意:otcformat.trading.StockEqvNotional 的精度按 MoneyRound=2 模拟。
/// </summary>
private static class FrontendCalcReference
{
/// <summary>模拟 otcformat.trading.StockEqvNotional(金额类,2位小数)</summary>
private static decimal StockEqvNotional(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero);
/// <summary>
/// 计算平仓页(unwind)的盯市盈亏与汇总。
/// 对应 unwindSwapTrade.js:196-261calcFloatClosePnl + calcCloseAmount)。
/// </summary>
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=收取)→+1PositionType(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"));
// calcCloseAmountSwapRealizedPnL/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:-1calcCloseAmount 内重新定义)
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
};
}
/// <summary>
/// 计算结息页(income)的盯市盈亏与汇总。
/// 对应 incomeSwapTrade.js:128-178。
/// 差异:用 CloseNotionalValue(非 CloseQty)作量纲,无 longRatio,无 Math.round/10000。
/// </summary>
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 用 floatRatioPayDirection),与 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<LegInput> InterestLegs = new();
public List<LegInput> 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 个测试场景(含用户可变输入分支)
+185
View File
@@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
namespace YLErp.Helpers
{
/// <summary>
/// 前端互换计算公式的后端参考实现(忠实重写 unwindSwapTrade.js / incomeSwapTrade.js)。
/// ============================================================================
/// 用途:
/// 1. 特征化测试的金标准(FrontendCalcCharacterizationTest)—— 冻结前端行为
/// 2. 后端只读校验(SwapDealService.ValidateFrontendPnL)—— 重算盈亏与前端传值比对
///
/// 注:前端保持快速反馈(用户改输入立即算),本类不替代前端,仅作后端校验兜底。
/// 命名规范(见《互换价格字段命名规范决策文档》):
/// PosiGrossPrice 现状名,实为"期初全价不含费",规范名 EntryDirtyPrice
/// TradingAmountAvg 现状名,实为"期末全价不含费",规范名 ExitDirtyPrice
/// ============================================================================
/// </summary>
public static class FrontendCalcReference
{
/// <summary>模拟 otcformat.trading.StockEqvNotional(金额类,2位小数)</summary>
private static decimal StockEqvNotional(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero);
/// <summary>
/// 计算平仓页(unwind)的盯市盈亏与汇总。
/// 对应 unwindSwapTrade.js:196-261calcFloatClosePnl + calcCloseAmount)。
/// </summary>
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=收取)→+1PositionType(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"));
// calcCloseAmountSwapRealizedPnL/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:-1calcCloseAmount 内重新定义)
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
};
}
/// <summary>
/// 计算结息页(income)的盯市盈亏与汇总。
/// 对应 incomeSwapTrade.js:128-178。
/// 差异:用 CloseNotionalValue(非 CloseQty)作量纲,无 longRatio,无 Math.round/10000。
/// </summary>
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 用 floatRatioPayDirection),与 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);
}
/// <summary>前端计算输入模型(对应前端可见的原始字段)</summary>
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<LegInput> InterestLegs = new();
public List<LegInput> MarginLegs = new();
}
/// <summary>利息腿/预付金腿输入</summary>
public class LegInput
{
public decimal InterestClosePnL; // 利息腿平仓盈亏(已含方向)
}
/// <summary>前端计算输出模型(前端算出的衍生字段)</summary>
public class UnwindResult
{
public decimal MarkClosePnl;
public decimal FloatPnlSum;
public decimal SwapRealizedPnL;
public decimal SwapCloseAmount;
public decimal SwapMarginRebatePnl;
public decimal TradingAmountFeeAvg;
}
}
@@ -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
/// <summary>
/// 用 FrontendCalcReference 公式重算盈亏,与前端传来的 unwindData 比对,
/// 差异 > 0.01 记 Error 日志。整体 try/catch 吞异常——校验自身错误绝不阻断交易。
///
/// 目的:前端保持快速反馈(用户改输入立即算),后端不替代前端,仅做合理性兜底,
/// 为将来公式统一积累"前后端差异"数据。
/// </summary>
/// <param name="unwindData">前端算好传入的结算数据</param>
/// <param name="isIncome">true=结息页(income公式)false=平仓页(unwind公式)</param>
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
/// <summary>
/// 平仓初始化
/// </summary>
@@ -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);