/~learn/s/ece383d9
本文档涵盖需求背景、设计方案、架构图和实现细节,一次性回答「做了什么、为什么、怎么做的」。
将数据拉取和策略推送拆分为两个独立进程,推送时直接读本地 CSV 缓存而不再实时调 API,从根本上消除「API 不稳定导致推送失败」的系统性风险。
Mermaid Live Edit: https://mermaid.live/edit#pako:eNp9k11rE0EUhv_KMFcKTaFtopILYdOmIChKKl7oiozZSbawH2E_FGkKTcEmMdmmommwWm3BtrnQraKWagv5L5Kd3Vz1L3imY6KxqXN1duY57znzntkFnDUVipM4p5mPsyqxHHQ9IxsIlu0-zFukoKJUevZmJn1Pxuzl997SVlD1To43Aq8ZdV6H7VrwtM2-lSCQ8X2RyFdqAvjA32Ctg2hvN2g8h1MUi11FqUk4yFEnqz5QiEMuXESB_xYoVqsGjfVhjUmeUZRuXUPdQy9sfwG9IkpN8U5O6eD95-jrzqgcVlkLnr0DOA5w-HE9bO5E_nbot4bhKdFSAqCfm3XEVtu9pdIo1bgAL_HSBxVW2hfoAKKGIhv_2CbN3k5n_ri2tspda9R_u1ZZCXePhopIE31nqIWEdQNTTstL3LoZMG2aAHODGCRPrXGbPKJoeu7OsBY3qeDaf0n1GxZS8VFSmkmUs1Li7lLif05KCQGda5C4IIqNi4Gy-nL3x0oRSZchodvZDJuvWGsPSp8c17uHNfZhu1f2ov3mmU54PmBRp8zewHylK7xg9YXtEI2yrXLkf-IKR57o8fwpiTctmuZzwmNYp5ZO5hX4FxY4I2NHpTqVcRJCheaIqzkylo1FQInrmHNPjCxOOpZLx7BbgLdMZ-YJTF7HyRzRbNi1TDevDr4KxLhrmno_JW_xUjxe_AXJE2kB
Mermaid Live Edit: https://mermaid.live/edit#pako:eNqNU0Fr2zAY_SvCpxTaFPvowyA4KRtZmZmyHTYPo9qyHWbLQZbbhSTQ3kphZZe0sB22MQpjsKSH0cNG92_iZP9in-w2jcIo88V6fu_Tp_f0eaB5qU81Uwvi9MCLCBeo03QYgifL90JOehGy9ZeOZvPUo1mGdBPxnLk-EcQNqPAiyuu9vqO9qorkg6U-A8bPY4pmk_fF-dXs97f56VSR7UhZuYUrupTrtQ2VNhTaWKMb9qMlLw9T20Db6A66ewSWKzWU-Q5bN2asGDNMlEXpgZsQ_rr0hGrF6dc_h0dqX2z8lzmrDTKgivFx8eWw-HhhopAKN-A0ixi0W3PzuCnl48vi7WR28mlxfW2iOCX-mgpboJp_P5uPLxaTz_PJucLaWO5xdVwcTauD3-vdalgPW1BQhuURMLStGsDPgR3I6Ri5ul_3sn2F3211GoogoYLUxRtxb1f85NlTq7X0Wvx8p2zakRZEnsEcUrhOIg8H737QZYR59F9bYx1tbT2AYVKhUcGdCsKw3GBDxbAoP3SwgodNaGzJUHYJIyGMQ0b26VCGouqKDz-Ks8thmcbyRFUPq11Bq10qF9NfaJnRbUF19Xc8hLzSpKoEwaoQhuCmjVVCG2ubWkJ5Qro-_MYDSUKGEU0gLhOWPg1IHsO1OGwEUpKLFPeZp5mC53RTy3sQMm12CVxQopkBiTP4ytM8jJaoR9iLNE1uS0IuW8n16C-_I2cB
{code}_{k_type_suffix}.csv,例如 399102.SZ_1d.csv
k_type_suffix:1d / 1wk / 1mo / 5m / 15m / 1m
全量覆盖,不是追加。每次拉取 1990-01-01 ~ 今天,返回全量 DataFrame,完整覆写整个 CSV。好处是数据始终完整一致,坏处是每次重复拉全量历史(约 3000~8000 行/品种)。
后续可优化为增量更新:只拉最近 N 天,与已有 CSV 合并后重新写入。
.meta.txt 格式:
fetched_at=2026-05-26T09:35:12
latest_trade_date=2026-05-25
rows=3847
推送时的时效检查只需读这 2 行文本,不需要加载整个 CSV。
DataCacheManager.save() 三道防线:
latest_trade_date 比旧缓存更旧 → 跳过.tmp + os.rename 原子替换,防止断电写坏
择时策略代码池(show_marker.py 的 codes_to_process) + 截面 WATCHLIST,去重后约 66 个品种。定时:08:00(全量)、09:30、12:00、14:00、20:00、02:00。
注意拉取时用 _quick_fetch_one() 直接调 adata API,跳过 tushare(后者不稳定且容易并发超限)。非 ETF 代码(HSI、GLD、QQQ、980092.CNI 等)仍走 fetch_data()。
全市场 ETF 池(CURATED_ETF_POOL),约 76 个品种。
| 文件 | 操作 | 说明 |
| ------ | ------ | ------ |
| data_cache.py | 新建 | DataCacheManager:save/load/get_freshness/find_stale_codes |
| run_data_fetcher.py | 新建 | 独立拉取进程,schedule 定时循环 |
| run_fetch.sh | 新建 | CLI 入口脚本 |
| data_cache/ | 新建目录 | CSV 缓存,已 gitignore |
| show_marker.py | 修改 | 时效检查→get_freshness;回测数据→load;截面引擎→fetch_all_data_from_cache |
| engine.py | 修改 | 新增 fetch_all_data_from_cache() |
| .gitignore | 修改 | 排除 data_cache |
不改的文件:df_loader.py、allocation_utils.py、run_push.sh、所有策略文件。
# 启动数据拉取定时循环(长期运行,建议 nohup 后台)
nohup ./run_fetch.sh > fetch.log 2>&1 &
# 或手动触发单次
./run_fetch.sh -f # 全量(Tier1 + Tier2),跑完退出
./run_fetch.sh -t # 仅 Tier1,跑完退出
# 策略推送定时循环(另一个终端)
nohup .venv/bin/python -m unittest \
adata.aohan.quant.chart_test.show_marker.MarketDataPlotly.test_schedule_push -v \
> push.log 2>&1 &