Files
zszq-trs/项目文档/互换部分平仓后利息端预付金默认盈亏偏大问题分析.md
T
hjhan cd03fc53fb 互换两个独立bug分析文档+诊断测试
Bug①利息默认值偏大: 互换结清后eod的InterestProfitSum(待实现)没归零
(=RealizedInterest=77.26), 导致后续平仓默认值=77.26(本该0)+增量→偏大。
6-29收盘后三天对比坐实: 6-30默认83.70, 7-1默认90.14, 均含已实现的77.26。
根因 eod公式cs:829 对互换场景扣减失效(TdInterestIncome≈TdCloseInterest抵消)。

Bug②收益结算审核后状态卡死: ApproveSwapTrade(cs:1525)漏ExerciseDate判断,
到期互换审核后状态退回'确认成交'(CloseMethod默认0≠全部平仓1),
导致EodCheckMaturityTrade一直阻止收盘。引入点083848fb(2025-05-28)。

- 新增利息偏大分析文档(根因/修复方案A扣RealizedInterest/B eod归零)
- 新增审核状态卡死分析文档(根因/修复补ExerciseDate判断)
- 新增SwapPartialUnwindInterestDefaultTest(Step0探查/Step0b诊断/Step0d三天对比)
2026-07-01 09:57:25 +08:00

15 KiB
Raw Blame History

互换部分平仓后:利息端/预付金"平仓盈亏默认值"偏大问题分析

本文分析收益互换"昨天收益结算(互换)→ 收盘 → 今天平仓"场景下,预付金平仓盈亏和利息端平仓盈亏的默认值计算偏大的问题:根因、涉及的文件、如何确认、修复方案与自测方法。 成文于 2026-07-01。


一、问题概述

复现场景

  1. 第一天:对互换交易做收益结算(互换) → 收盘
  2. 第二天:再次进入平仓

现象:平仓页的"预付金平仓盈亏"和"利息端平仓盈亏"默认值偏大——包含了之前已经通过互换/收益结算平掉的那部分利息,没有扣除。

用户怀疑:"这部分是不是没有减去之前已经平掉的部分?"——怀疑成立,下文逐层证明。


二、数据流:这两个默认值从哪来

关键结论:默认值是后端算的,前端只展示

前端 unwindSwapTrade.js 不计算这两个默认值,只是把后端返回的 InterestClosePnL 原样填到输入框:

界面字段 绑定字段 数据来源
预付金平仓盈亏 marginList[].InterestClosePnL 后端接口返回
利息端平仓盈亏 interestList[].InterestClosePnL 后端接口返回
浮动端平仓盈亏 floatPosition.FloatPnlSum(前端现算) 前端 calcFloatClosePnl

前端调接口(unwindSwapTrade.js:264-265):

{ valueDate, unwindDate, tradeId, closePercent, eventType: 2 }

后端接口链路

POST /swaptrade2/GetUnwindInterestList  (SwapTrade2Controller.cs:287)
  └─ SwapDealService.GetUnwindInterests        (SwapDealService.cs:302)
       ├─ 取 closeList = 当天已完成的平仓/互换事件  (cs:336)
       └─ GetInterests                          (cs:358)
            ├─ CalcNotionalByMode               (cs:453)  ← 按利息模式算计息基数
            ├─ CalcUnwindInterest → InitSwapDealInterest (cs:595/626)
            │    └─ CalcDailySimpleInterest / CalcDailyCompoundInterest (cs:771/705)
            │         ↑ 这里全量重算利息,不扣历史已平部分
            └─ 同日去重:closeList 扣减            (cs:418-446)  ← 只扣当天,跨天漏

三、根因:互换结清后 eod 的 InterestProfitSum(待实现)没扣减已实现部分

6-29 收盘后用交易 1889GLMS-20260616-0004)三天对比实测坐实。用户的判断"从开头算了"正确。

3.1 实测坐实(6-29 收盘后,6-29/6-30/7-1 三天对比)

利息腿 PositionId=34695(标的期初全价),29号收益结算(互换) + 29号收盘后:

日期         平仓默认值    eod待实现(IPS)   eod已实现(RI)   说明
6-29(互换前)  0.0003      77.26(6-28eod)   0.00           当天看=0,正确
6-30         83.6986     77.26(6-29eod)   77.26          偏大!
7-1          90.1370     77.26(6-29eod)   77.26          更偏大!

