Files
zszq-trs/项目文档/互换计算口径一致性_残留问题与治理路线.md
hjhan 66a97e03ef fix(swap): 复利重置日=平仓日时 calcLast 不再跳过 FR007 取价
根因(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 状态更新
2026-08-07 20:43:16 +08:00

360 lines
27 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.
# 互换(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×(105100)×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` | 系统自身的权威定义含 longRatioincome 偏离它 |
| 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 不可见。
> 注:第 14 点解释的是"longRatio 不一致为何没被抓到"。而 8.2/8.4 的"基准不滚动"问题,
> 即便被测也需"多次收益结算"样本才能触发,现有单笔 income / 单笔 close 测试同样覆盖不到——属另一类盲区。