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:
@@ -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