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