chore(swap)+test: 删分红EOD历史死代码 + 补计算过程常驻日志(零行为语义变更);契约修复转为暂缓

死代码删除(SwapEodPositionService.UpdateEodPosition,论证+实证链完整):
- todayConsumedDividend/originNotional/totalPayment/totalInterest 自 0910969e(7/2 改递推式)
  起计算结果从未被消费,仅残留全历史 CalcBondPayment 只读查询+日志副作用,且构成
  脏数据(OriginalStockEqvNotional=null/PosiNetPrice=0)下 EOD 崩溃点(除零,实测复现)
- 配套边界测试 DividendEodNoDoubleCountTest.脏数据边界_*:删除前红(崩溃)→删除后绿
  (不抛异常+四输出字段与正常数据逐字段一致);stash 基线对比确认离线套件无其他差异
- 止血预案:git revert 本提交即回到已验证态

计算过程常驻日志(快速定位,纯增):
- [EOD平仓后收盘结息]:口径/autoSwap/平仓前/剩余/平掉额——排障第一入口
- [分红-EOD计提Copy/Update]:窗口/qty/税率/当日新计/账面前值→后值——与
  [分红-登记日口径]窗口命中日志构成分红计算全链
- [利息-全平专属分支]:全平分支触发标记,兼作契约修复落地后的验证哨兵

