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:
@@ -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-261(calcFloatClosePnl + 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=收取)→+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
|
||||
};
|
||||
}
|
||||
|
||||
/// <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 用 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<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 个测试场景(含用户可变输入分支)
|
||||
|
||||
@@ -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-261(calcFloatClosePnl + 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=收取)→+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
|
||||
};
|
||||
}
|
||||
|
||||
/// <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 用 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);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
Reference in New Issue
Block a user