Files
zhixing-system/docs/market-data-sync.md
T

10 KiB

市场数据同步 Job

market-data-sync 是外部调度器触发的一次性任务。FastAPI 进程不包含定时器;PostgreSQL 是策略查询事实源,market-data 卷中的 CSV 只作为 qfq 落地快照和恢复介质。

环境与数据库连接

开发环境由 docker-compose.dev.yml 启动项目自带的独立 PostgreSQL,容器内连接串保持:

ZHIXING_DATABASE_URL=postgresql://zhixing:zhixing@postgres:5432/zhixing

生产环境不启动项目自带 PostgreSQL。现有 1Panel PostgreSQL 容器通过外部 Docker 网络 1panel-network 的稳定别名 postgresql 访问,数据库和用户均使用 zhixing-system。生产 .env 必须显式设置连接串;Compose 不会回退到开发数据库:

ZHIXING_DATABASE_URL=postgresql://zhixing-system:<url-encoded-password>@postgresql:5432/zhixing-system?sslmode=disable

当前服务器执行 SHOW ssl 的结果为 off,因此使用 sslmode=disable 或省略该参数。若日后在 1Panel/PostgreSQL 中启用 TLS,再将它改成 sslmode=require。密码中的 @、:、/ 等字符必须 URL 编码。启用 TLS 后的示例如下:

ZHIXING_DATABASE_URL=postgresql://zhixing-system:<url-encoded-password>@postgresql:5432/zhixing-system?sslmode=require

部署前确认外部网络和数据库容器都在该网络中:

docker network inspect 1panel-network --format '{{json .Containers}}'
docker inspect 1Panel-postgresql-5Fc7 --format '{{json .NetworkSettings.Networks}}'

迁移或同步容器连接成功后,可只输出当前连接是否使用 SSL(不会打印连接串):

docker compose -f docker-compose.prod.yml --profile jobs run --rm --no-deps market-sync \
  python -c 'import psycopg; from zhixing_server.bootstrap.config import get_settings; connection=psycopg.connect(get_settings().database_url); print(connection.execute("SELECT ssl FROM pg_stat_ssl WHERE pid=pg_backend_pid()").fetchone()[0]); connection.close()'

输出 t 表示当前连接使用 SSL,输出 f 表示未使用。本服务器当前应输出 f,与 sslmode=disable 配套;若日后要求 TLS,应先检查 PostgreSQL/1Panel 的 SSL 配置,再改用 sslmode=require。

运行时保留普通 postgresql:// 形式供 Psycopg 3 使用;Alembic/SQLAlchemy 会自动转换为 postgresql+psycopg://,并保留 sslmode 等 query 参数。

首次部署

先准备 .env,至少设置 ZHIXING_TUSHARE_TOKEN、PostgreSQL 凭据和 ZHIXING_DATABASE_URL,然后执行迁移:

docker compose -f docker-compose.prod.yml --profile jobs run --rm migrate

首次初始化会回补六年交易日指标,并为当前沪深非 ST A 股获取六年 qfq 行情:

docker compose -f docker-compose.prod.yml --profile jobs run --rm market-sync --initialize

日常收盘后同步默认解析最近的开市日;也可以显式指定日期:

docker compose -f docker-compose.prod.yml --profile jobs run --rm market-sync --trade-date 2026-08-05

success 返回 0;覆盖率低于配置阈值或出现部分失败返回 2;没有可用成功结果或基础设施失败返回 1。CLI 输出不包含 Tushare token 或数据库密码。

行情阶段默认使用 8 路固定 worker;可通过 ZHIXING_MARKET_DATA_MAX_WORKERS 调低或调高,取值必须 至少为 1。每个 worker 从有上限的 PostgreSQL 连接池借用独立连接,主线程批量写入同步审计并通过 一次集合查询计算覆盖率。所有 worker 共用 Tushare 频控协调器;普通请求保持并发,命中 403、429 或 “访问频繁”等提示时共享 60/120/180 秒冷却窗口。若供应商频控持续发生,先把 worker 降到 1,再通过 --retry-batch-id 只恢复失败对象。

Cron 与重试

宿主机 cron 只负责启动临时容器,不写入容器内部的 crontab。下面的示例每天工作日 18:00 触发;交易日历、唯一约束和 PostgreSQL advisory lock 使周末、节假日、重复触发和重叠触发保持安全:

0 18 * * 1-5 cd /srv/zhixing-system && docker compose -f docker-compose.prod.yml --profile jobs run --rm market-sync >> /var/log/zhixing-market-sync.log 2>&1

部分成功批次的输出会包含 batch_id。修复凭据、网络或数据库问题后,只重试该批次失败的股票和指标日期:

docker compose -f docker-compose.prod.yml --profile jobs run --rm market-sync \
  --retry-batch-id <batch-id>

单只股票按数据库事务和 CSV 发布作为恢复边界。数据库提交失败时正式 CSV 不会替换;CSV 发布失败时数据库写入保持幂等,重试可以再次发布。成功批次完成后,数据库事实表和正式日期快照清理六年窗口起点以前的数据;失败对象保留上一次成功版本。

本地检查

docker compose -f docker-compose.dev.yml config
docker compose -f docker-compose.dev.yml --profile jobs config
docker compose -f docker-compose.prod.yml config
docker compose -f docker-compose.prod.yml --profile jobs config

