Files
zhixing-system/.trellis/spec/backend/market-data-sync.md
T

7.2 KiB
Raw Blame History

市场数据同步代码规格

Scenario: Tushare qfq 日线与每日指标一次性同步

1. Scope / Trigger

  • 触发:新增 modules/market_data bounded context、Alembic PostgreSQL schema、Tushare/CSV/Compose Job 集成。
  • 目标:外部调度器每次启动一个进程,向 PostgreSQL 写入当前沪深非 ST A 股的六年 qfq 日线和按交易日归属的 daily_basic,并发布可恢复 CSV。
  • 边界:FastAPI 生命周期不启动定时器;本上下文不实现具体选股策略、分钟行情或退市成员资格回溯。

2. Signatures

  • 应用用例:SyncMarketData.execute(command: SyncMarketDataCommand | None = None) -> SyncBatchSummary。
  • CLI:market-data-sync [--initialize | --retry-batch-id <id>] [--trade-date YYYY-MM-DD]。
  • 数据源端口:fetch_stocks()、fetch_open_dates(start, end)、fetch_bars(ts_code, window)、fetch_daily_basic(trade_date)。
  • TushareAdapter.from_token 必须保存 ts.pro_api(token) 返回的 client,并通过 ts.pro_bar(api=client, ...) 调用 qfq 行情;不得依赖 Tushare 模块级全局 token。
  • 数据库事实表:market_stock、market_daily_bar(ts_code, trade_date)、market_daily_basic(ts_code, trade_date);同步审计表:market_sync_batch、market_sync_item。

3. Contracts

  • 配置入口只允许 bootstrap.config.Settings 读取环境变量:
    • ZHIXING_DATABASE_URL
    • ZHIXING_TUSHARE_TOKEN
    • ZHIXING_MARKET_DATA_CSV_ROOT
    • ZHIXING_MARKET_DATA_COVERAGE_THRESHOLD,默认 0.99
    • ZHIXING_MARKET_DATA_MAX_RETRIES、ZHIXING_MARKET_DATA_RETRY_BACKOFF_SECONDS、ZHIXING_MARKET_DATA_REQUEST_INTERVAL_SECONDS
    • ZHIXING_MARKET_DATA_ADVISORY_LOCK_KEY
  • ZHIXING_DATABASE_URL 使用普通 postgresql://... 形式供 Psycopg 3 直接连接;Alembic/SQLAlchemy 在边界处转换为 postgresql+psycopg://...,不得丢失 sslmode 等 query 参数。
  • 开发 Compose 使用项目自带 PostgreSQL;生产 Compose 不声明内部 PostgreSQL 服务,server、migrate 和 market-sync 连接外部 Docker 网络 1panel-network。server 同时保留 Compose 默认网络以供 web 访问。
  • 当前生产 PostgreSQL 在 1panel-network 上的稳定别名为 postgresql,服务端 SSL 为关闭状态,生产连接串使用 sslmode=disable;数据库用户必须具备 public schema 的 CREATE 权限以执行 Alembic 迁移。
  • 股票 qfq 快照路径为 bars/<ts_code>.csv;每日指标路径为 daily-basic/<YYYY>/<YYYYMMDD>.csv;当前股票主数据为 stock-basic/current.csv。
  • market_daily_bar 的唯一键是 (ts_code, trade_date),source_adj 必须是 qfq;所有价格、金额和比率使用有限 NUMERIC/Decimal。
  • Tushare 返回的空字符串、None、浮点或字符串 NaN 映射为数据库 NULL;正负无穷和其他非有限数值必须拒绝,不能写入事实表。
  • SyncBatchSummary 至少返回 batch_id、目标交易日、窗口、状态、目标数、有效数、覆盖率、策略资格、插入数、更新数、未变化数和失败列表。
  • 退出码:0 表示覆盖率达标的成功批次;2 表示部分成功或覆盖率不足;1 表示失败或没有可用成功结果。
  • 单股票流程必须保持“临时 CSV → 指纹比较 → PostgreSQL 事务 → os.replace 发布”顺序;数据库异常不得替换正式 CSV。

