Files
zszq-trs/项目文档/互换部分平仓后利息端预付金默认盈亏偏大问题分析.md
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

296 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 互换部分平仓后:利息端/预付金"平仓盈亏默认值"偏大问题分析
> 本文分析收益互换"昨天收益结算(互换)→ 收盘 → 今天平仓"场景下,**预付金平仓盈亏和利息端平仓盈亏的默认值计算偏大**的问题:根因、涉及的文件、如何确认、修复方案与自测方法。
> 成文于 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 收盘后用交易 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.26**`RealizedInterest`(已实现)= **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`(收盘更新待实现利息):
```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:829IPS = 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 扣减 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
`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 核心 |
| 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: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|