契约修复(EOD传真实比例)转为暂缓:影响快照种子,黄金回放验收门因96库网络未恢复
无法执行,按风险优先级撤回行为变更;方案+回归网(同形状等价oracle)+落地哨兵已
全部就绪并留档于裁决文档,DB恢复后可直接落地。
This commit is contained in:
hjhan
2026-08-16 15:47:09 +08:00
parent c9f44d4fde
commit 9efaa1b5cc
5 changed files with 132 additions and 43 deletions
@@ -106,9 +106,11 @@ namespace YLErp.Modules.SwapModule
/// <summary>
/// 【EOD 当日有平仓后的收盘结息】显式入口——原 SaveAutoEodWithCloseInterestPosition 直调
/// CalcSwapInterests(settment:false) 的具名封装(2026-08 显式化重构)。
/// 语义契约见 InterestCalcRequest.EodPostCloseSettle 工厂注释(平仓后剩余 + 实际平掉额 + 恒1全额结息,
/// 触发 GetInterests 内 mode2/mode9 本金修正)。计息走 CalcUnwindInterest 全区间重放。
/// 默认实现仍经 CalcSwapInterests 转发,保持既有测试替身对该虚接缝的拦截不变。
/// (契约修复§六暂缓中:落地时 autoSwap=false 分支改 Intraday 形状,见裁决文档与调用点注释。)
/// </summary>
protected virtual List<swap_flow_event> CalcEodPostCloseSettleInterests(InterestCalcRequest req)
=> CalcSwapInterests(req.Td, req.TradeExtend, req.ValueDate, req.UnwindDate, req.EodPositions, req.Positions,
@@ -1321,6 +1323,15 @@ namespace YLErp.Modules.SwapModule
var calcLast = tradeExtend?.InterestCalcMode?.EndsWith("1") ?? true;
// 显式入口:平仓后剩余本金 + 实际平掉额 + 恒1全额结息(语义见 InterestCalcRequest.EodPostCloseSettle)。
// 该组合触发 GetInterests 内共享计息器的模式2/9本金修正(见其"根因位置"注释,勿删)。
// ⚠️ 契约修复暂缓中(2026-08-16 撤回):按裁决文档§六,autoSwap=false 本应改传 Intraday 形状
// (平仓前剩余+真实比例),但该变更影响快照种子(TdInterestPrincipal等),黄金回放验收门未过前不落地;
// 落地时见 项目文档/双入口口径裁决-复利mode2部分平仓-20260816.md §六 与已就绪的回归网
//GetInterestsEntrySemanticsTest.复利_mode2_部分平仓_双入口契约口径一致)。
// 口径选择常驻记录(快速定位第一入口):出问题先看这行确认当日本次事件的金额输入,再顺着
// SwapCalcTrace 分段过程日志追计算;autoSwap=观察日结现路径。
Log.Info($"[EOD平仓后收盘结息] tradeId={td.id} valueDate={valueDate:yyyy-MM-dd} autoSwap={autoSwap} " +
$"口径=恒1全额结息(历史行为,契约修复暂缓) " +
$"oriPosi(平仓前)={oriPosiNotionalValue} posi(剩余)={posiNotionalValue} close(平掉)={closeNational}");
var interests = CalcEodPostCloseSettleInterests(InterestCalcRequest.EodPostCloseSettle(
td, td.trade_extend, valueDate, valueDate, preEodPositions, positions,
posiNotionalValue, closeNational,
@@ -1762,6 +1773,11 @@ namespace YLErp.Modules.SwapModule
curretEod.TdPosiDividend = DividendCalc.AfterTax(payment, tax);
}
curretEod.PosiDividendSum = eod.PosiQuantity > 0 ? Math.Round(eod.PosiDividendSum + curretEod.TdPosiDividend, 2) : 0;
// 分红递推过程常驻记录(快速定位):窗口/数量/税率/当日新计/累计前后值——
// 配合 BondPaymentService 的[分红-登记日口径]窗口命中日志,构成"命中哪些登记日→算出多少→账滚到多少"全链
Log.Info($"[分红-EOD计提Copy] tradeId={td.id} posiId={eod.PositionId} valueDate={valueDate:yyyy-MM-dd} " +
$"window=({eod.ValueDate:yyyy-MM-dd},{valueDate:yyyy-MM-dd}] qty={curretEod.PosiQuantity} tax={tax} " +
$"TdPosiDividend={curretEod.TdPosiDividend} PosiDividendSum {eod.PosiDividendSum}->{curretEod.PosiDividendSum}");
curretEod.PosiQuantity = eod.PosiQuantity;
if (curretEod.PosiStatus == 1)
{
@@ -1834,11 +1850,11 @@ namespace YLErp.Modules.SwapModule
int shortRatio = DirectionRatio.LongShort(eod.PositionType);
int directionRatio = DirectionRatio.ReceivePay(eod.PosiDirection);
var price = GetSwapValuationPrice(eod.UnderlyingCode, dealDate, out decimal vobp);
var todayConsumedDividend = CalcConsumedDividend(curretEod, unwindEvents);
var originNotional = (decimal)td.OriginalStockEqvNotional / swapPosition.PosiNetPrice;
decimal totalPayment = CalcBondPayment(curretEod.UnderlyingCode, td.StartDate.Value, valueDate, (decimal)originNotional, shortRatio, directionRatio);
// 历史遗留死代码已删(2026-08-16,论证+边界测试见 DividendEodNoDoubleCountTest.脏数据边界_*):
// todayConsumedDividend / originNotional / totalPayment / totalInterest 自 0910969e(2026-07-02
// 改递推式) 起计算结果从未被消费,仅残留一次全历史 CalcBondPayment 只读查询+日志副作用,
// 且构成脏数据(OriginalStockEqvNotional=null/PosiNetPrice=0)下的 EOD 崩溃点。回退=git revert 本提交。
decimal tax = um.ValueAddedTax ?? 0;
decimal totalInterest = DividendCalc.AfterTaxRaw(totalPayment, tax);
SetPriceInfoByFlowEvent(eod, curretEod, unwindEvents, swapPosition);
curretEod.dv01 = Dv01Helper.CalcDv01(eod.UnderlyingCode, curretEod.PosiQuantity, eod.PosiDirection, eod.PositionType, vobp);
curretEod.UnderlyingPrice = price;
@@ -1871,6 +1887,11 @@ namespace YLErp.Modules.SwapModule
{
curretEod.PosiDividendSum = 0;
}
// 分红递推过程常驻记录(快速定位):当日事件路径含实现扣减(前日+新计-当日实现)
Log.Info($"[分红-EOD计提Update] tradeId={td.id} posiId={eod.PositionId} valueDate={valueDate:yyyy-MM-dd} " +
$"window=({eod.ValueDate:yyyy-MM-dd},{valueDate:yyyy-MM-dd}] qty={curretEod.PosiQuantity} tax={tax} " +
$"TdPosiDividend={curretEod.TdPosiDividend} TdCloseDividend={curretEod.TdCloseDividend} " +
$"PosiDividendSum {eod.PosiDividendSum}->{curretEod.PosiDividendSum}");
EodPnlCalculator.SetFloatingRealizedPnl(curretEod);
curretEod.SwapPositionValue -= curretEod.TdCloseDividend;
@@ -1893,20 +1914,6 @@ namespace YLErp.Modules.SwapModule
return curretEod;
}
private decimal CalcConsumedDividend(eod_swap_position curretEod, List<swap_flow_event> events)
{
decimal consumedDividend = 0;
List<int> swapEventTypes = new List<int>() { (int)SwapEventTypeEnum., (int)SwapEventTypeEnum. };
//这里要剔除掉平仓产生的分红
consumedDividend = events
.Where(x => x.SwapTradeId == curretEod.SwapTradeId
&& swapEventTypes.Contains(x.EventType)
&& x.DataState == (int)SwapFlowDateStateEnum.)
.Sum(s => s.DividendIn);
return consumedDividend;
}
/// <summary>
/// 根据开平仓事件算价格及后付费用
/// </summary>