真实 PostgreSQL 迁移和批量 upsert 集成测试使用 ZHIXING_TEST_DATABASE_URL 显式开启;普通单元测试不会访问网络、Tushare 或数据库。

板块资金雷达 Job

sector-radar-build 同样是外部调度器触发的一次性任务,FastAPI 不会在进程内启动定时器。它只读取 Tushare 的 trade_cal、dc_index、dc_member、stock_basic、suspend_d、daily 和 moneyflow_dc,保存 point-in-time 原始快照与规范化事实,再生成明确标注为“知行独立实现”的版本化指标。生产运行时不请求 OneChartLab。雷达股票范围固定为构建时 stock_basic(list_status=L) 返回的沪深 A 股与有效板块成员的交集;历史回填也采用构建时当前上市股票池,不还原目标日当时已经退市的证券。

开发环境没有 token 时可以检查命令契约,但不能执行真实构建:

cd zhixing-server
uv run sector-radar-build --help

提供 ZHIXING_TUSHARE_TOKEN 并完成迁移后,可构建单日、按交易日顺序回填区间,或从一个 partial/failed publication 的来源检查点继续重试。失败 publication 会复用此前已成功保存的来源组;覆盖率不足的 partial 只刷新被标记为缺口的 daily 或 moneyflow_dc,不会全量重采:

docker compose -f docker-compose.prod.yml --profile jobs run --rm sector-radar-build \
  --trade-date 2026-08-28

docker compose -f docker-compose.prod.yml --profile jobs run --rm sector-radar-build \
  --start-date 2026-08-18 --end-date 2026-08-28

docker compose -f docker-compose.prod.yml --profile jobs run --rm sector-radar-build \
  --retry-publication-id <publication-id>

moneyflow_dc 先按交易日拉取全市场快照;即使首批达到 6000 行,也会根据上述候选股票检查实际覆盖,并用固定两路 worker 逐只补拉缺失代码。全市场请求、补拉和 retry 在同一 adapter 内共享 ZHIXING_SECTOR_RADAR_REQUEST_INTERVAL_SECONDS(默认 0.2 秒)的请求启动间隔;空分片或普通请求重试耗尽会保留为覆盖缺口并形成 partial,来源返回错误日期、错误代码、重复键或分片再次触顶则整次构建失败。该限流只在单进程 adapter 内生效,生产调度仍不得让 market-data-sync 与 sector-radar-build 重叠运行。

重复输入通过内容 hash 复用已有成功发布,不产生无意义修订;同一目标日由 PostgreSQL advisory lock 阻止并发构建。success 或 unchanged 返回 0,覆盖率不足的 partial 返回 2,输入、上游、锁或基础设施失败返回 1。partial/failed 会保留审计,但读取端只选择 success 作为 last-good。当前版本只提供手工和外部调度入口,不新增生产 Cron;待真实账号 capability、到达时点和首轮回填验证完成后再单独启用调度。

升级加权评分并重算历史

--recompute 只使用数据库中已有的板块聚合事实、交易日历和源快照,生成新的评分与排名发布;不调用 Tushare,不需要 token,不修改旧发布或历史成员。它与重新拉取上游的普通构建不同,不能和 --retry-publication-id 同用。

在生产项目目录中,将代码更新到包含加权评分的提交,沿用生产 .env,依次执行以下命令;每步成功后再执行下一步。Compose 为不同服务使用不同镜像名,因此除服务端和前端外,也要构建迁移与重算 Job 的新镜像。

# 构建新代码,暂不替换运行中的服务
docker compose -f docker-compose.prod.yml --profile jobs build \
  server web migrate sector-radar-build

# 先增加 nullable weighted_score 列(0010),再启动新服务
docker compose -f docker-compose.prod.yml --profile jobs run --rm --no-deps migrate
docker compose -f docker-compose.prod.yml up -d server web

# 使用新 Job 镜像,按交易日顺序重算已有历史
docker compose -f docker-compose.prod.yml --profile jobs run --rm --no-deps sector-radar-build \
  --recompute --start-date 2026-08-28 --end-date 2026-09-21

这里的日期是示例,应覆盖需要展示的已有历史,区间包含起止日。单日评分需要连续 5 个交易日成交额,波段评分需要连续 10 个交易日流入率;近 5 日波段排名变化还要求更早的对比日也有足够历史并已采用同一算法版本。首次升级建议从最早已有成功发布开始重算整个展示区间。缺少交易日、板块或可比版本时显示“—”,重算不会补造缺失数据。

输出的 outcomes 列出逐日结果;整个命令 status 为 success 或 unchanged 且退出码为 0 表示完成。相同输入重复运行返回 unchanged;失败会保留原 last-good,可修复原因后重跑同一区间。重算期间避免同时启动覆盖相同日期的普通构建或另一个重算任务。

以后只重算某一天,可复用已构建的新 Job 镜像:

docker compose -f docker-compose.prod.yml --profile jobs run --rm --no-deps sector-radar-build \
  --recompute --trade-date 2026-09-21

--no-deps 用于上述已手动完成迁移的流程,避免再次启动依赖服务。sector-radar-build 服务配置了同名 entrypoint,后面直接传 --recompute 等参数即可。