根因(GLMS-JIATT-20260805):InterestCalcMode='10'(算头不算尾,calcLast=false)时, CalcDailyCompoundInterest 循环的 'if(!calcLast && accrueDate==endDate) continue' 会跳过平仓日。若平仓日恰好是重置日(i%period==0),取价代码块被一并跳过, 导致 flowEvent.FloatRate 落库为旧周期利率,传染后续 EOD 复利计算。 修复:把重置日的 FR007 取价提前到 calcFirst/calcLast 跳过判断之前—— calcLast 只应跳过'计息',不应跳过'重置日利率取价'。同时循环外用最终 floatRate 兜底赋值 flowEvent.FloatRate,确保落库值反映最后重置日的利率。 验证: - CI_007 合成测试(平仓日=重置日):修复前 FloatRate=旧值(FAIL),修复后=新值(PASS) - 连库验证(GLMS-JIATT-20260805 8/4平仓):FloatRate 0.0123→0.0213(8/3新值) - 利息金额不变(calcLast 不计当天利息,Amount 不受影响,只修正 FloatRate 字段) - 全套 swap 测试无新增回归(334通过,2失败均为pre-existing单利/EOD路径) 附:CI_007 复现测试 + GLMS20260805 FR007/EOD 诊断工具 + 文档 longRatio 状态更新
360 lines
27 KiB
Markdown
360 lines
27 KiB
Markdown
# 互换(TRS)计算口径一致性:残留问题与治理路线
|
||
|
||
> 配套文档:`互换分红损益字段语义与重复计算分析.md`(2026-06,根因分析)
|
||
> 本次分析目的:在最新提交 `e3c473ba`(分红收益改由 EOD 单一可信源)之后,
|
||
> 复核"分散计算 / 前后端重复算 / 精度口径不统一"这类 BUG 是否仍在其它地方存在,
|
||
> 并给出"如何避免 + 后续逐渐解决"的路线。
|
||
> 分析日期:2026-08-06
|
||
|
||
---
|
||
|
||
## ⚠️ 更新状态(2026-08-07)
|
||
|
||
> 本文档最初分析日期为 2026-08-06。**第七章、第八章 8.1 关于 income 页 `longRatio` 的论断已被代码修复采纳,原文描述已过时**,阅读时请注意:
|
||
>
|
||
> | 文档原文论断 | 当前代码状态 | 修复提交 |
|
||
> |--------------|------------|----------|
|
||
> | income 页漏乘 `longRatio`(多空方向),空头会算出相反符号 | ✅ **已修复**:前端 `incomeSwapTrade.js:200,208`、后端 `FrontendCalcReference.CalcIncome:100,108` 均已补 `longRatio` | `d78d1f48`(2026-08-07) |
|
||
> | income 空头分支无测试覆盖 | ✅ **已补**:`收取空头_价格上涨_应为亏损` + jest 对应用例 | `d78d1f48` |
|
||
> | `FrontendCalcReference.CalcIncome` 注释写"无 longRatio" | ✅ **已更新**为含 longRatio 的公式 | `d78d1f48` |
|
||
>
|
||
> **仍有效的部分**(治理路线主体,代码尚未动):
|
||
> - 第三章 A 节:两页 `MarkClosePnl` 仍**各自内联**(`incomeSwapTrade.js:208` / `unwindSwapTrade.js:306`),未接入共享的 `swapCalc.calcMarkClosePnl`——"单一可信源已有却不采纳"仍在。
|
||
> - 第三章 B 节:硬编码精度魔法数字**全部仍在**(`SwapFlowService:116-118` Round(4)、`SwapDealService:1561` F10、`SwapTradeAutoService:458/460` Round(10))。
|
||
> - 第三章 C 节 / 第四章 / 第五章:swapCalc 未接入生产、精度集中化、分阶段治理路线——**仍是有效的后续路线**。
|
||
>
|
||
> 阅读建议:第三~六章按"仍有效的治理路线"读;第七、八章按"历史论证记录"读(结论已被采纳落地)。
|
||
|
||
---
|
||
|
||
## 一、这类 BUG 的本质(统一定义)
|
||
|
||
最新修复的"分红收益误显 -36,160",根因不是某一个 if 写错,而是一种**结构性缺陷**:
|
||
|
||
> **金融计算缺乏「单一可信源 / 统一口径」** —— 同一个业务量在
|
||
> **前端 JS / 后端 C# / EOD 批处理** 三条路径里被**各自重算**,
|
||
> 且价格基准、方向符号、精度位数都零散硬编码,导致:
|
||
> 1. 前后端(或同一功能的两页)对同一量算出**不同值**;
|
||
> 2. 分红等分量被**重复计入**(MarkClosePnl 含分红 → 汇总翻倍);
|
||
> 3. 金额/价格**精度不统一**,相邻环节差 1~2 位小数。
|
||
|
||
它表现为 3 个子模式:
|
||
|
||
| 子模式 | 例子 | 状态 |
|
||
|--------|------|------|
|
||
| ① 分散计算、无单一可信源 | 分红前端自算(`getDivindIn`)vs EOD `PosiDividendSum` | **最新提交已根治(方案C)** |
|
||
| ② 字段语义重叠→重复累加 | `MarkClosePnl` 含分红,汇总 `RealizedPnl` 翻倍 | 自动路径已按方向A改造;**前端两页仍有口径差** |
|
||
| ③ 硬编码精度/魔法数字 | `Math.Round(...,4/10)`、`ToString("F10")` vs `ConsGlobal.*` | **大量残留** |
|
||
|
||
---
|
||
|
||
## 二、最新提交(e3c473ba)做了什么(已修复)
|
||
|
||
- 把"浮动端平仓盈亏·分红 `DividendIn`"从**前端 `getDivindIn` 自算**(`期初持仓×totalInterest`)
|
||
改为**后端 `SwapDealService.GetPreEodDividendSum` 读 EOD `PosiDividendSum`**(单一可信源)。
|
||
- 前端 `unwindSwapTrade.js` / `incomeSwapTrade.js` 的 `getDivindIn` 删除自算逻辑,只保留后端值。
|
||
- `GetBondPayMentInterest` 标记废弃(保留接口供历史调用)。
|
||
- 配套绿灯验收测试 `GLMS20260105PartialCloseDividendBugTest.cs`。
|
||
|
||
**结论**:子模式①(分红分散计算)在"预览页展示"这条链路已根治。但**②④③ 仍在**。
|
||
|
||
---
|
||
|
||
## 三、同类问题在别处是否还存在(带 file:line 证据)
|
||
|
||
### A. 前端两页 `MarkClosePnl` 口径不一致 —— 最危险、最具体的残留 ⚠️
|
||
|
||
> **2026-08-06 修正**:初版称"平仓页用净价、互换页用全价"——经核对代码**不准确**。
|
||
> 实际两页都用**全价**:`unwindSwapTrade.js:111` 把 `initPosiNetPrice` 绑到 `PosiGrossPrice`(变量名有误导性),
|
||
> `incomeSwapTrade.js:103/207` 用 `initPosiGrossPrice`(也是全价)。真正差异见下表与 A.1。
|
||
|
||
同一字段 `MarkClosePnl`(盯市盈亏),实现对照(来源:团队金标准 `YLErpDAL/Helpers/FrontendCalcReference.cs`):
|
||
|
||
| 实现 | 位置 | 公式 | 价格基准 | 方向 | 数量基准 |
|
||
|------|------|------|----------|------|----------|
|
||
| 平仓页 | `unwindSwapTrade.js:313` | `CloseQty × (价 − 期初全价) × floatRatio × longRatio` | 全价(变量名骗人) | 含 `longRatio` | `CloseQty` |
|
||
| 互换页 | `incomeSwapTrade.js:207` | `positionAmount × (价 − 期初全价) × floatRatio` | 全价 | **无 `longRatio`** | `positionAmount=PositionQty×ContractSize` |
|
||
| 后端 | `SwapDealService.cs:1613` | `(价 − PosiGrossPrice) × unwindQty × floatRatio × longRatio` | 全价 | 含 `longRatio` | `unwindQty` |
|
||
| 金标准 | `FrontendCalcReference.CalcUnwind:44` / `CalcIncome:107` | 同上(`CalcIncome` 注释明示"无 longRatio") | 全价 | unwind 有 / income 无 | — |
|
||
|
||
**真正的风险点(已用红测试证明,见 A.1):**
|
||
1. **互换页缺 `longRatio`(多空方向)** —— 这是实打实的 bug。一旦 `PositionType=2`(空头),
|
||
unwind 页与后端会乘 `−1`,而 income 页不乘 → **同一笔空头两页算出相反符号**。
|
||
债券 TRS(国联民生等)普遍支持空头,并非边缘场景。
|
||
2. **数量基准不同**(设计使然,非 bug):平仓页用 `CloseQty`(本次平仓量),互换页用 `PositionQty×ContractSize`
|
||
(剩余持仓量,因互换是全额置换剩余持仓)。语义不同但各自自洽,需业务确认是否期望一致。
|
||
3. **中间取整**:income 无 `×10000/10000` 步骤(`FrontendCalcReference` 注释明示),但末端都保留 2 位,
|
||
干净输入下等价(parity 已证明),属低风险。
|
||
|
||
**共享的 `swapCalc.calcMarkClosePnl`(`swapCalc.js:113`,全价+`longRatio`+`scale`)已存在,
|
||
但两个生产页面都没调用**——仍是"单一可信源已有却不采纳"。
|
||
|
||
**会进库吗?** 会。手动平仓/互换的 `MarkClosePnl` 由前端算好传入,后端**直接存库不重算**
|
||
(`SwapDealService:1500/1991` 的 `ValidateFrontendPnL` 仅"只读告警、不阻断")。
|
||
因此 income 页空头符号错误会直接落到 `swap_flow_event.MarkClosePnl`,并带偏 `FloatPnlSum`/`SwapRealizedPnL`。
|
||
|
||
### A.1 为什么一直没暴露?—— 红测试证据(2026-08-06 补)
|
||
|
||
三层叠加导致这个 bug 长期潜伏:
|
||
1. **校验同源、永远自洽**:`ValidateFrontendPnL` 用 `FrontendCalcReference.CalcIncome` 重算比对,
|
||
而 `CalcIncome` 本身就没 `longRatio`,所以"被校验的前端"和"校验用的公式"完全一致,永远不告警。
|
||
2. **特征化测试全是多头**:`FrontendCalcCharacterizationTest` 的 income 场景 `FC_006~009`
|
||
**全部 `PositionType=1`(多头)**;唯一空头场景 `FC_005` 是 unwind。income 的空头分支从未被触发。
|
||
3. **告警不阻断** + 生产数据里"空头做互换"相对少见,进一步降低暴露概率。
|
||
|
||
**已落地红测试(证明 bug 真实存在 + 证明改动可修复):**
|
||
- `YLErpWeb/fe-tests/markClosePnlShortConsistency.test.js`(jest,与 `parity.test.js` 同风格)
|
||
- `YLErpWeb/fe-tests/_proof_short_income.js`(纯 Node 可跑,无需依赖)
|
||
- 运行结果:多头场景两页一致(PASS,掩盖了问题);**空头场景 `unwind=−300` / `income=+300`(FAIL,符号相反)**;
|
||
给 income 补 `longRatio` 后 `income=−300` 与 unwind 一致(PASS)。
|
||
→ 既证明"当前代码对空头不一致(有问题)",也证明"给 income 补 `longRatio` 即可修复"。
|
||
|
||
### B. 后端硬编码精度魔法数字 —— 与集中常量冲突/不绑定的残留 ⚠️
|
||
|
||
集中常量(`Framework/YLErp.Core/ConsGlobal.cs`):`PriceRound=11`、`SwapDeliveryPriceRound=9`、
|
||
`MoneyRound=2`;模块内 `InterestCalculationPrecision=12`、`EodInterestStoragePrecision=12`。
|
||
|
||
残留的裸数字(不引用上述常量):
|
||
|
||
| 位置 | 写法 | 应参照 | 问题 |
|
||
|------|------|--------|------|
|
||
| `SwapFlowService.cs:116-118` | `Math.Round(price, 4)`(ClosePrice/SettlePrice/ReferencePrice) | `PriceRound=11` / `SwapDeliveryPriceRound=9` | 价格存储 4 位 vs 全系统 9~11 位,**口径不一致** |
|
||
| `SwapDealService.cs:1543` | `unwindPriceFee.ToString("F10")` | — | 硬编码 10 位 |
|
||
| `SwapTradeAutoService.cs:458/460` | `Math.Round(..., 10)`(净均价) | `PriceRound=11` | 差 1 位 |
|
||
| `SwapTradeAutoService.cs:788/1409` | `Math.Round(..., 4)`(费用/平仓费) | `MoneyRound=2` | **费用 4 位 vs 全系统金额 2 位,错配** |
|
||
| 分红链路多处 `Math.Round(...,2)`:`SwapEodPositionService.cs:1647/1711/1713/2003`、`SwapDealService.cs:1658/1882/1883` | 裸 `2` | `ConsGlobal.MoneyRound` | 目前恰等于 2,但属硬编码,`MoneyRound` 一旦配置化即失真 |
|
||
|
||
说明:第 4 行(费用 4 位 vs 金额 2 位)是**真实精度错配**,不是巧合一致;其余价格/净均价是"差 1 位"的隐患。
|
||
|
||
### C. 交叉校验基础设施已建,但未"落地到生产" —— 治理杠杆闲置
|
||
|
||
- `swapCalc.js` 的 `calcUnwind` / `calcIncome`(`:135`/`:170`)是**"参考规格,未接入生产代码"**——
|
||
注释明确写着生产 Vue 只调用 4 个叶子函数,聚合逻辑仍是各页内联。
|
||
- `fe-tests/parity.test.js` + `swapCalc.test.js` 已能冻结 8 个 FC 场景,但只覆盖已迁移的 4 个叶子函数。
|
||
- 这正是"避免反复打补丁"的关键设施,**却没把生产聚合逻辑迁过去**。
|
||
|
||
### D. 横向同类风险(建议扫描,本次未深入)
|
||
|
||
互换之外的 期货 / 期权 / 定价引擎(greeks)/ 估值报告 等模块,
|
||
很可能也存在"前端重算 + 后端重算""硬编码精度"的同构问题,需专项扫描。
|
||
|
||
---
|
||
|
||
## 四、如何避免(规范 / 治理层)
|
||
|
||
1. **单一可信源原则(最高优先级)**
|
||
- 每个金融量只允许**一个权威计算点**:分红→EOD `PosiDividendSum`;盯市→`swapCalc.calcMarkClosePnl`;
|
||
金额聚合→`swapCalc.calcFloatPnlSum` / `calcUnwind` / `calcIncome`。
|
||
- 前端**只展示、不重算**;手动平仓/互换落库时由后端**重算**而非信任前端传值。
|
||
2. **精度集中化(红线)**
|
||
- 任何 `Math.Round` / `ToString("F")` 必须引用 `ConsGlobal.*` 或 `GetStorageDeliveryPriceRound(underlyingInstrumentType, code)`,
|
||
**禁止裸数字**。代码评审把"裸精度数字"列为 blocking 项。
|
||
3. **字段语义单一职责(已定义,需固化)**
|
||
- `MarkClosePnl`=纯价差盯市、`DividendIn`=纯分红、`RealizedPnl`=不重叠分量之和。
|
||
- 写进评审清单 + 用集成测试守护。
|
||
4. **自动守护测试(把已建设施用起来)**
|
||
- 扩展 `parity.test.js` 的"生产表达式逐字抄录 vs `SwapCalc`"模式到**所有关键公式**;
|
||
- 后端补"分红守恒 / 持仓守恒"集成测试:`ΔSwapPositionValue + ΔRealizedPnl == 0`
|
||
(参考分析文档附录 A.2 的 SQL 思路,转成 `SwapEodPositionServiceIntegrationTest` 断言)。
|
||
5. **防回归**:将 `swapCalc.calcUnwind/calcIncome` 接入生产,删除两页内联实现。
|
||
|
||
---
|
||
|
||
## 五、后续逐渐解决的路线(分阶段、低风险)
|
||
|
||
- **阶段 0(已具备)**:`parity.test.js` / `swapCalc.test.js` / `GLMS20260105PartialCloseDividendBugTest.cs` 框架。
|
||
- **阶段 1(低风险、先消除最危险口径)**:
|
||
让 `incomeSwapTrade.js:207` 与 `unwindSwapTrade.js:313` 统一调用 `swapCalc.calcMarkClosePnl`
|
||
(全价 + `longRatio` + `scale`),并确认与后端 `PosiGrossPrice` 口径一致;
|
||
新增"净价≠全价""空头 longRatio"两个 parity 场景守护。**改动小、收益高。**
|
||
- **阶段 2(消除魔法数字)**:
|
||
把 `SwapFlowService` / `SwapTradeAutoService` / `SwapDealService` 里的裸 `Math.Round(...,4/10)`、
|
||
`ToString("F10")` 替换为 `ConsGlobal.*` / `GetStorageDeliveryPriceRound`;分红的裸 `2` 改为 `ConsGlobal.MoneyRound`。
|
||
逐文件改,每改一处跑 parity + 现有单测。
|
||
- **阶段 3(后端收口)**:
|
||
手动平仓/互换落库时,后端对 `MarkClosePnl` / `FloatPnlSum` / `SwapRealizedPnL` **重算**(与 `swapCalc` 金标准一致),
|
||
不再信任前端;同时跑"持仓守恒"SQL 校验历史数据是否已被两页口径差污染。
|
||
- **阶段 4(横向扫描)**:
|
||
用脚本/Explore 扫描 期货、期权、定价引擎、估值报告 模块,查找"前端重算+后端重算""硬编码精度"同类结构,建立清单逐个治理。
|
||
- **阶段 5(文档固化)**:
|
||
更新 `互换分红损益字段语义与重复计算分析.md`,把"已修复 / 待修复"状态机化,作为新人评审清单与回归基线。
|
||
|
||
---
|
||
|
||
## 六、一句话结论 【longRatio 部分已于 d78d1f48 修复】
|
||
|
||
> 最新修复根治了"分红预览"这一条链路的分散计算;~~互换页(income)漏乘 `longRatio`~~
|
||
> **该问题已于 `d78d1f48` 修复**(income 现已含 longRatio,空头符号与平仓一致)。
|
||
>
|
||
> **当前仍残留的同类结构**:
|
||
> - 前端平仓页/互换页的 `MarkClosePnl` 仍**各自内联**,未接入共享的 `swapCalc.calcMarkClosePnl`(单一可信源已有却不采纳);
|
||
> - 后端仍有**费用 4 位 vs 金额 2 位**等硬编码精度错配(`SwapFlowService` Round(4)、`SwapDealService` F10 等)。
|
||
>
|
||
> 治理的关键不是再打补丁,而是**把已建好的 `swapCalc` 单一可信源 + parity 守护真正接入生产**,
|
||
> 并按上述 5 个阶段低风险推进。
|
||
|
||
---
|
||
|
||
## 七、如何确认"income 错 / unwind 对"(而非相反)+ 改动安全性 【✅ 已由 d78d1f48 修复,本章留作论证记录】
|
||
|
||
> 这一章回答一个关键质疑:两页口径不同,凭什么断定是 income 漏了 `longRatio`、而不是 unwind 多算了?
|
||
> 以及:给 income 补 `longRatio` 会不会把正确逻辑改坏、或造成"双重翻转"?
|
||
|
||
> ✅ **更新(2026-08-07)**:本章的论断已被团队采纳并落地。`d78d1f48` 按本章论证给 income 补了 `longRatio`
|
||
> (前端 `incomeSwapTrade.js:208`、后端 `FrontendCalcReference.CalcIncome:108`),并补了空头测试。
|
||
> 本章原"待业务背书的 Working Hypothesis"已成为既成事实,保留作论证记录与防回归参考。
|
||
|
||
> ⚠️ **状态说明(2026-08-07,原文)**:本章关于"income 漏 longRatio → 错 / unwind 对"的论断,原是**待业务背书的 Working Hypothesis**。
|
||
> 后经代码铁证(7.2/7.3)直接采纳修复,见上方更新。
|
||
|
||
### 7.1 两个方向乘子是**独立轴**(这是避免误判的前提)
|
||
|
||
- `floatRatio = PayDirection==1(收取) ? +1 : -1` —— 跟随**收付方向**(`FrontendCalcReference.cs:35`)
|
||
- `longRatio = PositionType==1(多头) ? +1 : -1` —— 跟随**多空方向**(`FrontendCalcReference.cs:36`)
|
||
|
||
二者在本系统里**互不决定**:一个"空头"完全可以 `PayDirection=收取`。
|
||
|
||
### 7.2 裁决性证据:FC_005(空头平仓)冻结基线
|
||
|
||
`UnitTestProject/.../FrontendCalcCharacterizationTest.cs` 的 `FC_005`:
|
||
|
||
```
|
||
输入:PositionType=2(空头) + PayDirection=1(收取)
|
||
注释:floatRatio=1(收取), longRatio=-1(空头)
|
||
期望:MarkClosePnl = 1000×(105−100)×1×(−1) = −5000 (空头涨价=亏损,经济正确)
|
||
```
|
||
|
||
**它证明两件事**:
|
||
1. 空头下 `floatRatio` 仍是 **+1**(方向不靠 `floatRatio` 编码)→ 所以 income 只乘 `floatRatio(+1)` 而漏 `longRatio(-1)`,
|
||
对同一个空头会算出 **+5000**,与权威 −5000 相反 → **income 错、unwind 对**。
|
||
2. 因为两轴独立,**给 income 补 `longRatio` 是纠正、不是双重翻转**("floatRatio 已编码方向"的担忧不成立)。
|
||
|
||
### 7.3 三条互相独立的证据链(任一都足以定罪)
|
||
|
||
| # | 证据 | 来源 | 结论 |
|
||
|---|------|------|------|
|
||
| 1 | 盯市盈亏空头必须翻转符号(空头跌价才盈利)——会计不变式 | 业务数学,独立于代码 | income 漏方向乘子→空头符号必错 |
|
||
| 2 | 后端平仓结算 `:1620` 重算并**覆写** `MarkClosePnl=…*floatRatio*longRatio`(入库存后端口径);`SwapIncome:1980-2013` **不重算**、原样存前端值 | `SwapDealService.cs` | 系统自身的权威定义含 longRatio,income 偏离它 |
|
||
| 3 | `SwapIncome:1994` 用前端 `SwapRealizedPnl`(由 `MarkClosePnl` 派生)做 `AddClientCash(-SwapRealizedPnl)` | `SwapDealService.cs` | 空头 income 不仅显示错,**实际现金流方向也错**(非纯展示) |
|
||
|
||
### 7.4 下游是否会"双重翻转 / 补偿性 hack"?
|
||
|
||
- `SwapFlowEventService.cs:589` 对所有流水事件的 `MarkClosePnl/DividendIn/CloseFee/...` **统一取反**(全局视角翻转,firm book→client view),
|
||
**不针对 income 或空头**。修复后 income 与 unwind 走同一套取反,对称性不变 → 安全。
|
||
- 全仓检索仅此一处 `MarkClosePnl=-MarkClosePnl`,无针对 income/空头的补偿性符号翻转 → 不存在"加了 longRatio 反而翻错"的 hack。
|
||
|
||
### 7.5 改动安全性论证(如何确保不影响现有正确逻辑)
|
||
|
||
修复 = 给 income 的 `MarkClosePnl`(含 `FrontendCalcReference.CalcIncome:107`)补 `× longRatio`,使其与权威 unwind 公式口径一致。
|
||
|
||
1. **静态保证(零回归)**:对所有**多头**(现有 `FC_006~009` 及全部生产多头 income)`longRatio=+1`,
|
||
乘积不变 → 数值**逐位相同**,现有正确行为完全不动。改动是"对多头的恒等变换 + 对空头的纠错",是严格的超集。
|
||
2. **回归护栏**:
|
||
- 跑现有 `FC_001~009` + `SwapFrontendPnlValidateTest` + `SwapIncomeScenarioTest` → 全绿(均为多头,不受影响)。
|
||
- 新增 `FC_010`(空头 income,`PayDirection=1, PositionType=2`)镜像 `FC_005`,锁定纠正后行为,并断言 `income(空头)==unwind(空头)`(parity)。
|
||
- jest 红测试 `markClosePnlShortConsistency.test.js` 的"空头"用例由 FAIL 转 PASS,多头用例保持 PASS。
|
||
3. **剩余风险(非逻辑正确性,需业务决策)**:
|
||
- **历史脏数据**:过去"空头+互换"事件已用错符号落库;修复后新事件正确,跨时间对比会出现不连续。需决定:回溯校正(改 `MarkClosePnl`+重算 `SwapRealizedPnl`+对账 `AddClientCash` 历史)还是标注留痕。
|
||
- **改动范围**:严格限定在 income 页 `:207` 与 `CalcIncome:107` 补 `longRatio`,切忌顺手改 `floatRatio` 或其他页面。
|
||
4. **前置确认**:建议先让业务/量化签字"income 的 `MarkClosePnl` 应含多空方向(与平仓一致)",再动手——因为结论虽由代码+基线铁证支撑,但涉及客户现金流,需业务背书。
|
||
|
||
---
|
||
|
||
## 八、当前行为实录与待确认项(2026-08-07)【8.1 longRatio 部分已由 d78d1f48 修复】
|
||
|
||
> 本章**只记录"代码现在实际怎么做"**(可验证事实),并明确列出"哪些还无法判定谁对"。
|
||
> 与第七章(论断层)的区别:第七章给出了"income 错 / unwind 对"的论证,但该论断**尚未取得业务/量化背书**,
|
||
> 且涉及"价差基准是否应在收益结算后滚动"这一更基础的产品定义。因此把它们在本章降格为"待确认假设",
|
||
> 先如实记录现状,避免过早定性。
|
||
> 文档定位:**当前不修改代码、不加测试,仅留痕**。正确性待业务/量化逐项确认后再回填。
|
||
>
|
||
> ✅ **更新(2026-08-07)**:8.1 关于"income 不含 longRatio"的实录**已过时**——`d78d1f48` 已补 longRatio。
|
||
> 8.2(价差基准不滚动)、8.4(待确认问题清单中除 longRatio 外的价差基准/多次结算问题)**仍待业务确认**。
|
||
|
||
### 8.1 收益结算(income)页当前行为实录 【✅ longRatio 部分已修复】
|
||
|
||
- **界面入口**:交易详情页头部操作区「**收益结算**」按钮(权限 `交易管理_收益互换`),打开 `/swaptrade2/SwapIncome/`
|
||
(`SwapIncome.cshtml` + `incomeSwapTrade.js`)。与「**平仓**」按钮(`SwapUnwind.cshtml`)外观相似,
|
||
但**没有平仓比例、没有事件日期**——本质是"期间结算、头寸保留",而非关闭头寸。
|
||
- **价差损益 `MarkClosePnl` 当前公式(代码事实)**:
|
||
- `incomeSwapTrade.js:104` `initPosiGrossPrice = this.floatPosition.PosiGrossPrice`
|
||
- `:199-200` `floatRatio = PayDirection==1 ? 1 : -1`;`longRatio = PositionType==1 ? 1 : -1`
|
||
- `:208` `MarkClosePnl = positionAmount × (deliveryPrice − initPosiGrossPrice) × floatRatio × longRatio`
|
||
(`positionAmount = PositionQty × ContractSize`,`deliveryPrice = getStorageDeliveryPrice()`)
|
||
- 后端同口径 `FrontendCalcReference.CalcIncome:100,108`:**已含 `longRatio`**(`d78d1f48` 修复)。
|
||
- **事实结论**(已更新):income 页当前**已含 `floatRatio × longRatio`**(`d78d1f48`),与 unwind/后端口径一致。
|
||
原文"只用 floatRatio,未乘 longRatio"的描述**已失效**。
|
||
|
||
### 8.2 价差基准 `swap_position.PosiGrossPrice` 当前行为实录
|
||
|
||
- **开仓时**:`PosiGrossPrice = TradingAmountAvg`(成交/期初全价,`SwapTradeService.cs:396`),规范名 `EntryDirtyPrice`
|
||
(`FrontendCalcReference.cs:15/156`)。
|
||
- **收益结算后**:`UpdateInitalPosition`(`SwapDealService.cs:2276-2281`)对"互换"分支**只更新费用 `PosiTradingFee`,不改 `PosiGrossPrice`**;
|
||
`SwapIncome`(`:2005`)直接落库、**不调用** `UpdateInitalPosition`。
|
||
- **平仓后**:`SaveSwapDealInternal`(`:2212`)只改 `Quantity/PositionQty`,不改动基准价含义。
|
||
- **EOD 日终**:`SwapEodPositionService` 操作的是快照表 `eod_swap_position`,其 `PosiGrossPrice` 为成本基准、
|
||
靠 `eod.Clone()` 跨日结转(`CopyEodPosition:1688`),每日行情价只写入 `UnderlyingPrice`(`:1716`),**不覆盖成本基准**。
|
||
活表 `swap_position.PosiGrossPrice` 最终由 EOD 回写(`UpdateSwapPosition:62` / `UpdateSwapPositionWithRealTime:85`),
|
||
但回写的仍是"成本基准"而非当日行情价。
|
||
- **事实结论**:`swap_position.PosiGrossPrice` 在持仓生命周期内**恒等于开仓期初价 P0,从不滚动到上一次结算价**。
|
||
|
||
### 8.3 已确认事实 vs 待确认假设 对照
|
||
|
||
| 项 | 已确认事实(代码可验证) | 待确认假设(需业务/量化背书) |
|
||
|----|--------------------------|------------------------------|
|
||
| income 是否含 `longRatio` | ✅ **已含**(`d78d1f48` 修复,`incomeSwapTrade.js:208` / `CalcIncome:108`) | ~~是否应该含~~ —— 已按第七章论证采纳修复,不再待确认 |
|
||
| 空头 income 符号 | ✅ **已修正**(`d78d1f48`,空头涨价=亏损,与平仓一致) | ~~是否是错误~~ —— 已确认是 bug 并修复 |
|
||
| 价差基准是否滚动 | 当前**不滚动**(恒为 P0) | 收益结算应是"增量(从上次结算价)"还是"绝对(从 P0)"?——**产品定义未确认** |
|
||
| 多次收益结算重复计入 | 当前若对**同一开放持仓做多次收益结算**,每次都按 (当前价−P0) 计,**首段会被重复计入**(数学推导) | 业务实际是否允许/发生过"同一持仓多次收益结算"?——**需生产数据确认** |
|
||
| 下游现金流 | `SwapIncome:2007` `AddClientCash(-SwapRealizedPnl)` 用前端值记账(code 实证) | 若公式口径需改,历史已落库金额是否需追溯校正? |
|
||
|
||
### 8.4 待确认问题清单(请业务 / 量化 / 产品逐项答复)
|
||
|
||
1. **空头收益结算的符号约定**:空头 TRS 做期间结算,客户现金流方向应是"空头涨价=亏"(与平仓一致,即含 `longRatio`)还是相反?
|
||
请给出业务样例与会计分录。
|
||
2. **收益结算的价差口径**:期间结算的 `MarkClosePnl` 应是"本段增量"(结算后把持仓基准滚动到本次结算价)还是"从开仓期初价累计"?
|
||
两者在"单次结算后随即平仓"时数值相同,但在**多次结算 / 结算后仍保留持仓**时相差巨大。
|
||
3. **是否存在"同一开放持仓多次收益结算"的真实业务场景**?若有,当前"不滚动基准"会导致重复计入,必须修;
|
||
若无(每次结算后即关仓),则当前行为无害、仅作防御性加固。
|
||
4. **多空与收付是否允许非对角组合**(`多头+收取` / `空头+支付`)?若存在,则"收支/多空"两字段非冗余、必须分别存储;
|
||
若不存在(vanilla TRS 永远反向),则可视为冗余但当前仍各存各的。
|
||
|
||
### 8.5 验证脚本(用于把"待确认"转为"已确认")
|
||
|
||
```sql
|
||
-- (a) 是否存在「空头 + 走收益结算/互换路径」且 MarkClosePnl 非 0 的事件
|
||
SELECT trade_id, position_id, event_type, position_type, pay_direction,
|
||
mark_close_pnl, value_date
|
||
FROM swap_event
|
||
WHERE position_type = 2
|
||
AND event_type IN ('互换','结息') -- 按实际枚举值调整
|
||
AND mark_close_pnl <> 0
|
||
ORDER BY value_date DESC;
|
||
|
||
-- (b) 同一持仓是否被多次收益结算(判断是否触发"基准不滚动 → 重复计入")
|
||
SELECT position_id, COUNT(*) cnt, MIN(value_date) first_dt, MAX(value_date) last_dt
|
||
FROM swap_event
|
||
WHERE event_type IN ('互换','结息') -- 按实际枚举值调整
|
||
GROUP BY position_id
|
||
HAVING COUNT(*) > 1
|
||
ORDER BY cnt DESC;
|
||
```
|
||
- 若 (a) 返回 0 行 → 当前无"空头收益结算"样本,longRatio 不一致问题**未实际触发**;
|
||
- 若 (b) 返回 0 行 → 当前业务每次结算后即关仓,"基准不滚动"**无害**;
|
||
- 若 (b) 有行 → 立即按"结算后把 `swap_position.PosiGrossPrice` 滚动到本次结算价"评估修复。
|
||
|
||
### 8.6 多轮回归未暴露的原因(实证,非推断)【第1点已由 d78d1f48 补测试填补】
|
||
|
||
1. **覆盖空洞**:`FrontendCalcCharacterizationTest` 的 income 场景 `FC_006~009` **全是 `PositionType=1`(多头)**;
|
||
唯一空头场景 `FC_005` 是 unwind。income 的空头分支从未被构造。
|
||
✅ **已填补**(`d78d1f48`):新增 `收取空头_价格上涨_应为亏损` 测试(C#)+ jest 对应用例。
|
||
2. **校验同源**:`ValidateFrontendPnL`(`SwapDealService.cs:2004`)用 `BuildFrontendValidationDiffs` 以**同一 `CalcIncome`(也无 longRatio)**
|
||
重算比对,前端错值 == 后端重算 → diff 恒为 0 → 永不告警("预言机与被测代码共享同一 bug"盲区)。
|
||
3. **真实数据隐形**:golden `dividend_trade_1875/1891.json` 中,空头块为 `swap_position` 持仓行(`MarkClosePnl=0`),非结息事件;
|
||
真实样本从未走空头 income。
|
||
4. **多头恒等变换**:income 漏的是 `longRatio`,而多头 `longRatio=+1` 是恒等变换,故整个多头组合
|
||
(≈100% 真实数据 + 100% 回归)下两页数值一致,bug 不可见。
|
||
|
||
> 注:第 1–4 点解释的是"longRatio 不一致为何没被抓到"。而 8.2/8.4 的"基准不滚动"问题,
|
||
> 即便被测也需"多次收益结算"样本才能触发,现有单笔 income / 单笔 close 测试同样覆盖不到——属另一类盲区。
|