Merge branch 'glms/feature/1.4.2' of http://git.yiliantech.com/gitlab/otc-dev/zszq-trs into glms/feature/1.4.2

This commit is contained in:
锦麟 王
2026-06-26 14:13:01 +08:00
@@ -0,0 +1,159 @@
# MarkClosePnl 字段语义统一:决策文档
> **状态**:待团队决策 | **日期**2026-06-26 | **前置**:[互换分红损益字段语义与重复计算分析.md](./互换分红损益字段语义与重复计算分析.md)、修复提交 `ab70531f`2026-06-26MarkClosePnl 不再含分红)
---
## 一、问题背景:一个字段,三种语义
`swap_flow_event.MarkClosePnl`(浮动端平仓盈亏·盯市)在历史上**同时承担了三个互斥的职责**,导致各处使用公式不一致。2026-06-26 的修复(`ab70531f`)让它"不再含分红",但**消费端的公式没有同步调整**,新数据下会产生新的偏差。
### MarkClosePnl 的三种历史语义
| 语义 | 含义 | 谁在用 |
|---|---|---|
| **A. 纯价差盯市** | `MarkClosePnl = (平仓价-成本价)×量` | 修复后(`ab70531f`)的目标语义 |
| **B. 价差+费+分红 总和** | 历史上前端 `calcFloatClosePnl` 把费和分红加进去 | 界面"浮动"列(用户误以为是总盈亏)|
| **C. 收益计算基数** | 各处用不同公式减/不减分红得到 PosiPnl | 结算单、收益列表 |
**根因**2024-05-09 从山证 v2.3.0 拷贝(`f9d8a256`)时,`MarkClosePnl` 入库的就是 B(总和),但后续不同开发者按各自理解写了 C 的多种公式,造成不一致。
---
## 二、各处使用现状与不一致清单(含引入时间)
### 2.1 写入点(MarkClosePnl 怎么生成)
| 位置 | 公式 | 引入 | 语义 |
|---|---|---|---|
| `SwapDealService.cs:1101` 自动平仓 | `(平仓价-成本价)×量` | `f9d8a256` 2024-05-09 | **修复后=纯价差** |
| `SwapDealService.cs:1103` 自动平仓(加费) | 上面 `+费×-1` | `f9d8a256` | 修复后=价差-费 |
| `SwapEodPositionService.cs:463` 自动互换分红 | `=0`(修复前=`PosiDividendSum`| `ab70531f` 2026-06-26 | **修复后=0(不含分红)** |
| 前端 `incomeSwapTrade.js:133` 手动互换 | `calcFloatClosePnl``+DividendIn` | `ab70531f` | **修复后=纯价差** |
| 前端 `unwindSwapTrade.js:203` 手动平仓 | 同上 | `ab70531f` | **修复后=纯价差** |
> **关键**`ab70531f` 已把 4 个写入点统一为"纯价差/价差-费",不含分红。
### 2.2 消费点(PosiPnl / 收益公式)—— ⚠ 不一致集中区
| # | 位置 | 公式 | 减分红? | 引入 | 修复后影响 |
|---|---|---|---|---|---|
| ① | `SwapFlowEvent.cs:384` | `PosiPnl = MarkClosePnl - TradingFee` | **否** | `f9d8a256` 2024-05-09 | 新数据偏大(没减分红)|
| ② | `TradeSettleBillGenerator.cs:110` 结算单 | `PosiPnl = -(MarkClosePnl - tradingFee - DividendIn)` | **是** | `48cf0592` 2026-05-06 | **新数据多减一份分红** ⚠ |
| ③ | `SwapFlowEventService.cs:528` 收益列表 | `PosiPnl = MarkClosePnl - TradeFee - DividendIn - TradingFee` | **是** | `829334a4` 2025-05-30 | **新数据多减一份分红** ⚠ |
**核心风险(②③)**:修复前 MarkClosePnl 含分红,减去 DividendIn 正好还原纯价差;修复后 MarkClosePnl **不含**分红了,再减 DividendIn 就**多减了一份**,结算单和收益列表金额偏小。
### 2.3 当"总和"用的地方
| 位置 | 公式 | 引入 | 含义 |
|---|---|---|---|
| `SwapEodPositionService.cs:681` | `amount = MarkClosePnl + CloseFee + DividendIn` | `f9d8a256` 2024-05-09 | 判是否产生资金记录(三者总和)|
| `SwapFlowEventService.cs:529` | `NetSettmentAmount = MarkClosePnl + InterestClosePnL` | `083848fb` 2025-05-28 | 净结算额 |
| `SwapEodPositionService.cs:1610` | `TdCloseMtmPnl = Σ MarkClosePnl` | `f9d8a256` 2024-05-09 | eod 盯市列累加 |
### 2.4 界面展示(eventlist/SwapflowList/EodPositionRisks
"浮动端平仓盈亏"在界面**拆成 3 个独立列**,没有"总和"列:
- `MarkClosePnl` → 列名"浮动端平仓盈亏·**浮动**"
- `DividendIn` → 列名"浮动端平仓盈亏·**分红**"
- `CloseFee` → 列名"浮动端平仓盈亏·**费用**"
> 用户历史上看到的"浮动列"= 总盈亏,是 buggy 的 MarkClosePnl 凑出来的;修复后该列=纯价差,会变小。**"三者总和"从未有独立列**。
---
## 三、历史数据问题
已入库的 `swap_flow_event.MarkClosePnl` 不可逆——**历史记录含分红,新记录不含**。这意味着任何消费点公式,对历史和新数据只能选一种正确:
| 公式策略 | 历史数据(含分红) | 新数据(纯价差) |
|---|---|---|
| 不减 DividendIn(如 ①)| 偏大 | 偏大 |
| 减 DividendIn(如 ②③)| 正确(抵消)| **偏小(多减)** |
**没有一种公式能同时让历史和新数据都对**,除非:
- (a) 数据迁移:刷新历史 MarkClosePnl 还原为纯价差,或
- (b) 加版本标记:区分"修复前/后"的事件,分别用不同公式
---
## 四、三种改法方案(供团队选择)
### 方案一:修正消费点公式(最小改动,推荐作为第一步)
**做法**:把 MarkClosePnl 的语义**正式定为"纯价差盯市"**,修正 ②③ 去掉 `-DividendIn`
| 位置 | 改动 |
|---|---|
| ② `TradeSettleBillGenerator.cs:110` | `-(MarkClosePnl - tradingFee)`(去掉 -DividendIn|
| ③ `SwapFlowEventService.cs:528` | `MarkClosePnl - TradeFee - TradingFee`(去掉 -DividendIn|
| ① `SwapFlowEvent.cs:384` | 保持,或补 `-DividendIn` 与 ③ 统一(需业务确认收益口径)|
-**优点**:改动小(3 行),新数据全部正确,语义立即清晰
-**缺点**:**历史数据仍不一致**(见第三节),需配合数据迁移或接受历史偏差
- 🎯 **适合**:快速止血,让修复(ab70531f)真正生效,不让新数据出错
### 方案二:数据迁移 + 方案一(彻底统一)
**做法**:在方案一基础上,对历史 `swap_flow_event` 执行数据迁移:
```sql
-- 把历史 MarkClosePnl 还原为纯价差(减去当时混入的分红和费)
UPDATE swap_flow_event
SET MarkClosePnl = MarkClosePnl - DividendIn - CloseFee
WHERE EventType IN (2,3,4) AND DataState = 100
AND EventDate < '2026-06-26'; -- 修复提交前的事件
```
(实际迁移需先 SELECT 验证范围、备份、分批执行)
-**优点**:历史与新数据语义完全统一,所有公式一套即可
-**缺点**:改库有风险,需备份+回滚预案;eod 历史表(RealizedMtmPnL 等)是否也需重算需评估
- 🎯 **适合**:团队接受改历史数据,追求长期干净
### 方案三:新增 TotalClosePnl 字段,职责分离(最长远正确)
**做法**:让每个字段职责单一:
1. `MarkClosePnl` 永远=纯价差(修复已做到,语义固定)
2. 新增 `[NotMapped] decimal TotalClosePnl => MarkClosePnl + DividendIn + CloseFee`,专给"总和"场景(结算单、资金记录判断、界面总和列)
3. 界面如需"总盈亏"列,显式绑定 `TotalClosePnl`,而非靠 MarkClosePnl 隐式承担
-**优点**:根除"一字段三语义"债务,未来不会再出现"改一处引发另一处偏差"
-**缺点**:改动面最大(涉及结算单/资金记录/界面多处),需排期
- 🎯 **适合**:作为技术债治理的长期目标,可与方案一/二分阶段实施
---
## 五、建议路径
**推荐分阶段推进**,兼顾止血与长期:
1. **立即(方案一)**:修正 ②③ 消费点公式,避免修复后新数据出错(结算单/收益金额偏小)。这是 `ab70531f` 修复的必要补全,不做的话修复只做了一半。
2. **近期(方案二)**:评估数据迁移可行性,统一历史数据语义。
3. **中期(方案三)**:引入 TotalClosePnl,彻底消除语义债务。
---
## 六、需团队决策的问题
1. **收益(PosiPnl)口径**:① 不减分红、②③ 减分红——哪个是业务正确的"收益"定义?(决定公式统一方向)
2. **历史数据**:是否接受数据迁移(方案二)?还是接受历史偏差、只保证新数据正确?
3. **界面"总盈亏"列**:是否需要显式新增一列展示 `MarkClosePnl+DividendIn+CloseFee` 总和?(决定是否走方案三)
4. **实施优先级**:方案一是否立即执行(它阻塞 ab70531f 修复的完整生效)?
---
## 附录:字段引用全景(便于实施时定位)
| 文件:行 | 用法 | 语义 |
|---|---|---|
| `SwapFlowEvent.cs:384` | `PosiPnl = MarkClosePnl - TradingFee` | 收益(不减分红) |
| `SwapFlowEvent.cs:211` | 字段定义 | 入库列 |
| `TradeSettleBillGenerator.cs:110,112,116` | 减 DividendIn / 取负 / 净结算 | 收益+结算 |
| `SwapFlowEventService.cs:287,378,429,509,528,529` | 赋值/取负/收益/净结算 | 多用途 |
| `SwapEodPositionService.cs:463,681,694,1610` | 赋0/总和/累加/eod盯市 | 写入+累加 |
| `SwapDealService.cs:101,275,1101,1103,1219,1350` | 赋值 | 写入 |
| `SwapEndConfirmService.cs:96` | `SwapCloseAmount = MarkClosePnl` | 展示 |
| `RealTimeClientBanlanceService.cs:1337,1343` | `+ MarkClosePnl` 累加 | 收益累加 |
| `SwapFlowService.cs:381` | 导出格式化 | 导出 |
| 前端 `eventlist.js:247`/`SwapflowList.js:556` | 界面"浮动"列 | 展示 |