核心铁证6-29 收盘后,InterestProfitSum(待实现)= 77.26RealizedInterest(已实现)= 77.26两个相等——互换已经把全部利息实现了(77.26),但"待实现"没归零,还是 77.26。

所以后续平仓默认值 = 待实现(77.26) + 每日增量(6.44/天)。6-30 = 77.26+6.44=83.707-1 = 77.26+12.88=90.14。偏大的量正是那个本该归零却没归零的 77.26

3.2 根因机制:eod 扣减公式对"互换"场景失效

SwapEodPositionService.cs:829(收盘更新待实现利息):

InterestIncomeSum = pre.InterestIncomeSum + TdInterestIncome - TdCloseInterest;
//                  = 77.26(6-28)      + 77.26(当日新计)  - 77.26(当日互换实现)
//                  = 77.26  ← 抵消了,没扣减!

为什么抵消:互换操作当日会重新计息(TdInterestIncome = 77.26,相当于把到期前的利息算了一遍),同时互换实现(TdCloseInterest = 77.26)。两个相等,相减为 0,所以 InterestIncomeSum 维持在 77.26 不变。

正常平仓不会这样:平仓时 TdInterestIncome 是"昨日到今日的增量"(小),TdCloseInterest 是"平仓实现的全部"(大),两者不等 → 能扣减。但互换(收益结算)的计息逻辑把全程利息都算了一遍(TdInterestIncome ≈ TdCloseInterest),导致扣减失效。

3.3 CalcDailySimpleInterest 读这个"没扣减的待实现"作为起点

SwapDealService.cs:771CalcDailySimpleInterest):

decimal interestProfitSum = preEodPosition.InterestProfitSum;  // :774 读 77.26(本该归零)
decimal interest = interestProfitSum * closePercent;           // :776 起点 = 77.26
for (int i = 0; i <= calcDays; i++)                            // :784 补算昨日之后增量
{
    if (accrueDate > preEodPosition.ValueDate)                 // :789
        interest += interest1;                                  // :823 每天+6.44
}
// 结果:6-30 = 77.26 + 6.44 = 83.70(偏大,含已实现的77.26)

GetInterests 的同日去重(cs:435-443)只覆盖"当天"的已完成事件,29号互换 EventDate=6-29 ≠ 平仓日 6-30 → 跨天去重不生效

3.4 根因总结

互换结清后正确的状态应该是:InterestProfitSum(待实现) = 0(全部已实现)
但实际:InterestProfitSum = 77.26= RealizedInterest,没归零)
         ↓ 原因
eod 公式 cs:829IPS = pre.IPS + TdInterestIncome - TdCloseInterest
                   互换时 TdInterestIncome ≈ TdCloseInterest → 抵消 → IPS 不变
         ↓ 后果
后续平仓默认值 = IPS(77.26,本该0) + 增量 → 偏大 77.26

四、涉及哪些文件

