diff --git a/项目文档/互换价格字段命名规范决策文档.md b/项目文档/互换价格字段命名规范决策文档.md new file mode 100644 index 00000000..79618e67 --- /dev/null +++ b/项目文档/互换价格字段命名规范决策文档.md @@ -0,0 +1,291 @@ +# 互换价格字段命名规范决策 + +> 本文档是**命名规范的决策与论证**,回答"应该叫什么、为什么这么叫"。 +> 现状梳理(字段流向、存储显示规则)见姊妹文档 [互换交易价格字段存储与显示规范.md](互换交易价格字段存储与显示规范.md)。 +> 适用范围:互换业务(收益互换)涉及的价格、方向、时点字段。期权/远期业务字段不在本文档范围。 + +--- + +## 一、为什么命名必须先定下来 + +现状是**同一概念多套名字、名字与含义颠倒**,已经反复造成 Bug。三条最严重的证据: + +1. **`PosiNetPrice` 名为"Net",实为"含费全价"**。真正的净价是 `PosiNetNoFeePrice`。连现状文档(《互换交易价格字段存储与显示规范》)都把它误标成"期初净价"——文档作者都被字段名骗了。 +2. **两套并行命名无对应关系**:持仓表 `PosiNetPrice` ↔ 流水表 `TradingAmountFeeAvg`,持仓表 `PosiGrossPrice` ↔ 流水表 `TradingAmountAvg`,名字里没有一处对得上。 +3. **前端同一中文标签绑不同字段**:`成交净价` 一处绑 `TradingAmountNet`,另一处绑 `TradingAmountNetAvg`;`期初标的价格` 录入页绑 `PosiGrossPrice`,平仓明细页绑 `PosiNetPrice`。 + +命名不定下来,后续每个改动都要反复核对数据流向,且极易引入新 Bug。本文档的目的是**把每个维度的命名一次性钉死,给出对比理由**。 + +--- + +## 二、三个维度的决策 + +互换价格有三个正交维度,必须分别命名: + +| 维度 | 取值 | 现状混乱 | +|---|---|---| +| **A. 价格种类** | 全价 / 净价 | Net/Gross 颠倒;Posi vs TradingAmount 两套 | +| **B. 是否含交易费** | 含费 / 不含费 | Fee 标记位置在两套体系里相反 | +| **C. 时点** | 期初 / 期末 / 盯市 | Start/Begin/End/Initial/Final/Spot/Open 散布,无统一规则 | + +外加一个**方向维度**(收取方/支付方),目前用中文枚举,trade_swap 老表用 Get/Pay。 + +下面逐项决策。 + +--- + +## 三、维度 A:价格种类 —— 采用 Clean / Dirty + +### 候选对比 + +| 方案 | 优点 | 缺点 | 评价 | +|---|---|---|---| +| **`Clean / Dirty`** | 债券行业标准术语;歧义最小;与中债估值表 `dirty_price_close`/`net_price`、量化库常见命名一致 | 非 OTDer 可能需要学习成本 | **✅ 推荐** | +| `Net / Gross`(现状持仓表) | 简短 | **本项目已颠倒**(PosiNetPrice 是含费全价);Net/Gross 在金融里本身就有"净值/毛额"歧义,不专指债券净价全价 | ❌ 已证明不可行 | +| `Clean / Full` | Full 含义尚可 | 不如 Dirty 标准;中债表用 dirty,不统一 | △ | +| `净价 / 全价`(中文直译) | 业务人员熟悉 | C# 字段用中文不合规;与现有英文体系混搭更乱 | ❌ | + +### 决策 + +**全价用 `Dirty`,净价用 `Clean`。** + +理由: +1. **行业术语,零歧义**。Clean/Dirty 是债券领域全世界通用的无歧义术语(clean price = 不含应计利息,dirty price = 含应计利息)。 +2. **与数据源一致**。录入默认价格来源 `china_bond_valuation` 表用的就是 `dirty_price_close`(日终估值全价)和 `net_price`(估价净值)。字段命名与数据源对齐,减少心智负担。 +3. **现状 Net/Gross 已被证明失败**。它在本项目里语义颠倒,且 Net 在不同字段里含义不同(PosiNetPrice 的 Net = 含费,PosiNetNoFeePrice 的 Net = 净价),是混乱根源,必须废弃。 + +### 与"费"维度组合后的完整 4 象限见第五节。 + +--- + +## 四、维度 B:费用 —— 采用 Fee / NoFee + +### 先澄清:"费"是什么 + +**"费" = 交易费用 / 佣金(TradingFee)**,由 `TradeFeeHelper.CalcPriceWithFee(price, fee, qty)` 计算:`含费价 = 不含费价 + 单位佣金`。它**不是**应计利息(应计利息体现在 Clean/Dirty 维度),也**不是**利息腿费用。 + +### 候选对比 + +| 方案 | 优点 | 缺点 | 评价 | +|---|---|---|---| +| **`Fee / NoFee`(后缀)** | 现状已在用(PosiNetNoFeePrice/TradingAmountFeeAvg);直白 | — | **✅ 推荐**(沿用现有约定,零迁移成本) | +| `Gross / Net`(用 Net 表示含费) | — | 与维度 A 的 Clean/Dirty 严重冲突,两套词打架 | ❌ | +| `WithFee / WithoutFee` | 更明确 | 太长;后缀冗余 | △ | +| `PreFee / PostFee` | 金融衍生品常见 | 偏衍生品语境,互换收益端用着别扭 | △ | + +### 决策 + +**含费用 `Fee` 后缀,不含费用 `NoFee` 后缀。** + +理由:现状流水表已经是这套(`TradingAmountAvg` 不含费 / `TradingAmountFeeAvg` 含费 / `TradingAmountNetAvg` 净价不含费 / `TradingAmountNetFeeAvg` 净价含费),**这个约定本身没问题**,问题是持仓表用了另一套颠倒的 Posi 命名。统一到 Fee/NoFee 即可。 + +--- + +## 五、维度 A+B 组合:价格 4 象限最终命名 + +### 决策表 + +| 业务含义 | **规范命名** | 现状持仓表 | 现状流水表 | +|---|---|---|---| +| 全价 · 不含费 | `XxxDirtyPrice` | `PosiGrossPrice` | `TradingAmountAvg` | +| 全价 · 含费 | `XxxDirtyFeePrice` | `PosiNetPrice` ⚠️ | `TradingAmountFeeAvg` | +| 净价 · 不含费 | `XxxCleanPrice` | `PosiNetNoFeePrice` | `TradingAmountNetAvg` | +| 净价 · 含费 | `XxxCleanFeePrice` | `PosiNetFeePrice` | `TradingAmountNetFeeAvg` | + +其中 `Xxx` 是时点前缀(见第六节)。 + +### 命名规则一句话 + +> **`{时点}{Dirty|Clean}{Fee|}{Price}`** —— Dirty/Clean 选价格种类,Fee 出现表示含费、不出现表示不含费。 + +例:`EntryDirtyPrice`(期初全价不含费)、`EntryDirtyFeePrice`(期初全价含费)、`ExitCleanPrice`(期末净价不含费)。 + +--- + +## 六、维度 C:时点 —— 采用 Entry / Exit(+ Market) + +这是你重点要求对比的部分。先看现状,再逐词对比。 + +### 现状:时点词散布(统计自实体字段) + +| 现状词 | 出现次数(实体字段) | 用在什么字段 | +|---|---|---| +| `Start` | 36 | StartDate(起始日) | +| `Spot` | 28+4 | SpotPrice(期初价,期权/远期)、GetSpotPrice/PaySpotPrice(trade_swap) | +| `Open` | 3 | OpenPrice、OpenTradePrice(开仓价) | +| `Init/Initial` | 4 | InitYtm、InitialSpotPrice([NotMapped]) | +| `Final` | 10 | FinalPrice(期末价)、GetFinalPrice/PayFinalPrice | +| `End` | 12 | EndDate(结束日) | +| `Close` | 21 | ClosePrice(收盘价,行情侧) | +| `Maturity` | 14 | MaturityDate(到期日) | +| `Entry/Exit` | **0** | 未使用 | + +**现状问题**:表达"期初价"用了 `Spot`、`Open`、`Init`、`Initial` 四种;表达"期末价"用了 `Final`、`End`、`Close` 三种;且 `Close` 还和"行情收盘价"语义重叠。 + +### 候选词逐个对比 + +#### 期初价(开仓成本价)候选 + +| 方案 | 含义直觉 | 优点 | 缺点 | 评价 | +|---|---|---|---|---| +| **`Entry`** | 入场/建仓 | 交易术语,明确"进入持仓那一刻";与 `Exit` 对称;**不与现有任何日期/价格词冲突** | 全新词,需迁移 | **✅ 推荐** | +| `Start` | 开始 | 与 `StartDate`(起始日)一致,延续性最好 | **与日期混淆**:`StartPrice` 容易被误读成"起始日当天的价格"而非"建仓成本价";价格 vs 日期语义纠缠 | △ | +| `Begin` | 开始 | 同 Start | 同 Start;且与 Start 二选一造成二次混乱(现已有 Start,再加 Begin 是雪上加霜) | ❌ | +| `Initial` / `Init` | 初始 | 语义尚可 | `Init` 是缩写不规整;现状 `InitYtm` 已用,但 `InitialPrice` 太长;**Init/Initial 两种并存**(现状就有 `InitYtm` 和 `InitialSpotPrice`)本身就是混乱证据 | ❌ | +| `Spot` | 即期 | 现状 trade_swap 在用 | **语义错误**:Spot 在金融里指"即期(spot vs forward)",不是"期初";互换里强行用 Spot 表示期初是误用;且与"即期价格"概念冲突 | ❌ | +| `Open` | 开仓 | 有"开仓"意味 | `OpenPrice` 在期权语境指"开盘价"(open=开盘 vs close=收盘),互换用 Open 会与行情术语打架 | ❌ | + +#### 期末价(平仓/到期价)候选 + +| 方案 | 含义直觉 | 优点 | 缺点 | 评价 | +|---|---|---|---|---| +| **`Exit`** | 出场/了结 | 与 Entry 对称;明确"退出持仓那一刻";**不与 Close/Maturity 冲突** | 全新词,需迁移 | **✅ 推荐** | +| `Final` | 最终 | 现状在用(FinalPrice) | `Final` 在期权语境常指"到期(final/expiration)",与互换"平仓了结"语义有偏差;且 Final 给人"永远结束"的感觉,但互换可能多次部分平仓(每次都是 Exit) | △ | +| `End` | 结束 | 简短 | 与 `EndDate`(结束日)混淆,同 Start 的问题 | ❌ | +| `Close` | 收盘 | 行情侧在用(ClosePrice) | **语义已被占用**:Close = 收盘行情价;互换平仓价用 Close 会和"每日盯市收盘价"混淆。这是现状混乱之一 | ❌ | +| `Maturity` | 到期 | 债券标准术语 | 只适用于"持有到期",不适用于"提前平仓";互换平仓≠到期 | ❌(仅 MaturityDate 用于到期日) | + +### 决策 + +**期初用 `Entry`,期末用 `Exit`,每日盯市用 `Market`。** + +### 为什么是 Entry/Exit 而不是其他 —— 三条核心理由 + +**理由 1:Entry/Exit 是唯一一组"互相对称、且不与现有词冲突"的方案。** + +| 对称组 | 冲突情况 | +|---|---| +| **Entry / Exit** | ✅ 零冲突,现有代码 0 处使用 | +| Start / End | ❌ 与 StartDate/EndDate 日期字段冲突 | +| Open / Close | ❌ 与行情开盘价/收盘价冲突 | +| Initial / Final | △ Initial 太长,Final 偏"到期"语义 | +| Begin / End | ❌ 与 Start 重复造词 | + +互换价格的核心心智模型是"**Entry(建仓成本)vs Exit(了结价格)**"——盈亏 = Exit − Entry。Entry/Exit 直接表达这个对偶关系,其他词都带着别的语义包袱。 + +**理由 2:互换"多次部分平仓"语义下,Entry/Exit 比 Start/End/Final 更准确。** + +互换持仓期间可以有多次部分平仓(每次部分平仓都是一次 Exit),不是"一次性 Start→End"。`Final`(最终)暗示"只有最后一次",`Start`/`End` 暗示"单一起止点",都不符合"多次 Exit"的现实。`Exit` 天然支持"多次出场"语义(每次平仓 = 一次 Exit)。 + +**理由 3:与"盯市价"清晰分层。** + +| 时点 | 命名 | 含义 | +|---|---|---| +| 建仓时 | `Entry*` | 成本价(固定) | +| 平仓时 | `Exit*` | 了结成交价(每次平仓一个) | +| 持仓期间每日 | `Market*` | 盯市价(随行情变动) | + +三者职责清晰:Entry 是成本基线,Exit 是实现盈亏的时点,Market 是浮动盈亏的基准。现状 `UnderlyingPrice`(盯市价)改为 `MarketDirtyPrice` 后,与 Entry/Exit 形成 `Entry / Market / Exit` 完整时点链。 + +### 收益率字段同理 + +| 含义 | 规范命名 | 现状 | +|---|---|---| +| 期初 YTM | `EntryYtm` | InitYtm | +| 平仓 YTM | `ExitYtm` | ytm / Ytm | + +--- + +## 七、维度 D:方向 —— 收取/支付方的命名 + +### 现状 + +| 位置 | 现状命名 | 问题 | +|---|---|---| +| `SwapDirectionEnum`(V2 在用) | `收取 / 支付`(中文) | C# 字段用中文不合规;与英文代码混排突兀 | +| `trade_swap`(老表,已弃用互换主流程) | `Get* / Pay*`(如 GetSpotPrice/PaySpotPrice) | Get 语义弱,不像金融术语;你已指出这是老互换遗留 | +| `PositionTypeFlag`(多空) | `Long / Short` | ✅ 这个没问题,标准术语 | +| 业务文档/前端 | "收取方/支付方"、"固定端/浮动端" | 中文展示 OK | + +### 候选对比(英文方向词) + +| 方案 | 优点 | 缺点 | 评价 | +|---|---|---|---| +| **`Rcv / Pay`** | 金融工程标准(IRS 收付固定方叫 Receive/Pay);FINCAD/Bloomberg/量化库通用;3字母缩写简洁 | Rcv 需要新人熟悉"是 receive 缩写" | **✅ 推荐** | +| `Receive / Pay` | 不缩写,更明确 | Receive 较长,字段名 `ReceiveDirtyPrice` 偏长 | △ | +| `Get / Pay`(现状老表) | 现状 | Get 不是金融术语,语义弱("获得"太泛);与 Pay 不对称(Pay 是金融词,Get 是口语) | ❌ | +| `Long / Short` | 标准术语 | **语义不同**:Long/Short 是多头/空头(PositionType),不是收取/支付方(SwapDirection);互换里收取方可能是空头。**两者不能混用** | ❌(维度不同) | +| `Buy / Sell` | 通用 | 互换不是买卖关系,是交换现金流;Buy/Sell 易误导 | ❌ | + +### 决策 + +**英文方向词用 `Rcv / Pay`,仅用于代码字段名(如 `RcvEntryDirtyPrice`);枚举值和前端展示保持中文"收取/支付"。** + +理由: +1. **金融标准**。利率互换(IRS)行业里固定方/浮动方标准叫法就是 Receive fixed / Pay fixed,FINCAD、Bloomberg、主流量化库都用 Rcv/Pay。 +2. **纠正老表 Get 的错误**。trade_swap 的 `Get/Pay` 里 Get 不是金融词,是早期随手起的,正好借命名规范统一为 Rcv/Pay。 +3. **不与 Long/Short 冲突**。Rcv/Pay 是 SwapDirection(收取/支付哪一端现金流),Long/Short 是 PositionType(多空),两个维度独立,不能互相替代。 +4. **中文枚举保留**。`SwapDirectionEnum.收取/支付` 已被业务文档、前端、监管报表广泛使用,强行改英文枚举值成本高且无收益。只在新英文字段名里用 Rcv/Pay。 + +> 注:当前 V2 互换主流程(swap_position/eod_swap_position)已经不按"收取/支付腿"存价格(价格存在不分腿的持仓记录里,方向用 `PosiDirection` 字段标记),所以 `Rcv/Pay` 前缀**只在需要显式区分双腿的场景**(如 trade_swap 录入、双边互换)使用,不是所有字段都加。 + +--- + +## 八、最终命名总表(一锤定音) + +### 8.1 价格 4 象限(含时点前缀) + +| 业务含义 | 规范命名 | 现状(持仓/流水) | +|---|---|---| +| 期初全价·不含费 | `EntryDirtyPrice` | PosiGrossPrice / — | +| 期初全价·含费 | `EntryDirtyFeePrice` | PosiNetPrice ⚠️ / — | +| 期初净价·不含费 | `EntryCleanPrice` | PosiNetNoFeePrice / — | +| 期初净价·含费 | `EntryCleanFeePrice` | PosiNetFeePrice / — | +| 期末/平仓全价·不含费 | `ExitDirtyPrice` | — / TradingAmountAvg | +| 期末/平仓全价·含费 | `ExitDirtyFeePrice` | — / TradingAmountFeeAvg | +| 期末/平仓净价·不含费 | `ExitCleanPrice` | — / TradingAmountNetAvg | +| 期末/平仓净价·含费 | `ExitCleanFeePrice` | — / TradingAmountNetFeeAvg | +| 盯市全价 | `MarketDirtyPrice` | UnderlyingPrice | +| 期初收益率 | `EntryYtm` | InitYtm | +| 期末收益率 | `ExitYtm` | ytm | + +### 8.2 方向 + +| 业务含义 | 代码字段名 | 枚举/展示值 | +|---|---|---| +| 收取方 | `Rcv*`(如需区分腿) | 收取(SwapDirectionEnum.收取) | +| 支付方 | `Pay*`(如需区分腿) | 支付(SwapDirectionEnum.支付) | +| 多头 | `Long`(PositionTypeFlag.Long) | 多头 | +| 空头 | `Short`(PositionTypeFlag.Short) | 空头 | + +### 8.3 命名规则速记 + +> **价格 = `{Entry|Exit|Market}{Dirty|Clean}{Fee|}Price`** +> **方向 = `Rcv|Pay`(SwapDirection)与 `Long|Short`(PositionType)是两个独立维度,不混用** +> **收益率 = `{Entry|Exit}Ytm`** + +--- + +## 九、落地策略(渐进式,不一次性改) + +直接全局重命名风险极高(DB 列 + EF 实体 + ~15 服务类 + ~20 前端文件 + 交易确认书 + Excel 导入)。建议分阶段: + +| 阶段 | 动作 | 风险 | 产出 | +|---|---|---|---| +| **0. 文档定调(本文档)** | 命名规则钉死,团队达成共识 | 零 | 本文档 | +| **1. 注释止血** | 在现状最混乱的字段(PosiNetPrice/PosiGrossPrice)加 `[Obsolete]` + XML 注释,写明真实含义与规范名 | 零 | 防止再被字段名误导 | +| **2. 前端统一取值封装** | 新增 `swapPriceHelper.js`,把分散的 `item.PosiGrossPrice` 等收敛成 `getEntryDirtyPrice(item)`;先修"同标签绑不同字段"的混乱 | 低 | 前端混乱点消除 | +| **3. 新代码强制规范** | 新增字段/新功能必须用规范命名;对外 API 新增 DTO 用规范名,内部旧字段靠 Mapper 转换 | 低 | 增量规范化 | +| **4. 大重构(慎重)** | DB 列重命名 + EF 映射 + 全量替换,配合数据迁移。建议趁大版本升级做 | 高 | 彻底统一 | + +**阶段 1 最该立刻做**:给 `PosiNetPrice`(含费全价,非净价)和 `PosiGrossPrice`(不含费全价)加注释,因为连现状文档都被它们误导过。 + +--- + +## 附录:现状混乱全证据清单(决策依据) + +> 以下每条都有代码/DDL/前端文件佐证,是上述决策的事实基础。 + +| # | 混乱点 | 关键证据位置 | +|---|---|---| +| 1 | PosiNetPrice 名为 Net 实为含费全价 | `SwapTradeService.cs:372-373` 赋值:PosiNetPrice←TradingAmountFeeAvg(含费均价) | +| 2 | PosiGrossPrice 名为 Gross 实为不含费全价 | 同上:PosiGrossPrice←TradingAmountAvg(不含费均价) | +| 3 | 同字段两表 DDL 注释矛盾 | PosiNetPrice:swap_position 注释"持仓净价",eod_swap_position 注释"期初标的价格" | +| 4 | 持仓表/流水表两套命名无对应 | Posi*(Net/Gross)vs TradingAmount*(Avg/FeeAvg),名字对不上 | +| 5 | 前端同标签绑不同字段 | `成交净价` 绑 TradingAmountNet(303) 也绑 TradingAmountNetAvg(508),见 SwapflowList.js | +| 6 | 作者自曝字段语义错位 | `SwapIncome.cshtml:155`、`SwapDealService.cs:279` 注释:"TradingAmountNetAvg 字段名为期末语义,实装期初值" | +| 7 | trade_swap 第三套命名 | GetSpotPrice/PaySpotPrice/GetFinalPrice/PayFinalPrice,与 Posi* 平行无对应 | +| 8 | 时点词散布 | 期初价用了 Spot/Open/Init/Initial 四种;期末价用 Final/End/Close 三种 | +| 9 | Close 语义被占用 | ClosePrice=行情收盘价 vs 互换平仓价混用 | +| 10 | Get 不是金融术语 | trade_swap 的 Get/Pay,Get 语义弱,与 Pay 不对称 | +| 11 | 现状文档自身被误导 | 《互换交易价格字段存储与显示规范》把 PosiNetPrice 标为"期初净价",实为含费全价 |