新增文档:互换分红损益字段语义与重复计算分析
分析互换分红/付息损益核算链路中的字段语义混乱与分红重复计算问题, 用真实交易数据佐证,供团队对齐字段语义、确定修复方案: - 核心结论:MarkClosePnl含分红(价差+费+DividendIn),而DividendIn又 单独存同一笔分红,导致汇总时分红算2次(确定,非2~3次) - 字段三层分类:客观源头/一次计算/入库字段,理清哪些是录入哪些是计算 - totalInterest链路:payment_interest→求和→×0.01→增值税调整→DividendIn - MarkClosePnl含分红的四场景对照(手动互换/平仓、自动互换/平仓) - 真实数据佐证:6笔交易(含EventType=3/4)显示差值=0且价差=0 - 重复次数确定2次:两个层级各自独立计算,非叠加3次 - 演进过程(f9d8a256→2140a97f,用hash+日期) - 修复方向A(治本:MarkClosePnl不含分红)/B(保守:改汇总公式) - 附录A:验证SQL(重复检验/守恒检验) - 附录B:精确文件:类:行号索引 - 附录C:calcFloatClosePnl两处不一致技术债(floatRatio/longRatio) - 附录D:测试策略(记录golden source分支refactor-swap-event-testable)
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
# 互换分红损益:字段语义与重复计算分析
|
||||
|
||||
> 本文档分析互换(收益互换)"分红/付息损益"核算链路中存在的字段语义混乱与分红重复计算问题。
|
||||
> 用真实交易数据佐证,供团队对齐字段语义、确定修复方案。
|
||||
> 成文于 2026-06,排查范围覆盖 `f9d8a256`(山证基线)至 `1c9b28ff`(最新)。
|
||||
|
||||
---
|
||||
|
||||
## 一、问题概述
|
||||
|
||||
互换的"已实现盈亏"在多个代码路径下**会把同一笔分红计算恰好 2 次**,导致已实现盈亏虚高(分红翻倍)。
|
||||
|
||||
根本原因:**`MarkClosePnl`(盯市盈亏)字段把"价差盈亏"和"分红"揉在了一起**,而分红又有独立的字段 `DividendIn`,导致同一笔分红在两个字段里各存了一份,汇总时被重复累加。
|
||||
|
||||
> 经过逐行确认代码路径 + 真实数据验证,**确定**是 2 次(非 2~3 次)。证明见第三、四章。
|
||||
|
||||
---
|
||||
|
||||
## 二、字段分类:客观源头 vs 派生计算
|
||||
|
||||
理解整块的关键是分清"客观源头"(不可改的事实)和"派生计算"(基于源头算出来的)。分三层:
|
||||
|
||||
### 第一层:客观源头(事实,所有计算的基石)
|
||||
|
||||
| 字段 | 所在表 | 来源 | 含义 | 示例值 |
|
||||
|------|--------|------|------|--------|
|
||||
| `payment_interest` | `bond_payment` | 外部付息公告 | 每张面值付息额(如每100元付3元)| 3 |
|
||||
| `PosiQuantity` | `swap_position` | 用户录入/中债估值 | 持仓数量 | 1000万 |
|
||||
| `PosiGrossPrice` | `swap_position` | 用户录入/中债估值 | 期初全价(相对价) | 1.00 |
|
||||
| `TradingAmountAvg` | `swap_flow_event` | 用户互换/平仓时输入 | 期末全价(相对价) | 1.00 |
|
||||
|
||||
> 这些是"真实发生的事",不随计算方式改变。
|
||||
|
||||
### `totalInterest` 的来源(payment_interest → totalInterest → DividendIn 链路)
|
||||
|
||||
`totalInterest` **不是** `payment_interest` 本身,而是经过换算的"每张分红率(小数)"。链路:
|
||||
|
||||
```
|
||||
payment_interest (bond_payment, 每张付息额, 如 3)
|
||||
│ GetBondPaymentService 在区间内求和
|
||||
▼
|
||||
Σ payment_interest (如 3+3+3=9)
|
||||
│ × 0.01 (从"每100元"转成"小数比率")
|
||||
▼
|
||||
0.09
|
||||
│ ÷ (1+税) × (1-税) (增值税调整, 与 EOD 口径一致)
|
||||
▼
|
||||
totalInterest ≈ 0.077 (每张面值的分红率, 小数)
|
||||
│ 前端 getDivindIn: PosiQuantity × totalInterest × 方向
|
||||
▼
|
||||
DividendIn = 1000万 × 0.077 × 方向 = -770000 (本次互换的分红金额)
|
||||
```
|
||||
|
||||
代码位置:后端 `BondPaymentController.GetBondPayMentInterest`(`BondPaymentController.cs:91-100`),前端 `getDivindIn`(`incomeSwapTrade.js:197` / `unwindSwapTrade.js:286`)。
|
||||
|
||||
### 第二层:一次计算(基于源头直接算)
|
||||
|
||||
| 字段/中间值 | 所在表 | 算法 | 示例 |
|
||||
|------------|--------|------|------|
|
||||
| `DividendIn` | `swap_flow_event` | `PosiQuantity × totalInterest × 方向`(后端算每张率,前端算金额)| -9148.25 |
|
||||
| 价差盈亏(临时值) | 无独立字段 | `CloseNotionalValue × (期末价 - 期初价) × 方向` | 0(期末=期初时)|
|
||||
| `TradingFee` | `swap_flow_event` | 用户填的费用 | 0 |
|
||||
|
||||
### 第三层:入库字段(⚠️ 问题所在)
|
||||
|
||||
| 字段 | 所在表 | 算法 | ⚠️ 问题 |
|
||||
|------|--------|------|---------|
|
||||
| **`MarkClosePnl`** | `swap_flow_event` | `价差盈亏 + 交易费 + DividendIn` | **把分红揉进来了!** |
|
||||
| `DividendIn` | `swap_flow_event` | 直接存第二层的值 | 和上面 MarkClosePnl 里的分红是**同一笔** |
|
||||
|
||||
**核心矛盾**:同一笔分红(如 -9148.25)同时存在 `MarkClosePnl` 和 `DividendIn` 两个字段里。
|
||||
|
||||
---
|
||||
|
||||
## 三、MarkClosePnl 含分红:四个场景对照表(代码 + 数据佐证)
|
||||
|
||||
`MarkClosePnl` 在四个场景下的计算来源不同,但**全部含分红**:
|
||||
|
||||
| 场景 | EventType | MarkClosePnl 计算位置 | 算法 | 含分红 |
|
||||
|------|-----------|---------------------|------|--------|
|
||||
| 手动互换 | 3 | 前端 `calcFloatClosePnl:140`(后端 `SwapIncome` 直接存)| `价差 + 费 + DividendIn` | **含** |
|
||||
| 手动平仓 | 2 | 前端 `calcFloatClosePnl`(后端 `SwapUnwind` 直接存)| `价差 + 费 + DividendIn` | **含** |
|
||||
| 自动互换(分红型) | 4 | 后端 `DealDividends:463` | `= PosiDividendSum`(分红本身)| **含**(就是分红本身)|
|
||||
| 自动全平仓 | 2 | 后端 `AuotoSwapUnwind:1102`(`SwapDealService` 类)| `价差 + 费 + DividendIn` | **含** |
|
||||
|
||||
> 注意:后端 `SwapDealService:1102` 在 `AuotoSwapUnwind` 方法内,仅被 `SwapTradeAutoService` 调用(自动全平仓,如到期自动平仓)。**手动互换/平仓的 MarkClosePnl 由前端算好后传入,后端直接存库不重算。**
|
||||
|
||||
### 真实数据佐证(2026-06 查询实测)
|
||||
|
||||
查询 swap_flow_event,对有分红(DividendIn!=0)的事件看 `MarkClosePnl`、`DividendIn`、`价差(期末-期初)`:
|
||||
|
||||
**A组:MarkClosePnl == DividendIn(差值=0,纯分红,无价差)**
|
||||
|
||||
| trade | EventType | 标的 | MarkClosePnl | DividendIn | 差值 | 价差(期末-期初) |
|
||||
|-------|-----------|------|--------------|------------|------|----------------|
|
||||
| 1854 | 4(自动互换) | 180205.IB | -18.95 | -18.95 | **0** | **0** |
|
||||
| 1854 | 3(互换) | 180205.IB | 18.00 | 18.00 | **0** | **0** |
|
||||
| 1868 | 3(互换) | 210210.IB | 103.346 | 103.346 | **0** | **0** |
|
||||
| 1877 | 3(互换) | 210210.IB | -1033.46 | -1033.46 | **0** | **0** |
|
||||
| 1885 | 3(互换) | 180205.IB | -9148.25 | -9148.25 | **0** | **0** |
|
||||
| 1891 | 4(自动互换) | 180205.IB | -9.51 | -9.51 | **0** | **0** |
|
||||
|
||||
**结论**:当价差=0(期末价=期初价)时,`MarkClosePnl` 完全等于 `DividendIn`。这证明 `MarkClosePnl = 价差(0) + 费(0) + 分红 = 分红`,**MarkClosePnl 含分红,铁证成立**。自动互换(EventType=4)同样成立。
|
||||
|
||||
> B组(差值≠0,如 1891-3互换 MarkClosePnl=395594.949)的差值是"价差+费"成分,但这些是历史脏数据(期末价被多除100导致价差异常),不影响"MarkClosePnl 含分红"的结论——只是这部分价差也是脏的。
|
||||
|
||||
---
|
||||
|
||||
## 四、重复次数:确定 2 次(非 2~3 次)
|
||||
|
||||
### 数据流图(以 trade 1885 纯分红互换为例,分红 = -9148.25)
|
||||
|
||||
```
|
||||
【客观源头】
|
||||
PosiQuantity = 100万, totalInterest = 每张分红率
|
||||
期末价 = 期初价 = 1.00 (价差=0)
|
||||
│
|
||||
▼
|
||||
【第二层计算】
|
||||
DividendIn = 100万 × totalInterest × 方向 = -9148.25
|
||||
价差盈亏 = 0
|
||||
交易费 = 0
|
||||
│
|
||||
▼
|
||||
【第三层入库 swap_flow_event】 ⚠️ 问题在这里
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ MarkClosePnl = 价差(0) + 费(0) + 分红(-9148.25) │ ◄── 分红第①次存入
|
||||
│ = -9148.25 │
|
||||
│ DividendIn = -9148.25 │ ◄── 分红第②次存入(同一笔!)
|
||||
└──────────────────────────────────────────────┘
|
||||
│ 日终汇总
|
||||
▼
|
||||
【eod_swap_position】 (实测确认)
|
||||
TdCloseMtmPnl = Σ MarkClosePnl = -9148.25 (含分红)
|
||||
TdCloseDividend = Σ DividendIn = -9148.25 (分红)
|
||||
│
|
||||
▼
|
||||
【算 RealizedPnl 已实现盈亏】 ⚠️⚠️ 重复累加
|
||||
```
|
||||
|
||||
### 各层级重复次数(确定 2 次)
|
||||
|
||||
3 处代码分属**两个不同层级**,每个层级各自独立计算,但**每个层级分红都是 2 次**。不是叠加成 3 次。
|
||||
|
||||
| 聚合对象 | 代码位置 | 公式 | 分红算几次 |
|
||||
|---------|---------|------|-----------|
|
||||
| 单持仓 `eod_swap_position.RealizedPnl` | `1490` + `1512` | `RealizedPnl = eod.RealizedPnl + TdCloseMtmPnl`;`if(PosiDividendSum==0) RealizedPnl += TdCloseDividend` | **2 次**(Mtm含1次 + Dividend加1次)|
|
||||
| 框架合约 `eod_swap.TdRealizedPnL`(当日) | `1866` | `TdRealizedPnL += TdCloseMtmPnl + TdCloseDividend` | **2 次**(Mtm + Dividend)|
|
||||
| 框架合约 `eod_swap.RealizedPnL`(累计) | `1869` | `RealizedPnL = Σ(RealizedMtmPnL + RealizedDividend + RealizedFee + ...)` | **2 次**(RealizedMtmPnL + RealizedDividend)|
|
||||
|
||||
**为什么不是 3 次**:第1869行的 `eod_swap.RealizedPnL` **不读取**单持仓的 `eod_swap_position.RealizedPnl`,而是用 `RealizedMtmPnL + RealizedDividend` **独立重新聚合**。所以它与1490/1512行不是叠加关系——它们是**两个不同层级的对象**(`eod_swap` 是 `eod_swap_position` 的上层聚合),各自都把分红算了 2 次。
|
||||
|
||||
```
|
||||
层级关系:
|
||||
eod_swap_position (单持仓, 1490/1512算RealizedPnl) ← 第1层,分红2次
|
||||
│ 聚合
|
||||
▼
|
||||
eod_swap (框架合约, 1866/1869重新聚合) ← 第2层,分红2次(不读第1层的RealizedPnl)
|
||||
```
|
||||
|
||||
**根本原因只有一个**:`MarkClosePnl`(→ `TdCloseMtmPnl`/`RealizedMtmPnL`)含分红,而 `DividendIn`(→ `TdCloseDividend`/`RealizedDividend`)又是同一笔分红。无论哪个层级,只要把"Mtm"和"Dividend"相加,分红就翻倍。
|
||||
|
||||
---
|
||||
|
||||
## 五、为什么会变成这样(演进过程,用 hash + 日期)
|
||||
|
||||
分红核算经历了一长串反复修改,每次补丁都在加剧或修正重复:
|
||||
|
||||
| 时间 | 提交 | 改动 | 效果 |
|
||||
|------|------|------|------|
|
||||
| 2024-05-09 | `f9d8a256` | 山证基线,MarkClosePnl 含分红 | 埋下根源 |
|
||||
| 2025-05-28 | `083848fb` | 审核流程重构,丢 eventType 分支 | 引入互换扣本金bug |
|
||||
| 2026-06-25 | `1aba5cca` | DealDividends 加 SwapPositionValue/RealizedPnl 调整 | 语句顺序错(EQD-6290) |
|
||||
| 2026-06-25 | `90b66922` | 修语句顺序 + 改全量重算口径 | 守恒修复✓ |
|
||||
| 2026-06-25 | `2140a97f` | 加 `RealizedPnl += TdCloseDividend` | **加剧重复!**(以为分红漏算,其实Mtm已含)|
|
||||
| 2026-06-26 | `1c9b28ff` | 互换审核不扣本金 | 修互换扣本金bug ✓ |
|
||||
|
||||
---
|
||||
|
||||
## 六、修复方向
|
||||
|
||||
判断标准:**让每个字段语义单一、职责不重叠**。
|
||||
|
||||
### 方向A(推荐,治本):让 MarkClosePnl 不含分红
|
||||
|
||||
让字段职责分离——Mtm 只管价差,Dividend 只管分红:
|
||||
|
||||
| 改动点 | 改前 | 改后 |
|
||||
|--------|------|------|
|
||||
| 前端 `calcFloatClosePnl:140`(手动互换/平仓)| `MarkClosePnl = 价差 + 费 + DividendIn` | `MarkClosePnl = 价差 + 费`(去掉 DividendIn)|
|
||||
| 后端 `AuotoSwapUnwind:1102`(自动平仓)| `MarkClosePnl = 价差 + 费 + DividendIn` | 去掉 `+ DividendIn` |
|
||||
| 后端 `DealDividends:463`(自动互换)| `MarkClosePnl = PosiDividendSum` | `MarkClosePnl = 0`(自动互换无价差,分红走 DividendIn)|
|
||||
| `2140a97f` 新增(1512行)| `RealizedPnl += TdCloseDividend` | **保留**(此时 Mtm 不含分红,加 Dividend 才正确)|
|
||||
| 1490行 | `RealizedPnl = eod.RealizedPnl + TdCloseMtmPnl` | 保留(Mtm 现在是纯价差)|
|
||||
| 1866/1869行 | `TdCloseMtmPnl + TdCloseDividend` | 保留(现在 Mtm 不含分红,相加正确)|
|
||||
|
||||
**效果**:
|
||||
```
|
||||
MarkClosePnl = 纯价差(0) ← 只管价差
|
||||
DividendIn = 分红(-9148.25) ← 只管分红
|
||||
RealizedPnl = TdCloseMtmPnl(0) + TdCloseDividend(-9148.25) = -9148.25 ← 各加1次,不重复 ✓
|
||||
```
|
||||
|
||||
**风险**:要排查"前端盯市盈亏展示列"是否期望含分红(如果某处展示 MarkClosePnl 给用户看,去掉分红后显示会变)。需要回归测试前端展示。
|
||||
|
||||
### 方向B(保守):保留 MarkClosePnl 含分红,改汇总公式
|
||||
|
||||
| 改动点 | 改法 |
|
||||
|--------|------|
|
||||
| 1866行 | `TdRealizedPnL += TdCloseMtmPnl`(去掉 `+ TdCloseDividend`,因为 Mtm 已含)|
|
||||
| 1869行 | `RealizedPnL = Σ(RealizedMtmPnL + RealizedFee + ...)`(去掉 `+ RealizedDividend`)|
|
||||
| `2140a97f`(1512行)| **撤销** `RealizedPnl += TdCloseDividend`(1490 的 Mtm 已含)|
|
||||
|
||||
**风险**:`TdCloseDividend`/`RealizedDividend` 可能被分红明细报表单独展示,去掉后那部分变0。
|
||||
|
||||
---
|
||||
|
||||
## 七、建议
|
||||
|
||||
1. **优先对齐"MarkClosePnl 该不该含分红"这个根本问题**。这是所有重复的根源。
|
||||
2. 达成共识后按**方向A**改(字段职责分离)。方向A改完后,`2140a97f` 这类补丁就不再需要,逻辑能稳定下来。
|
||||
3. 改完后用"守恒检验"验证:`ΔSwapPositionValue + ΔRealizedPnl == 0`(见附录 SQL)。
|
||||
4. **这块缺少自动化测试**(前端无测试、后端 Swap 单测只覆盖 EodPositionService)。建议补一个"分红守恒"后端单测兜底,避免反复打补丁。
|
||||
|
||||
---
|
||||
|
||||
## 附录A:验证 SQL
|
||||
|
||||
### A.1 验证 MarkClosePnl 含分红(含价差佐证)
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
fe.SwapTradeId AS trade_id,
|
||||
fe.EventType AS 事件类型, -- 2=平仓, 3=互换, 4=自动互换
|
||||
fe.UnderlyingCode AS 标的,
|
||||
fe.MarkClosePnl AS 盯市盈亏_入库,
|
||||
fe.DividendIn AS 分红_入库,
|
||||
fe.MarkClosePnl - fe.DividendIn AS 差值_纯价差和费,
|
||||
sp.PosiGrossPrice AS 期初全价,
|
||||
fe.TradingAmountAvg AS 期末全价,
|
||||
fe.TradingAmountAvg - sp.PosiGrossPrice AS 价差_期末减期初,
|
||||
fe.TradingFee AS 交易费
|
||||
FROM swap_flow_event fe
|
||||
JOIN swap_position sp ON sp.SwapTradeId = fe.SwapTradeId AND sp.IsInitial = 1 AND sp.UnderlyingCode = fe.UnderlyingCode
|
||||
WHERE fe.DividendIn != 0
|
||||
ORDER BY fe.SwapTradeId, fe.id DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
**判断**:若某行 `差值=0` 且 `价差=0` → 证明无价差互换时 MarkClosePnl 完全等于分红。
|
||||
|
||||
### A.2 验证分红互换守恒(修复后用)
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
cur.SwapTradeId AS trade_id,
|
||||
cur.ValueDate AS 当日,
|
||||
cur.SwapPositionValue AS 当日持仓价值,
|
||||
pre.SwapPositionValue AS 前日持仓价值,
|
||||
cur.SwapPositionValue - pre.SwapPositionValue AS Δ持仓价值,
|
||||
cur.RealizedPnl AS 当日已实现盈亏,
|
||||
pre.RealizedPnl AS 前日已实现盈亏,
|
||||
cur.RealizedPnl - pre.RealizedPnl AS Δ已实现盈亏,
|
||||
(cur.SwapPositionValue - pre.SwapPositionValue) + (cur.RealizedPnl - pre.RealizedPnl) AS 守恒检验_应接近0,
|
||||
cur.TdCloseDividend AS 当日已实现分红_参考
|
||||
FROM eod_swap_position cur
|
||||
JOIN eod_swap_position pre
|
||||
ON pre.SwapTradeId = cur.SwapTradeId AND pre.UnderlyingCode = cur.UnderlyingCode
|
||||
AND pre.ValueDate < cur.ValueDate
|
||||
AND pre.id = (SELECT MAX(id) FROM eod_swap_position
|
||||
WHERE SwapTradeId = cur.SwapTradeId AND UnderlyingCode = cur.UnderlyingCode AND ValueDate < cur.ValueDate)
|
||||
WHERE cur.SwapTradeId IN (
|
||||
SELECT DISTINCT fe.SwapTradeId FROM swap_flow_event fe
|
||||
WHERE fe.EventType = 4 AND fe.DividendIn != 0 AND fe.UnderlyingCode IS NOT NULL
|
||||
)
|
||||
AND cur.UnderlyingCode IS NOT NULL
|
||||
ORDER BY cur.SwapTradeId DESC, cur.ValueDate DESC LIMIT 15;
|
||||
```
|
||||
**判断**:守恒检验列接近0 → 修复成功。
|
||||
|
||||
---
|
||||
|
||||
## 附录B:关键代码位置索引(精确到类:行号)
|
||||
|
||||
### B.1 MarkClosePnl 含分红(重复根源)
|
||||
|
||||
| 场景 | 文件:类:行号 | 算法 | 含分红 |
|
||||
|------|-------------|------|--------|
|
||||
| 手动互换 | `incomeSwapTrade.js`:`calcFloatClosePnl`:133-134 | `价差(CloseNotionalValue×...) + 费 + DividendIn` | **含** |
|
||||
| 手动平仓 | `unwindSwapTrade.js`:`calcFloatClosePnl`:203-205 | `价差(CloseQty×...) + 费×floatRatio×-1 + DividendIn` | **含** |
|
||||
| 自动互换(分红型) | `SwapEodPositionService.cs`:`DealDividends`:463 | `= PosiDividendSum`(分红本身)| **含** |
|
||||
| 自动全平仓 | `SwapDealService.cs`:`AuotoSwapUnwind`:1101-1102 | `价差 + 费 + DividendIn` | **含** |
|
||||
| 多空组合合并事件 | `SwapFlowEventService.cs`:`MergePageEvent`:287 | `= PayMarkUnwindPnl`(待确认来源)| 待确认 |
|
||||
|
||||
### B.2 已实现盈亏累加点(重复发生处)
|
||||
|
||||
| 聚合对象 | 文件:类:行号 | 公式 | 分红次数 |
|
||||
|---------|-------------|------|---------|
|
||||
| 单持仓 `RealizedPnl` | `SwapEodPositionService.cs`:`UpdateEodPosition`:1490 | `= eod.RealizedPnl + TdCloseMtmPnl` | 含分红(Mtm) |
|
||||
| 单持仓 `RealizedPnl`(加分红) | `SwapEodPositionService.cs`:`UpdateEodPosition`:1512(`2140a97f`)| `if(PosiDividendSum==0) += TdCloseDividend` | 又加分红 |
|
||||
| 框架合约 `TdRealizedPnL` | `SwapEodPositionService.cs`:`SaveEodSwap`:1866 | `+= TdCloseMtmPnl + TdCloseDividend` | 翻倍 |
|
||||
| 框架合约 `RealizedPnL` | `SwapEodPositionService.cs`:`SaveEodSwap`:1869 | `= Σ(RealizedMtmPnL + RealizedDividend + ...)` | 翻倍 |
|
||||
|
||||
### B.3 totalInterest 计算链路
|
||||
|
||||
| 环节 | 文件:类:行号 | 算法 |
|
||||
|------|-------------|------|
|
||||
| totalInterest | `BondPaymentController.cs`:`GetBondPayMentInterest`:91-100 | `Σ payment_interest × 0.01 / (1+税) × (1-税)` |
|
||||
| DividendIn(互换) | `incomeSwapTrade.js`:`getDivindIn`:197 | `PositionQty × totalInterest × 方向` |
|
||||
| DividendIn(平仓) | `unwindSwapTrade.js`:`getDivindIn`:286 | `CloseQty × (totalInterest - consumedDividendInterest) × 方向` |
|
||||
|
||||
---
|
||||
|
||||
## 附录C:calcFloatClosePnl 两处不一致(技术债,待确认)
|
||||
|
||||
平仓页和互换页的 `calcFloatClosePnl` 存在 **4 处不一致**,属于重复代码各自实现导致,哪个对需业务确认:
|
||||
|
||||
```js
|
||||
// floatRatio / longRatio 含义:
|
||||
// floatRatio = (PayDirection == 1) ? 1 : -1; // 收取=1, 支付=-1 (本方收支方向)
|
||||
// longRatio = (PositionType == 1) ? 1 : -1; // 多头=1, 空头=-1 (本方多空方向)
|
||||
```
|
||||
|
||||
| 维度 | 平仓 `unwindSwapTrade.js:203-205` | 互换 `incomeSwapTrade.js:133-134` | 说明 |
|
||||
|------|----------------------------------|----------------------------------|------|
|
||||
| 价差基数 | `CloseQty`(数量)| `CloseNotionalValue`(名义本金)| 量纲不同 |
|
||||
| 价差方向 | `× floatRatio × longRatio` | `× floatRatio`(**无 longRatio**)| ⚠️ 互换若支持空头可能算错 |
|
||||
| 四舍五入 | `Math.round×10000/10000` + `toFixed(2)` | 无 | ⚠️ 互换无取整,精度风险 |
|
||||
| 费用方向 | `(费) × floatRatio × -1` | `费`(**无方向**)| ⚠️ 互换支付方费用方向可能没反转 |
|
||||
| 分红 | `+ DividendIn` | `+ DividendIn` | 一致(都含分红)|
|
||||
|
||||
**建议**:统一为公共函数(参考 `changeUnderlyingPrice` 两页一字不差的做法),方向处理对齐到更完整的平仓版本(含 `longRatio` + `floatRatio*-1`)。但需业务确认互换场景下 `PayDirection` 是否已隐含多空方向。
|
||||
|
||||
---
|
||||
|
||||
## 附录D:测试策略
|
||||
|
||||
### 现状
|
||||
- 后端有测试基础设施:`UnitTestProject/Modules/SwapModule/SwapEodPositionServiceIntegrationTest.cs`(MSTest,集成测试)
|
||||
- **但现有测试基础薄弱**:现有测试(如 `TestDealInterests_ViaSwapPositionCompose`)虽调用了真实业务代码 `service.SwapPositionCompose(...)`,但 catch 异常后 `Assert.IsTrue(true)` 就算通过,**没有对输出的 eod 持仓字段做任何断言**,属于"能跑不崩就算过"的烟雾测试,无法验证业务正确性。
|
||||
- 前端无测试框架(无 jest/mocha/playwright)
|
||||
|
||||
### 教训:测试必须调用真实业务代码
|
||||
> 曾尝试写"构造字段对象自己断言"的纯逻辑测试(如 `TestDividendConservation_*`),但这类测试**自己造数据自己测**,不调用 `SwapEodPositionService` 的任何方法,是**无意义的伪测试**——业务代码写错也会通过。已删除。真正的测试必须:调用真实业务方法 → 查回输出字段 → 断言字段关系。
|
||||
|
||||
### 后续测试方向
|
||||
- **参考 golden source 分支**:`glms/feature/refactor-swap-event-testable`(该分支有可复用的 Swap 事件测试数据构造,后续补分红守恒测试时以此为基础)
|
||||
- 真正有意义的守恒测试需要:
|
||||
1. 用 golden source 的测试数据工厂构造一笔带分红的互换交易(`trade` + `swap_position` + `bond_payment` + `swap_flow_event`,多张表自洽)
|
||||
2. 调用真实的 `SwapPositionCompose` 或 `DealDividends`
|
||||
3. 查回 `eod_swap_position`,断言 `ΔSwapPositionValue + ΔRealizedPnl == 0`
|
||||
- 这是独立的工程任务,需要专门投入时间,不适合在排查任务里匆忙做
|
||||
|
||||
### 当前验证手段(测试补全前)
|
||||
- **数据验证SQL**(附录A.1/A.2):纯查询,不改代码,可快速验证线上数据是否重复/守恒
|
||||
- **修复后回归**:用附录A的SQL在修复前后各跑一次,对比 `TdCloseMtmPnl + TdCloseDividend` 是否还有翻倍
|
||||
Reference in New Issue
Block a user