# 互换部分平仓后:利息端/预付金"平仓盈亏默认值"偏大问题分析 > 本文分析收益互换"昨天收益结算(互换)→ 收盘 → 今天平仓"场景下,**预付金平仓盈亏和利息端平仓盈亏的默认值计算偏大**的问题:根因、涉及的文件、如何确认、修复方案与自测方法。 > 成文于 2026-07-01。 --- ## 一、问题概述 **复现场景**: 1. 第一天:对互换交易做**收益结算(互换)** → 收盘 2. 第二天:再次进入**平仓**页 **现象**:平仓页的"预付金平仓盈亏"和"利息端平仓盈亏"**默认值偏大**——包含了之前已经通过互换/收益结算平掉的那部分利息,没有扣除。 **用户怀疑**:"这部分是不是没有减去之前已经平掉的部分?"——**怀疑成立**,下文逐层证明。 --- ## 二、数据流:这两个默认值从哪来 ### 关键结论:默认值是后端算的,前端只展示 前端 `unwindSwapTrade.js` **不计算**这两个默认值,只是把后端返回的 `InterestClosePnL` 原样填到输入框: | 界面字段 | 绑定字段 | 数据来源 | |---------|---------|---------| | 预付金平仓盈亏 | `marginList[].InterestClosePnL` | 后端接口返回 | | 利息端平仓盈亏 | `interestList[].InterestClosePnL` | 后端接口返回 | | 浮动端平仓盈亏 | `floatPosition.FloatPnlSum`(前端现算)| 前端 `calcFloatClosePnl` | 前端调接口(`unwindSwapTrade.js:264-265`): ```js { 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 收盘后用交易 1889(GLMS-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.26**,`RealizedInterest`(已实现)= **77.26**。**两个相等——互换已经把全部利息实现了(77.26),但"待实现"没归零**,还是 77.26。 所以后续平仓默认值 = 待实现(77.26) + 每日增量(6.44/天)。6-30 = 77.26+6.44=83.70,7-1 = 77.26+12.88=90.14。**偏大的量正是那个本该归零却没归零的 77.26**。 ### 3.2 根因机制:eod 扣减公式对"互换"场景失效 `SwapEodPositionService.cs:829`(收盘更新待实现利息): ```csharp 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:771`(CalcDailySimpleInterest): ```csharp 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:829:IPS = pre.IPS + TdInterestIncome - TdCloseInterest 互换时 TdInterestIncome ≈ TdCloseInterest → 抵消 → IPS 不变 ↓ 后果 后续平仓默认值 = IPS(77.26,本该0) + 增量 → 偏大 77.26 ``` --- ## 四、涉及哪些文件 | 层 | 文件:行号 | 作用/问题 | |----|----------|----------| | 后端·默认值入口 | `SwapTrade2Controller.cs:287` | GetUnwindInterestList 接口 | | 后端·默认值计算 | `SwapDealService.cs:302`(GetUnwindInterests)/ `:358`(GetInterests)| 主计算链路 | | 后端·单利重算 | `SwapDealService.cs:771`(CalcDailySimpleInterest)| 起点=InterestProfitSum×closePercent,补算昨日之后增量 | | 后端·复利重算 | `SwapDealService.cs:705`(CalcDailyCompoundInterest)| 从头算,注释"只能用要平仓的名义本金从头开始算" | | 后端·方向系数 | `SwapDealService.cs:673`(interestRatio)| 收取=1/支付=-1,**实测符号相反疑似此处** | | 后端·同日去重(可疑A)| `SwapDealService.cs:336`(closeList)/ `:435`(算尾跳过)| 跨天不扣 | | 后端·固定值本金(可疑B)| `SwapDealService.cs:461-463` | 固定值强制100% | | 后端·eod扣减待实现 | `SwapEodPositionService.cs:829` | InterestIncomeSum 扣 TdCloseInterest(**有扣减**)| | 前端·展示(非源)| `unwindSwapTrade.js:262-275` | 仅展示后端值 | > **前端不是 bug 源**:前端只把后端 `InterestClosePnL` 填进输入框,无额外计算。 --- ## 五、引入时间(git blame) | 代码点 | 提交 | 时间 | 说明 | |--------|------|------|------| | `interestProfitSum = preEodPosition.InterestProfitSum`(cs:774)| `f9d8a256` | 2024-05-09 | 山证基线 | | `interest = interestProfitSum * closePercent`(cs:776)| `6708878d` | 2026-04-13 hjhan | 改起点为×closePercent | | 同日去重(cs:435-443)| `535497c68` | 2026-06-04 吴方海 | 加同日去重补救,仅覆盖当天 | | eod 扣减 TdCloseInterest(cs:829)| 基线 | 2024-05 | eod 层有扣减已实现 | **结论**:利息计算链路是基线设计,经历过多次局部修补(`6708878d` 改起点、`535497c68` 加同日去重)。**具体哪个改动引入了用户反馈的偏差,需用干净样本定位**。 --- ## 六、如何确认 bug(已坐实) ### 6.1 实测坐实(交易 1889,6-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 `CalcDailySimpleInterest`(cs:774/776)读 `InterestProfitSum` 作为起点。既然 `InterestProfitSum` 没扣已实现,就在这里扣: ```csharp // 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=互换/自动互换)的收盘路径里特殊处理: ```csharp // 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)只查当天,跨天互换事件漏掉。可扩展到历史: ```csharp // cs:336 改 UnwindDate == unwindDate 为 UnwindDate <= unwindDate ``` 但 cs:435 的 `!calcLast` 限制仍会导致算尾配置下不扣。作为方案 A/B 的补充。 ### 方案对比 | 方案 | 改动量 | 治本程度 | 风险 | |------|--------|---------|------| | A(扣 RealizedInterest)| 极小(1行) | 修默认值 | 低,需验证字段口径 | | B(eod 公式归零)| 中 | 修源头 | 较高,改 eod 核心 | | C(closeList 扩历史)| 小 | 修去重 | 中,算尾仍漏 | **建议**:优先方案 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_event(48 条 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:287`(GetUnwindInterestList)| | 默认值计算主方法 | `SwapDealService.cs:302`(GetUnwindInterests)/ `:358`(GetInterests)| | 单利重算 | `SwapDealService.cs:771`(CalcDailySimpleInterest)| | 复利重算 | `SwapDealService.cs:705`(CalcDailyCompoundInterest)| | InterestProfitSum 读取(起点)| `SwapDealService.cs:774` / `:645`(InitSwapDealInterest)| | 方向系数(实测符号相反疑似)| `SwapDealService.cs:673`(interestRatio)| | 同日去重(可疑A)| `SwapDealService.cs:336`(closeList)/ `:435-443`(扣减)| | 固定值本金(可疑B)| `SwapDealService.cs:461-463`(CalcNotionalByMode)| | eod 扣减待实现(有扣减)| `SwapEodPositionService.cs:829`(InterestIncomeSum 扣 TdCloseInterest)| | 平仓后回写持仓 | `SwapDealService.cs:1664`(InterestAmount 累加)/ `:1668`(预付金扣减)| | 前端展示(非源)| `unwindSwapTrade.js:262-275` / `SwapUnwind.cshtml:113,146`| | 数据模型·待实现 | `EodSwapPosition.cs:282`(InterestProfitSum)/ `:270`(InterestIncomeSum)| | 数据模型·已实现 | `EodSwapPosition.cs:355`(RealizedInterest)/ `:368`(RealizedPnl)|