4. Validation & Error Matrix

条件 行为
非 SH/SZ、非上市、ST/退市名称或北交所股票 从当前目标股票池排除
指定日期不是交易日 批次准备失败,不发起行情写入
qfq 快照重复键、空快照、OHLC 非法 当前股票 item 失败,旧 CSV 和旧库行保持不变
重叠区间指纹相同 只批量插入旧上界之后的新交易日
重叠行字段变化、缺失或快照开始边界异常变化 当前股票执行六年窗口集合式 upsert/删除
daily_basic 没有目标股票行 当前指标日期 item 失败,不发布空正式快照
pro_bar 未绑定 pro_api(token) client 当前股票 item 失败;适配器必须把 client 作为 api 参数传入模块级 ts.pro_bar
daily_basic 可空数值为 NaN 该字段规范化为 NULL;正负无穷仍作为校验错误处理
PostgreSQL/COPY/迁移异常 转换为安全的 repository error;成功对象不回滚,失败对象保留旧发布版本
同一环境已有同步锁 返回 failed 和退出码 1,不执行第二个批次
valid / target < coverage_threshold 不触发选股,批次显式标记 strategy_eligible=false

5. Good/Base/Bad Cases

  • Good:相同六年 qfq 返回重复执行,指纹相同,数据库无无意义 update,正式 CSV 可原子替换,覆盖率为 1。
  • Base:新增一个开市日且历史重叠不变,只 COPY 新日期;批次成功后清理窗口起点之前的数据库行和日期文件。
  • Good:daily_basic 的缺失估值以 NULL 保存,使用 token client 的 pro_bar 能够完成真实 qfq 请求。
  • Bad:一只股票历史 qfq 行变化或数据库事务失败,只修复/失败该股票;其他股票成功结果保留,失败对象正式 CSV 不被替换。

6. Tests Required

  • 领域单元测试:六年窗口边界、输入顺序稳定性、历史值变化、重叠缺行、窗口提前、ST/北交所过滤;断言 SnapshotChange 和股票代码集合。
  • CSV 单元测试:固定表头、临时文件、原子发布、异常/丢弃后旧正式文件仍可读;断言正式文件内容和临时文件清理。
  • 同步编排测试:success、覆盖率、幂等重跑、空指标失败、部分失败和失败 item 重试;断言 batch/item 状态与退出码。
  • Tushare 适配器回归测试:断言 pro_bar 收到由 pro_api(token) 创建的 api client;领域测试断言 NaN -> None 且无穷值被拒绝。
  • PostgreSQL 集成测试:设置 ZHIXING_TEST_DATABASE_URL 后运行 Alembic upgrade,断言五张业务/审计表、唯一键和迁移头;批量 upsert 测试断言 insert/update/unchanged 计数和事务回滚。
  • 质量门禁:uv lock --check、后端 Ruff/Pyright/pytest、前端/root ./dev.sh check 与 ./dev.sh test、四种 Compose config。

7. Wrong vs Correct

Wrong

# 把最新总市值写入六年前每一根 K 线,产生前视偏差。
for bar in bars:
    bar.total_mv = latest_total_mv

Correct

# 行情和估值按各自交易日保存;策略只使用同一目标日的两类事实。
market_daily_bar[(ts_code, trade_date)] = bar
market_daily_basic[(ts_code, trade_date)] = daily_basic

Tushare client 与可空数值

错误:

# 这会绕过 pro_api(token) 返回的 client,依赖未配置的模块级全局 token。
ts.pro_bar(ts_code=ts_code, adj="qfq")

正确:

client = ts.pro_api(token)
ts.pro_bar(api=client, ts_code=ts_code, adj="qfq")

NaN 是供应商对缺失指标的常见表示,应在领域规范化阶段转为 None;inf 和 -inf 不属于缺失值,必须报校验错误。

Warning

:CSV 发布不是数据库事务的一部分。只有数据库事务成功后才能执行原子 os.replace;发布失败必须记录为 item 失败并依靠幂等重试恢复。