文件:行号 作用/问题
后端·默认值入口 SwapTrade2Controller.cs:287 GetUnwindInterestList 接口
后端·默认值计算 SwapDealService.cs:302GetUnwindInterests/ :358GetInterests 主计算链路
后端·单利重算 SwapDealService.cs:771CalcDailySimpleInterest 起点=InterestProfitSum×closePercent,补算昨日之后增量
后端·复利重算 SwapDealService.cs:705CalcDailyCompoundInterest 从头算,注释"只能用要平仓的名义本金从头开始算"
后端·方向系数 SwapDealService.cs:673interestRatio 收取=1/支付=-1实测符号相反疑似此处
后端·同日去重(可疑A SwapDealService.cs:336closeList/ :435(算尾跳过) 跨天不扣
后端·固定值本金(可疑B SwapDealService.cs:461-463 固定值强制100%
后端·eod扣减待实现 SwapEodPositionService.cs:829 InterestIncomeSum 扣 TdCloseInterest有扣减
前端·展示(非源) unwindSwapTrade.js:262-275 仅展示后端值

前端不是 bug 源:前端只把后端 InterestClosePnL 填进输入框,无额外计算。


五、引入时间(git blame

代码点 提交 时间 说明
interestProfitSum = preEodPosition.InterestProfitSumcs:774 f9d8a256 2024-05-09 山证基线
interest = interestProfitSum * closePercentcs:776 6708878d 2026-04-13 hjhan 改起点为×closePercent
同日去重(cs:435-443 535497c68 2026-06-04 吴方海 加同日去重补救,仅覆盖当天
eod 扣减 TdCloseInterestcs:829 基线 2024-05 eod 层有扣减已实现

结论:利息计算链路是基线设计,经历过多次局部修补(6708878d 改起点、535497c68 加同日去重)。具体哪个改动引入了用户反馈的偏差,需用干净样本定位


六、如何确认 bug(已坐实)

6.1 实测坐实(交易 18896-29 收盘后三天对比,Step0d)

用真实的 GetUnwindInterests 分别测 6-29/6-30/7-1 三天,结果(PositionId=34695 标的期初全价):

日期         平仓默认值    eod待实现(IPS)   eod已实现(RI)   说明
6-29(互换前)  0.0003      77.26(6-28eod)   0.00           当天看=0,正确
6-30         83.6986     77.26(6-29eod)   77.26          偏大!含已实现的77.26
7-1          90.1370     77.26(6-29eod)   77.26          更偏大!

核心铁证6-29 收盘后 InterestProfitSum(待实现)= 77.26 = RealizedInterest(已实现)= 77.26。互换已全部实现,但待实现没归零 → 后续默认值 = 77.26(本该0) + 增量 → 偏大。

6.2 录制测试

SwapPartialUnwindInterestDefaultTest.Step0d_DiagnoseTrade1889_FullTimeline

  • 展示 swap_event 全历史(甄别回退)
  • 展示 eod 按日序列(InterestProfitSum/RealizedInterest 演变)
  • 调用真实 GetUnwindInterests 拿默认值
  • 对比"默认值 vs eod待实现"判定是否重复计入

七、修复方案

根因已精确定位:互换结清后 InterestProfitSum 没归零(= RealizedInterest),导致后续平仓默认值偏大。

方案 A(推荐·最小改动):平仓默认值起点扣除 RealizedInterest

CalcDailySimpleInterestcs:774/776)读 InterestProfitSum 作为起点。既然 InterestProfitSum 没扣已实现,就在这里扣:

// SwapDealService.cs:774-776  改前
decimal interestProfitSum = preEodPosition.InterestProfitSum;
var TdInterestPrincipal = preEodPosition.TdInterestPrincipal;
decimal interest = interestProfitSum * closePercent;

// 改后:扣除已通过互换/平仓实现的部分
decimal interestProfitSum = preEodPosition.InterestProfitSum - preEodPosition.RealizedInterest;
var TdInterestPrincipal = preEodPosition.TdInterestPrincipal;
decimal interest = interestProfitSum * closePercent;

原理InterestProfitSum(待实现累计)- RealizedInterest(已实现累计)= 真正未实现的利息。互换结清后 IPS=77.26、RI=77.26,相减=0,起点正确归零。后续只加增量。

验证(用 1889 数据):

  • 6-30 默认值 = (77.26 - 77.26) + 6.44(一天增量) = 6.44 (而非偏大的 83.70
  • 7-1 默认值 = (77.26 - 77.26) + 12.88 = 12.88

优点:改动 1 行,利用现有字段,覆盖单利路径。 风险:需确认所有场景下 RealizedInterest 都正确累加了已实现利息。对复利路径(CalcDailyCompoundInterest cs:705)需同步检查。

方案 B(治本):修正 eod 扣减公式,让互换结清后 InterestProfitSum 正确归零

根因在 SwapEodPositionService.cs:829——互换时 TdInterestIncome ≈ TdCloseInterest 导致扣减抵消。需在互换(EventType=互换/自动互换)的收盘路径里特殊处理:

// SwapEodPositionService.cs:829 附近
// 互换结清时,待实现利息应直接归零(已全部实现),不能靠 TdInterestIncome - TdCloseInterest 抵消
if (isSwap && closePercent == 1)   // 全量互换结清
{
    newEodPayPosition.InterestIncomeSum = 0;  // 待实现归零
}
else
{
    newEodPayPosition.InterestIncomeSum = eodPayPosition.InterestIncomeSum
                                        + newEodPayPosition.TdInterestIncome
                                        - newEodPayPosition.TdCloseInterest;
}

优点:从源头修正,所有读 InterestProfitSum 的地方都受益。 缺点:改动 eod 核心逻辑,风险较大,需全面回归。

方案 C(补充):closeList 扩展到历史

GetUnwindInterests 的 closeList(cs:336)只查当天,跨天互换事件漏掉。可扩展到历史:

// cs:336 改 UnwindDate == unwindDate 为 UnwindDate <= unwindDate

但 cs:435 的 !calcLast 限制仍会导致算尾配置下不扣。作为方案 A/B 的补充。

方案对比

方案 改动量 治本程度 风险
A(扣 RealizedInterest 极小(1行) 修默认值 低,需验证字段口径
Beod 公式归零) 修源头 较高,改 eod 核心
CcloseList 扩历史) 修去重 中,算尾仍漏

建议:优先方案 A 止血(最小改动,直接验证),方案 B 作为后续治本。


八、录制 golden source

已坐实(Step0d,交易 1889,6-29 收盘后三天对比)

Step0d_DiagnoseTrade1889_FullTimeline 已调用真实服务坐实 bug

  • 6-29 收盘后 InterestProfitSum(待实现)=77.26 = RealizedInterest(已实现)=77.26没归零
  • 6-30 默认值 83.70 = 77.26(本该归零的待实现) + 6.44(一天增量) → 偏大 77.26
  • 7-1 默认值 90.14 = 77.26 + 12.88(两天增量) → 偏大 77.26

录制 golden JSON

当前 Step0d 是诊断输出(Console),尚未序列化 golden 文件。建议后续:

  1. 把 1889 的快照(swap_event + eod + 默认值结果)序列化为 SwapPartialUnwindInterest/partial_unwind_trade_1889.json
  2. 红灯断言:Assert(默认值 > eod待实现 × 合理倍数),修复后反转

注意:1889 有大量回退

1889 有 52 条 swap_event48 条 Invalid=True 的回退历史),测试环境反复回退复用。录制 golden 时需注意甄别有效事件(Invalid=False)。Step0d 已处理:只展示 Invalid=False 的 4 条有效事件。

与前两个 golden 的关系

三份 golden 构成"互换损益计算"回归基线:

  • SwapDividend/(分红重复计算)
  • SwapReEodDeleteCash/(重收盘误删资金)
  • SwapPartialUnwindInterest/(本次) Step0d 已坐实,golden 待序列化

九、团队决策问题

  1. 业务口径确认:部分平仓/互换后再次平仓,利息端盈亏默认值应该是"剩余持仓的应计利息(扣已实现)"?当前行为是"从头算(含已实现)",业务上预期哪个?
  2. 方案选择:方案 A(closeList 扩历史,推荐)/ 方案 B(扣 RealizedInterest)?
  3. 回退复用的影响:1889 被反复回退,eod 只到 6-22(29号收盘数据疑似被回退清掉)。需确认:回退是否应该清理 eod?还是回退后 eod 应保留?
  4. 是否补单测:建议把 Step0d 的坐实逻辑固化为 golden + 红灯断言,防止复发。

附录:关键代码位置索引

文件:行号
默认值接口入口 SwapTrade2Controller.cs:287GetUnwindInterestList
默认值计算主方法 SwapDealService.cs:302GetUnwindInterests/ :358GetInterests
单利重算 SwapDealService.cs:771CalcDailySimpleInterest
复利重算 SwapDealService.cs:705CalcDailyCompoundInterest
InterestProfitSum 读取(起点) SwapDealService.cs:774 / :645InitSwapDealInterest
方向系数(实测符号相反疑似) SwapDealService.cs:673interestRatio
同日去重(可疑A SwapDealService.cs:336closeList/ :435-443(扣减)
固定值本金(可疑B SwapDealService.cs:461-463CalcNotionalByMode
eod 扣减待实现(有扣减) SwapEodPositionService.cs:829InterestIncomeSum 扣 TdCloseInterest
平仓后回写持仓 SwapDealService.cs:1664InterestAmount 累加)/ :1668(预付金扣减)
前端展示(非源) unwindSwapTrade.js:262-275 / SwapUnwind.cshtml:113,146
数据模型·待实现 EodSwapPosition.cs:282InterestProfitSum/ :270InterestIncomeSum
数据模型·已实现 EodSwapPosition.cs:355RealizedInterest/ :368RealizedPnl