c3e53fbef3
- scripts/data_platform/_archive/legacy/: 归档20个独立探针/诊断/旧降级脚本(零引用验证) - docs/archive/data/: 归档17个数据相关旧设计/plan/report(保留fusion spec作深读) - docs/data-platform/README.md: 数据层单一权威记录(8节:架构/布局/源/管线/铁律/API/缺口/待办) - 删除 _mootdx_depth_result.txt - Phase2待办: 15m灌库链+旧回填import链(有测试/wrapper依赖,VPS schtask确认后归档)
222 lines
12 KiB
Markdown
222 lines
12 KiB
Markdown
# P0 数据补全实现计划(历史成份股 + ETF 全市场 + 退市 K 线)
|
||
|
||
> **For agentic workers:** 用 superpowers:subagent-driven-development 或 executing-plans 执行。Steps 用 `[ ]` 跟踪。
|
||
|
||
**Goal:** 补齐治幸存者偏差 + 策略核心缺口三类数据,落到 VPS 本地。
|
||
|
||
**Architecture:** 各源采集脚本 → staging parquet → 验证探针 → 合并主库;baostock 单登录守 48000/天;dbbardata 不动。
|
||
|
||
**Tech Stack:** python3.10 / akshare / baostock / xtquant(xtdata)/ pandas / pyarrow / sqlite3
|
||
|
||
---
|
||
|
||
## Global Constraints(所有 task 隐含)
|
||
|
||
- **baostock 单进程单登录**,不并发(防黑名单,日 ≤48000 query)
|
||
- **直连不走代理**:`$env:http_proxy=''; $env:https_proxy=''; $env:all_proxy=''`
|
||
- **dbbardata 不破坏**:只 INSERT OR REPLACE `daily_baostock_full` / 新表,不动 dbbardata 既有行
|
||
- **优先 baostock + miniQMT(xtdata)**
|
||
- **staging → 验证探针 → 合并主库**(用户铁律,不直接写主库)
|
||
- Windows VPS 49.232.102.198,`C:\Python310\python.exe -X utf8`,schtasks `/ru SYSTEM`
|
||
- 输出根:`C:\sanguo_vnpy_v2\data\`
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
| 文件 | 责任 |
|
||
|---|---|
|
||
| `scripts/data_platform/index_const_hist_download.py`(新) | 历史成份股采集(akshare 国证 + 新浪 + baostock 补时点) |
|
||
| `scripts/data_platform/build_daily_from_xtdata.py`(改 :40) | ETF universe 扩展(一次性全量) |
|
||
| `scripts/data_platform/daily_update_xtdata.py`(改 :114) | ETF 每日增量 universe |
|
||
| `scripts/data_platform/baostock_delisted_download.py`(新) | 退市股列表 + K 线采集 |
|
||
| `scripts/data_platform/import_delisted_to_db.py`(新) | 退市 K 线灌 `daily_baostock_full` |
|
||
| 各 `*_wrapper.ps1` + schtask | 部署 |
|
||
|
||
---
|
||
|
||
## Task 1: 历史成份股采集(治幸存者偏差)
|
||
|
||
**Files:** Create `scripts/data_platform/index_const_hist_download.py`;Output `data/index_const_hist/{code}.parquet`
|
||
|
||
**Interfaces:**
|
||
- Consumes: akshare `index_detail_hist_cni(symbol)` + `index_detail_hist_adjust_cni(symbol)`(国证源);新浪 `vII_HistoryComponent`(pandas.read_html, gb2312);baostock `query_hs300/zz500/sz50_stocks(date)`
|
||
- Produces: `data/index_const_hist/{code}.parquet`(列:`updateDate/index_code/code/code_name/adjust_type`);并集 = 曾经入选集
|
||
|
||
**指数清单:**
|
||
- 深证/国证(akshare 国证源):399001 / 399006 / 399101 / 399005 / 399330
|
||
- 中证(新浪):000852(中证1000)/ 932000(中证2000)/ 000300(交叉校验)/ 000016(上证50)
|
||
- baostock 已有(300/500/50 在 `bs_index_constituent`):Task1 补时点序列到同 schema
|
||
|
||
- [ ] **1.1 探针:akshare 国证源 hist 版**
|
||
```python
|
||
import akshare as ak
|
||
df = ak.index_detail_hist_cni(symbol="399101") # 历史样本(日期/样本代码/权重)
|
||
print(df.columns.tolist(), len(df), df.head(3))
|
||
adj = ak.index_detail_hist_adjust_cni(symbol="399101") # 调样记录(调整类型 OLD/+/-)
|
||
print(adj.columns.tolist(), len(adj))
|
||
```
|
||
预期:hist 有日期+样本+权重;adjust 有调整类型。**陷阱:必须 hist 版**(`index_detail_cni` 非 hist 版 2025-11-25 起只近期);`ak.index_stock_hist` 已下线别用。
|
||
|
||
- [ ] **1.2 探针:新浪中证历史成份**
|
||
```python
|
||
import pandas as pd
|
||
url = "http://vip.stock.finance.sina.com.cn/corp/go.php/vII_HistoryComponent/indexid/000852.phtml"
|
||
df = pd.read_html(url, encoding="gb2312")[0]
|
||
print(df.columns.tolist(), len(df), df.head(3))
|
||
```
|
||
预期:品种代码/品种名称/纳入日期/剔除日期(空=至今在列),含 *ST/退市股。
|
||
|
||
- [ ] **1.3 实现 `index_const_hist_download.py`**:三路采集 → 统一 schema(`updateDate/index_code/code/code_name/adjust_type`)→ 写 `data/index_const_hist/{code}.parquet`。串行 `time.sleep(0.8)`(akshare/新浪防封),单进程。环境变量 `BS_INDEX_HIST_OUT_DIR` 覆盖默认 Mac 路径(同 Day1 wrapper 模式)。
|
||
|
||
- [ ] **1.4 验证探针**:每指数 parquet 行数 + 抽样 3 行;**幸存者偏差校验** = 并集 `distinct code` 数 > 当前成份股数(证明含被踢股,例如 399101 并集 > 958 当前)。
|
||
|
||
- [ ] **1.5 wrapper + schtask**:`index_const_hist_wrapper.ps1`(设 OUT_DIR + utf8 + unset proxy + log);schtask `sanguo-index-hist` `/sc monthly /mo 2`(半年度调样后,6/12 月)`/ru SYSTEM`。
|
||
|
||
- [ ] **1.6 commit**:`git add scripts/data_platform/index_const_hist_download.py scripts/data_platform/index_const_hist_wrapper.ps1 && git commit -m "feat(data): 历史成份股采集(治幸存者偏差,国证+新浪+baostock)"`
|
||
|
||
---
|
||
|
||
## Task 2: ETF 全市场日线
|
||
|
||
**Files:** Modify `scripts/data_platform/build_daily_from_xtdata.py:40` + `daily_update_xtdata.py:114`
|
||
|
||
**Interfaces:**
|
||
- Consumes: xtdata `get_stock_list_in_sector('沪深A股'/'沪深ETF'/'沪深基金')` + `get_market_data_ex(dividend_type='front')`
|
||
- Produces: 全市场 ETF(~1000 只)日线**前复权**,落 parquet/dbbardata(复用现有 xtdata 管线)
|
||
|
||
- [ ] **2.1 探针:ETF universe + 1 只 K 线**
|
||
```python
|
||
from xtquant import xtdata as xd
|
||
etf = xd.get_stock_list_in_sector('沪深ETF') or []
|
||
fund = xd.get_stock_list_in_sector('沪深基金') or []
|
||
a = xd.get_stock_list_in_sector('沪深A股') or []
|
||
u = list(set(a + etf + fund))
|
||
print(f"A={len(a)} ETF={len(etf)} fund={len(fund)} union={len(u)}")
|
||
r = xd.get_market_data_ex([], ['510300.SH'], period='1d',
|
||
start_time='20240101', end_time='20260721', dividend_type='front')
|
||
df = r.get('510300.SH')
|
||
print('510300 bars:', 0 if df is None else len(df), '| tail close:', None if df is None else df['close'].iloc[-1])
|
||
```
|
||
预期:ETF ~1000,union > A 股数;510300 前复权日线有值,close 非 NaN。
|
||
|
||
- [ ] **2.2 改 universe**:`build_daily_from_xtdata.py:40` 和 `daily_update_xtdata.py:114` 把
|
||
```python
|
||
u = xd.get_stock_list_in_sector("沪深A股") or []
|
||
```
|
||
改为
|
||
```python
|
||
u = list(set(
|
||
(xd.get_stock_list_in_sector("沪深A股") or []) +
|
||
(xd.get_stock_list_in_sector("沪深ETF") or []) +
|
||
(xd.get_stock_list_in_sector("沪深基金") or [])
|
||
))
|
||
```
|
||
保留 `dividend_type='front'`(前复权,§13 默认)。
|
||
|
||
- [ ] **2.3 全量下载 ETF**:跑改后的 `build_daily_from_xtdata.py`(走现有 xtdata 管线,**无限流**)→ parquet。
|
||
|
||
- [ ] **2.4 验证**:ETF 数 + 抽样(510300/513050/159919)+ 前复权 close 非 NaN + 日期范围。
|
||
|
||
- [ ] **2.5 schtask**:复用 `sanguo-daily-update`(universe 扩展后自动含 ETF,无需新 schtask)。
|
||
|
||
- [ ] **2.6 commit**:`git commit -m "feat(data): ETF 全市场日线(xtdata universe 扩展+前复权)"`
|
||
|
||
---
|
||
|
||
## Task 3: 退市股 K 线(反幸存者偏差核心)
|
||
|
||
**Files:** Create `scripts/data_platform/baostock_delisted_download.py` + `import_delisted_to_db.py`;Output → `daily_baostock_full`
|
||
|
||
**Interfaces:**
|
||
- Consumes: baostock `query_all_stock(day)` + `query_stock_basic(code)`(status + 退市日期)+ `query_history_k_data_plus(code, fields, adjustflag=3)`
|
||
- Produces: 退市股 K 线 INSERT OR REPLACE `daily_baostock_full`(18 列,复用 `parse_baostock_code`)
|
||
|
||
**范围:** 近 5 年退市(退市日期 ≥ 2021;守 48000/天;退市股分天跑)
|
||
|
||
- [ ] **3.1 探针:退市股列表字段**
|
||
```python
|
||
import baostock as bs, pandas as pd
|
||
bs.login()
|
||
rs = bs.query_all_stock(day="2026-07-18")
|
||
rows = []
|
||
while (rs.error_code == '0') & rs.next():
|
||
rows.append(rs.get_row_data())
|
||
df = pd.DataFrame(rows, columns=rs.fields)
|
||
print('query_all_stock fields:', rs.fields, '| rows:', len(df))
|
||
rs2 = bs.query_stock_basic(code="sh.600000")
|
||
b = []
|
||
while (rs2.error_code == '0') & rs2.next():
|
||
b.append(rs2.get_row_data())
|
||
print('query_stock_basic fields:', rs2.fields, '| sample:', b[0] if b else None)
|
||
bs.logout()
|
||
```
|
||
预期:`query_stock_basic` 含 `type`(1股)/`status`(1上市 0退市)/`outDate`(退市日期)。筛 `status=0 & outDate>='2021-01-01'`。
|
||
|
||
- [ ] **3.2 实现 `baostock_delisted_download.py`**:
|
||
- 遍历全 code(或 `query_all_stock` 多日并集)→ `query_stock_basic` 筛 `status=0 & outDate>='2021-01-01'` → 退市股列表
|
||
- 逐只 `query_history_k_data_plus(code, start_date='1990-01-01', end_date=outDate, fields=18字段, adjustflag=3)` → staging `data/delisted_kline/{code}.parquet`
|
||
- 单进程单登录,`time.sleep` 守预算,marker 断点续传(复用 Day1 模板),DAILY_LIMIT 计数器
|
||
|
||
- [ ] **3.3 `import_delisted_to_db.py`**:staging → INSERT OR REPLACE `daily_baostock_full`(复用 `parse_baostock_code` sh.600000→600000+SH + `executemany`,WAL + busy_timeout=60000,同 `import_baostock_to_db.py`)。**dbbardata 不碰**。
|
||
|
||
- [ ] **3.4 验证探针**:退市股数 + 抽样(某退市股 K 线行数 + max(date) ≤ 退市日)+ `daily_baostock_full` 行数增量 + distinct symbol 增量。
|
||
|
||
- [ ] **3.5 wrapper + schtask**:`baostock_delisted_wrapper.ps1`;schtask `sanguo-delisted` `/sc monthly /ru SYSTEM`(月度,守 48000,错开 day2b 02:00 + bs-daily-increment 17:00)。
|
||
|
||
- [ ] **3.6 commit**:`git commit -m "feat(data): 退市股 K 线采集(baostock,反幸存者偏差)"`
|
||
|
||
---
|
||
|
||
## Task 4: baostock 日增量 → daily_baostock_full(#7 daily_update_static)
|
||
|
||
> **串行约束**:本 task 与 Task3 都用 baostock 长会话,**必须串行**(Task3 probe → Task3 执行 → Task4),不可并发(防黑名单)。
|
||
|
||
**Files:** Create `scripts/data_platform/daily_update_static.py` + `daily_update_static_wrapper.ps1`
|
||
|
||
**背景:** 现有 `daily_update_xtdata.py` 只产 parquet 不灌 `daily_baostock_full`(已知 gap,memory `db-primary-parquet-fallback` 记录)。本 task 补 baostock 日线的**每日增量灌库**。
|
||
|
||
**Interfaces:**
|
||
- Consumes: baostock `query_stock_basic`(全 A,type=1 含退市,复用 `baostock_daily_fullmarket_download.py:fetch_all_stocks`)+ `query_history_k_data_plus`(LOOKBACK 窗口,adjustflag=3 raw,18 字段同 `BS_FIELDS`)
|
||
- Produces: staging `data/daily_baostock_increment/{YYYYMMDD}/{code}.{exc}_daily.parquet`(审计)→ 同进程 INSERT OR REPLACE `daily_baostock_full`(复用 `parse_baostock_code`+executemany+WAL+busy_timeout,同 `import_baostock_to_db.py`)
|
||
|
||
**设计(LOOKBACK 窗口 + 幂等,不同于全量 marker 模式):**
|
||
- **不用 marker 断点续传**(全量才需要;增量每日全量重拉最近 N 天)
|
||
- `LOOKBACK_DAYS=7`(覆盖周末/节假日;baostock 日终更新,17:00 跑时当日 bar 已就绪)
|
||
- 每只 1 query → 5537 query/run ≪ 48000/天 ✅(留足余量给 day2b/Task3)
|
||
- `sleep 0.4s × 5537 ≈ 37min`(17:00 schtask 可接受)
|
||
- `QUERY_COUNT` 计数器 + `DAILY_LIMIT=40000` 防御(复用全量脚本模式)
|
||
- **一脚本贯通**:download LOOKBACK → staging parquet(审计)→ in-memory df → executemany INSERT OR REPLACE(幂等,重复跑同一天安全,`drop_duplicates keep last` 不需要因 PK+OR REPLACE 天然去重)
|
||
|
||
**Steps:**
|
||
- [ ] **4.1 探针(可选,Day1 已实证 query_history_k_data_plus 可用)**:ssh VPS 跑 1 只近 7 天确认接口 + 当日 bar 就绪
|
||
- [ ] **4.2 写 `daily_update_static.py`**:自包含,结构
|
||
- `unset proxy` + `socket.setdefaulttimeout(30)`(同全量脚本,baostock 坑)
|
||
- `_login_once`/`_relogin`/`fetch_all_stocks`/`fetch_one_daily`/`parse_baostock_code` 复用(可 import 或复制;优先 from `baostock_daily_fullmarket_download import ...`,注意 `QUERY_COUNT` global 需在同进程)
|
||
- `LOOKBACK` 窗口:`start=today-7, end=today`
|
||
- 主循环:逐只 `fetch_one_daily` → staging parquet → 累积 df → 每 100 只 `executemany INSERT OR REPLACE`(WAL+busy_timeout=60000)
|
||
- `QUERY_COUNT`/`DAILY_LIMIT`/断路器/定期重登 复用
|
||
- 结束 verify:抽样 3 只 `max(date) ≈ today`、当日新增行数
|
||
- 环境变量 `BS_INCREMENT_OUT_DIR`/`DB_PATH` 覆盖默认(同 Day1 wrapper 模式适配 Win)
|
||
- [ ] **4.3 小样本**:`--limit 10` 跑 10 只,确认 staging 有行 + DB 抽样 max(date)≈today
|
||
- [ ] **4.4 全量跑**:5537 只,守预算
|
||
- [ ] **4.5 wrapper + schtask**:`daily_update_static_wrapper.ps1`(unset proxy+utf8+OUT_DIR+log);schtask `sanguo-bs-daily-increment` `/sc daily /st 17:00 /ru SYSTEM`(错开 daily-update 16:30 + day2b 02:00 + Task3 月度)
|
||
- [ ] **4.6 commit**:`git commit -m "feat(data): baostock 日增量灌库 daily_update_static(#7 gap 补)"`
|
||
|
||
---
|
||
|
||
## Self-Review
|
||
|
||
- **Spec 覆盖**:Task1→spec §4 成份股行 + §8 P0.1;Task2→§4 ETF 行 + §8 P0.2;Task3→§4 退市行 + §8 P0.3 ✅
|
||
- **Placeholder 扫描**:无 TBD/TODO;采集脚本给接口+探针+schema,实现者按骨架写完整(采集脚本完整代码由执行 agent 基于 接口/schema/陷阱 产出)✅
|
||
- **类型一致**:`index_const_hist` schema 各源统一;`daily_baostock_full` 18 列复用 `import_baostock_to_db.py` 的 `parse_baostock_code`+executemany ✅
|
||
- **陷阱纳入**:`ak.index_stock_hist` 下线(1.1 标注)/ csindex SPA 无历史(用国证+新浪)/ 新浪 gb2312(1.2)/ hist 版必须(1.1)✅
|
||
|
||
---
|
||
|
||
## Execution Handoff
|
||
|
||
计划存 `docs/superpowers/plans/2026-07-21-data-fusion-p0.md`。执行方式:
|
||
1. **Subagent-Driven**(推荐):每 Task 派 fresh agent + task 间 review
|
||
2. **Inline**:本 session 批量执行 + checkpoint
|