diff options
Diffstat (limited to 'docs/stock-milestone-survey.md')
| -rw-r--r-- | docs/stock-milestone-survey.md | 147 |
1 files changed, 147 insertions, 0 deletions
diff --git a/docs/stock-milestone-survey.md b/docs/stock-milestone-survey.md new file mode 100644 index 0000000..ce96c5e --- /dev/null +++ b/docs/stock-milestone-survey.md @@ -0,0 +1,147 @@ +# 直接股票持仓里程碑调研 + +状态:**只调研,不实施**。本文给出实现「直接股票持仓里程碑」所需的既有限件、schema 现状与缺口、UI 范围,以及 AKShare 股票日行情最小探针设计。所有代码位置均为当前 `main`(`877e031`)实际位置,引用格式 `file:line`。 + +验收口径来自 `docs/design.md`(§4.5/§5.2/§10.1)、`docs/implementation-plan.md`(§10.1)与 `README.md`;环境与 Git 口径见 `docs/leader-environment-unblock.md`。 + +## 0. 结论摘要 + +- **已有可复用垂直切片**:股票买入/卖出/持仓/现金账本/行情探针/估值/UI 均已实现并有回归(`stock_trades`、`stock_positions`、`stock_sells`、`stock_cashflow_events`、`instrument_snapshots`;`POST /api/funds/{id}/stock-trades` 等)。 +- **里程碑真正缺口(设计已要求、当前未落地)**: + 1. 实际价与复权价分列(设计 `market_price_observation.price_kind`,`docs/design.md:107`);现有 `instrument_snapshots` 只有单一 `price`,无 `adjust`/`price_kind`。 + 2. 公司行动(除权除息/送股/配股)与停牌持久化;`stock_cashflow_events` 仅 `buy/sell/dividend`,无 split/rights。 + 3. 股票交易日历与每 instrument 的 `trading_rule` 版本(设计 `docs/design.md:182`、`docs/implementation-plan.md:227`)。 + 4. point-in-time 估值重算与无前视验收(当前估值按 `asOfDate` 取快照 + 实时探针兜底,未做严格 `published_at <= cutoff`)。 + 5. 代码搜索/选择(当前 UI 为自由输入六位码)。 +- **最小探针结论**:既有 collector 已能拉股票日线(`akshare_collector.py:461`,三源 fallback),里程碑探针只需**验证**交易日真实性、实际/复权列差异、公司行动跳变、缺失/修订与当日可用时点;**不需要新增 operation**。 + +## 1. 既有限件(硬约束,不可违背) + +摘自 `docs/design.md` §1/§4.4/§4.5 与 `README.md`: + +- 只用真实数据,缺失显示「未接入/数据不可用」,**不得伪造行情、不得把缺失当 0**(`docs/design.md:273`、`README.md:40`)。 +- 合成数据必须 `is_synthetic=true` 并与真实查询隔离(`docs/design.md:191`)。 +- 金额/价格/份额/净值用 F# `decimal` 与 PostgreSQL `numeric`;对外 JSON 用字符串(`docs/design.md:113`、`docs/design.md:295`)。 +- 写命令要求 `Idempotency-Key`;同 key 同哈希返回原结果,不同哈希 `409 IDEMPOTENCY_CONFLICT`;写入在单事务内(`docs/design.md:186`、`docs/design.md:366`)。 +- point-in-time:`published_at`/`first_seen_at` 分离,历史回填标 `backfilled` 且不得前视(`docs/design.md:220`)。 +- 不接真实券商/下单(`docs/design.md:17`、`docs/design.md:305`)。 +- 运行时数据库、缓存、凭证、构建产物不入 Git(`docs/design.md:317`)。 +- 前端中文,每个图表/持仓必须展示数据日期、来源状态、是否含合成(`docs/design.md:299`)。 + +## 2. 现有实现(可直接复用) + +### 2.1 Schema(`src/FundLab.Api/Persistence.fs`) + +| 表 | 位置 | 关键列 | 备注 | +| --- | --- | --- | --- | +| `instruments` | `Persistence.fs:853` | `code`(`^[0-9]{6}$`)、`name`、`fund_type`、`source`、`source_revision`、`first_seen_at`/`last_seen_at` | 基金/股票共用,无显式 `kind` | +| `stock_trades` | `Persistence.fs:1131` | `fund_id`、`instrument_code`、`stock_name`、`quantity`、`price`、`cost_cash`、`is_synthetic`、`executed_at` | 买入不可变事实 | +| `stock_trade_idempotencies` | `Persistence.fs:1143` | `idempotency_key`、`request_hash`、`trade_id`、`fund_id` | 幂等 | +| `stock_positions` | `Persistence.fs:1151` | `(fund_id, instrument_code)` 主键、`quantity`、`cost_cash`、`last_traded_at` | 持仓聚合(成本口径,非市值) | +| `stock_sells` | `Persistence.fs:1161` | `quantity`、`price`、`fee_amount`、`proceeds`、`is_synthetic`、`executed_at` | 卖出 | +| `stock_cashflow_events` | `Persistence.fs:1174` | `event_type IN ('buy','sell','dividend')`、`event_date`、`quantity`、`amount` | 无 split/rights | +| `instrument_snapshots` | `Persistence.fs:1296` | `(instrument_code, asset_class, snapshot_date)`、`price`、`source*` | `asset_class IN ('stock','bond')`,**无 adjust/price_kind** | + +设计对照缺口:`docs/design.md:107` 的 `market_price_observation(trade_date, price, price_kind, source, payload_hash)` 与 `docs/design.md:110` 的 `valuation_snapshot` **均未落地**;当前以 `instrument_snapshots` 与「按需计算」替代。 + +### 2.2 写入路径与 `stock_buy` 账本 + +- `FundRepository.CreateStockTrade`:`Persistence.fs:5951`。 + - 校验六位码/正数量/正价格:`Persistence.fs:5957`。 + - advisory lock + 幂等重放/冲突:`Persistence.fs:5970`–`5992`。 + - 可选现金借记(3d-34 起默认 `true`):`Persistence.fs:6002`–`6014`;余额不足回滚并返回 `StockTradeInsufficientFunds`:`Persistence.fs:6016`。 + - 写 `stock_trades` + 幂等记录:`Persistence.fs:6038`。 + - **写 `stock_buy` 账本事件**:`Persistence.fs:6041`–`6051`(`insertCashLedgerEvent … CashLedger.StockBuy … "stock_buy"`)。 + - upsert `stock_positions`:`Persistence.fs:6053`–`6080`。 +- `BackfillCashLedger` 的 `stock_buy` 补事件批(含 pre-3d-34 缺借记一次性修正,3d-39/`877e031`):`Persistence.fs:7240` 起。 +- HTTP 处理器 `createStockTrade`:`App.fs:2480`。 + - 实时行情取价:`App.fs:2499`;停牌 → `400 STOCK_SUSPENDED`:`App.fs:2504`;无价 → 数据错误:`App.fs:2511`。 + - 幂等冲突 `409`:`App.fs:2535`;余额不足 `400 INSUFFICIENT_FUNDS`:`App.fs:2539`。 +- 路由:`POST /api/funds/%s/stock-trades`(`App.fs:3670`)、`stock-sells`(`3671`)、`GET stock-positions`(`3672`)、`stock-cashflows`(`3673`/`3674`)。 + +### 2.3 行情探针与估值 + +- 路由:`GET /api/market/stock-quote`(`App.fs:3630`)、`GET /api/market/stock-daily`(`App.fs:3631`)、`GET /api/funds/%s/valuation`(`App.fs:3682`)。 +- 探针接口 `MarketProbes`(`App.fs:604`–`611`:`StockQuotes`/`StockDaily`);`MarketDataService.FetchStockDaily`:`MarketDataService.fs:215`(调用 `--operation stock-daily`)。 +- collector:`stock_market`(沪/深/北前缀映射)`akshare_collector.py:446`;`stock_daily`(三源 fallback)`akshare_collector.py:461`;`stock_quote`(`stock_bid_ask_em` → 快照)`akshare_collector.py:514`。 +- 估值处理器 `getFundValuation`:`App.fs:3317`;价格解析 `resolveValuationPrice`(快照优先,实时兜底):`App.fs:3306`;缺失标 `unavailable` 不补零:`App.fs:3406`。 +- 行情刷新把最新日线收盘写入 `instrument_snapshots`:`App.fs:3447`–`3496`。 +- 回归:`tests/FundLab.Api.Tests/StockDailyProbeTests.fs`(升序去重、代码不符拒绝、503/400 边界)、`StockQuoteProbeTests.fs`(名称缺失不伪造、停牌标记、幂等缓存)、`StockTradeTests.fs`/`StockSellTests.fs`/`StockCashflowTests.fs`。 + +### 2.4 UI(`src/FundLab.Web/App.fs`) + +- 股票行情探针面板(代码输入/查行情/查近 5 日日线):`App.fs:6166` 起。 +- 买入 + 股票持仓表 + 卖出:`App.fs:6240`–`6325`(表头「代码/名称/数量/成本/卖出数量/操作」)。 +- 组合估值卡(现金/持仓市值/组合合计/已定价·缺失,含 `priceSource`):`App.fs:6326` 起。 +- API 客户端 `./src/api.js` 导入:`App.fs:1143`–`1156`。 +- QA 场景:`qa/driver/browser-test.js:795`(`stocksScenario`)、`:858`(`stockDailyScenario`)、股票买卖与持仓(`:314`–`322`);桩采集器 `qa/stub/fund-lab-python:48`(`stock-quote`)/`:59`(`stock-daily`)。 + +## 3. Schema 现状与缺口(里程碑必须补) + +| 设计实体/字段 | 现状 | 缺口 | +| --- | --- | --- | +| `market_price_observation.price_kind`(actual/adjusted 分列) | 无;`instrument_snapshots.price` 单一值 | 需新增 `price_kind`/`adjust` 列或新表,估值默认 actual,复权仅用于收益分析 | +| 公司行动(送转/配股/除权除息) | `stock_cashflow_events.event_type` 仅 `buy/sell/dividend` | 缺 split/rights/除权日;复权因子无处存储 | +| 停牌状态 | 仅 `stock_quote` 响应内瞬态 `suspended`(`akshare_collector.py:537`) | 无持久化,估值/UI 无法回溯停牌 | +| 交易日历 | 无 | 需按数据源真实交易日,禁止把最近交易日冒充当天(`docs/design.md:223`) | +| 每 instrument `trading_rule` 版本 | 无 | 需绑定 T+1、最小 100 股、费用、结算、停牌规则(`docs/implementation-plan.md:227`) | +| `valuation_snapshot`(含 source_revision_set) | 按需计算,无快照表 | 需可重算快照以支持 point-in-time 与无前视 | +| 成本 vs 市值 | `stock_positions.cost_cash`(成本) | 市值来自 `instrument_snapshots`/实时价,二者未在同一快照冻结 | + +## 4. UI 范围 + +已有(可复用):行情探针、买入(整数股)、持仓表、卖出、估值卡、`priceSource` 来源标注、缺失/不可用中文提示。 + +里程碑需新增/调整: + +1. 股票代码**搜索/选择**(复用 `GET /api/instruments/search`,当前 UI 是自由输入六位码)。 +2. 持仓**金额或份额**输入(当前仅整数股数),并显示最小交易单位/费用。 +3. 实际价/复权价**口径标注**(`price_kind`)与「净资产/市值」曲线标签区分(`docs/design.md:299`)。 +4. 停牌/非交易日/缺失的**明确状态与原因**(不补零)。 +5. 公司行动(分红/送转)展示与再投资选择。 + +## 5. AKShare 股票日行情最小探针(只调研) + +**目标**:回答 `docs/design.md:266` 与 `docs/implementation-plan.md:226` 的六个问题,不新增 collector operation、不改代码。 + +复用既有 `stock_daily(code, days, adjust)`(`akshare_collector.py:461`),其 provider fallback 顺序: + +1. `ak.stock_zh_a_hist(symbol=code, period="daily", adjust=adjust)` — 东方财富,列 `日期/收盘/成交量/成交额`(`akshare_collector.py:481`)。 +2. `ak.stock_zh_a_daily(symbol="sh600519", adjust=adjust)` — 新浪(`:482`)。 +3. `ak.stock_zh_a_hist_tx(symbol="sh600519")` — 腾讯(`:483`)。 +4. `adjust ∈ {"", "qfq", "hfq"}`,交易所前缀由 `stock_market` 决定(`:446`、`:477`)。 + +建议新增 `scripts/probe_akshare_stock_daily.py`(仅脚本,不接入 API),采样 3–5 只(沪 `600519`、深 `000001`、北 `8xxxxx`),对每只分别以 `adjust=""`/`qfq`/`hfq` 拉取并输出: + +- `date`、`close`、`volume`、`amount`、`adjust`; +- **交易日真实性**:返回日期是否为真实交易日;周末/节假日是否缺行。 +- **停牌**:停牌日是否缺行或 `volume==0`。 +- **实际 vs 复权**:`adjust=""` 与 `qfq`/`hfq` 首尾值差异、复权基准漂移。 +- **公司行动**:除权除息日附近价格跳变是否与 `adjust` 对应。 +- **缺失与修订**:同日重复拉取是否修订历史行;`source_revision` 是否变化。 +- **当日可用时点**:T 日收盘价何时出现(类比基金净值延迟;3d-38 已证明净值端点会滞后)。 + +**判定门槛**:若某类股票在 AKShare 无稳定、可追溯、日期正确的数据,UI/API 必须显示「未接入/数据不可用」,不得合成行情混入真实曲线(`docs/design.md:273`)。 + +## 6. 最小实施切分(建议,非本单范围) + +| 步骤 | 内容 | 依赖 | +| --- | --- | --- | +| S1 | schema:`instrument_snapshots` 增 `price_kind`/`adjust`(或建 `market_price_observation`);停牌与交易日历表 | 迁移 + 回滚测试 | +| S2 | `scripts/probe_akshare_stock_daily.py` + 探针结论 | 真实解释器 | +| S3 | 交易规则:T+1、100 股整手、费用、结算、停牌拒单 | 规则版本表 | +| S4 | 公司行动/分红入账(现金/再投资) | `stock_cashflow_events` 扩展 | +| S5 | UI:搜索/选择、金额/份额输入、口径与状态标注 | S1/S3 | +| S6 | 估值 point-in-time 重算 + 无前视 + 浏览器验收 | S1–S5 | + +## 7. 引用索引(file:line) + +- `src/FundLab.Api/Persistence.fs:853` `instruments`;`:1131` `stock_trades`;`:1143` `stock_trade_idempotencies`;`:1151` `stock_positions`;`:1161` `stock_sells`;`:1174` `stock_cashflow_events`;`:1296` `instrument_snapshots`。 +- `src/FundLab.Api/Persistence.fs:5951` `CreateStockTrade`;`:6002` 借记;`:6041` `stock_buy` 账本事件;`:6053` `stock_positions` upsert;`:7240` `BackfillCashLedger` stock_buy 批。 +- `src/FundLab.Api/App.fs:2480` `createStockTrade`;`:3306` `resolveValuationPrice`;`:3317` `getFundValuation`;`:3670`–`3682` 路由;`:3630`/`:3631` market 探针路由;`:604` `MarketProbes`。 +- `src/FundLab.Api/MarketDataService.fs:215` `FetchStockDaily`。 +- `src/FundLab.Api/akshare_collector.py:446` `stock_market`;`:461` `stock_daily`;`:514` `stock_quote`。 +- `src/FundLab.Web/App.fs:6166` 行情探针面板;`:6240` 买入/持仓/卖出;`:6326` 估值卡;`:1143` api.js 导入。 +- `qa/driver/browser-test.js:795`/`:858` 股票 QA 场景;`qa/stub/fund-lab-python:48`/`:59` 桩采集器。 +- `docs/design.md:107` `market_price_observation`;`:110` `valuation_snapshot`;`:223` 交易日;`:266` 探针清单;`:273` 不可用不合成。 +- `docs/implementation-plan.md:226`–`231` 股票里程碑步骤。 |
