ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【Python量化系统工程实战 #08】从脚本到生产:量化系统上线 checklist 的最小闭环

【Python量化系统工程实战 #08】从脚本到生产:量化系统上线 checklist 的最小闭环 系列完结篇前 7 篇我们解决了「数据怎么存、自动怎么跑、挂了怎么知道、缺口怎么补」。本篇把脚本变成生产系统的最后 1 公里——环境隔离、日志轮转、数据备份、灰度对比、回滚预案每一项都是「出事时不至于半夜爬起来」的兜底。一、从个人脚本到生产系统的差距很多量化代码跑完回测就上线结果第一周就出各种事故环境同事用的 Python 3.12自己机器是 3.9pandasAPI 行为不一致 → 报错日志脚本跑了两周logs/app.log已经 8 GBtail命令打不开 → 排错要等 5 分钟数据升级版 SQLite schema 后忘了备份旧版本数据没了 → 重新拉 3 天灰度新策略直接替换旧策略3 天后收益率掉 30% → 不知道该不该回滚回滚出问题想回滚但没提前准备备份也不知道回滚要多久上线 checklist 的目标任何动作都有预演任何事故都有兜底。二、本文你将得到什么环境隔离自检脚本5 行检测 venv / 依赖 / 磁盘避免「在我机器能跑」问题日志轮转1 MB × 5 个文件自动切换避免日志膨胀数据备份一行shutil.copy2给 SQLite 打时间戳快照灰度对比新旧策略并行跑一次差异阈值报警回滚预案3 步从「出问题」恢复到「上次能用」三、上线 checklist 总表按重要性排序前 3 项是「不做立马翻车」后 2 项是「做了才稳」#项目风险兜底1环境隔离依赖冲突 / Python 版本错位venv 依赖版本锁2日志轮转日志爆炸 / 排错难RotatingFileHandler3数据备份数据丢失 / 升级翻车时间戳.bak副本4灰度对比新策略不稳定v1 vs v2 并行观察5回滚预案出问题没法恢复备份覆盖 二次回滚四、环境隔离5 行自检脚本「在我机器能跑到服务器就崩」的根因就是环境没隔离。生产环境必须用虚拟环境venv 或 conda env且依赖版本要钉死。importsys,shutildefcheck_env_isolation():is_venv(sys.prefix!sys.base_prefixorenvsinsys.executable.lower()or.venvinsys.executable.lower())print(fPython:{sys.executable})print(f隔离环境:{✅ifis_venvelse❌})forpkgin[mairui,pandas,numpy,apscheduler]:try:mod__import__(pkg)print(f{pkg}: ✅ v{getattr(mod,__version__,?)})exceptImportError:print(f{pkg}: ❌)dushutil.disk_usage(.)free_mbround(du.free/1024/1024,1)print(f磁盘剩余:{free_mb}MB{✅iffree_mb500else⚠️})实测Python: your_python 隔离环境: ✅ mairui: ✅ v1.0.0 pandas: ✅ v2.2.3 numpy: ✅ v2.0.2 apscheduler: ✅ v3.10.4 磁盘剩余: 58760.6 MB ✅额外动作requirements.txt钉死版本mairui1.0.0不要写mairuipyproject.toml或Pipfile.lock锁整个依赖图生产环境加pip-audit跑漏洞扫描Docker 化可直接跳过 Python 版本问题但 Dockerfile 本身也要版本锁五、日志轮转RotatingFileHandler策略跑一个月logs/app.log单文件能从 0 涨到 10 GBtail都打不开必须轮转importloggingfromlogging.handlersimportRotatingFileHandlerimportosdefsetup_logging(log_path:strlogs/app.log):os.makedirs(os.path.dirname(log_path),exist_okTrue)rootlogging.getLogger()root.setLevel(logging.INFO)root.handlers.clear()# 重要避免重复挂载fmtlogging.Formatter(%(asctime)s [%(levelname)s] %(message)s)# 1 MB 切一次保留 5 个历史文件fhRotatingFileHandler(log_path,maxBytes1024*1024,backupCount5,encodingutf-8)fh.setFormatter(fmt)root.addHandler(fh)# 同时输出 console 便于调试shlogging.StreamHandler()sh.setFormatter(fmt)root.addHandler(sh)实测2026-09-11 10:06:26 [INFO] [test] 第 1 条测试日志 2026-09-11 10:06:26 [INFO] [test] 第 2 条测试日志 2026-09-11 10:06:26 [INFO] [test] 第 3 条测试日志 2026-09-11 10:06:26 [INFO] [test] 第 4 条测试日志 2026-09-11 10:06:26 [INFO] [test] 第 5 条测试日志 已写入 logs/app.log (440 bytes)轮转策略选择RotatingFileHandler按文件大小切适合写日志频率稳定的场景TimedRotatingFileHandler按时间切每天/每小时一个文件适合审计生产建议两个都上大小轮转保底 时间轮转便于合规审计六、数据备份一行 shutil.copy2升级 schema 前、出问题想回滚前先备份再说。SQLite 单文件数据库备份成本最低importshutil,osfromdatetimeimportdatetimedefbackup_database(db_path:str,backup_dir:strbackups)-str:ifnotos.path.exists(db_path):returnos.makedirs(backup_dir,exist_okTrue)tsdatetime.now().strftime(%Y%m%d_%H%M%S)backup_pathos.path.join(backup_dir,f{os.path.basename(db_path)}.{ts}.bak)shutil.copy2(db_path,backup_path)returnbackup_path实测备份成功: backups\history_kline.db.20260911_100626.bak (24576 bytes)生产备份三重门本地快照每次升级 / 重大操作前—— 上节代码每日定时全量cron0 2 * * * python backup.py保留 7 天 / 4 周 / 6 月滚动异地容灾每天rsync到另一台机器或对象存储OSS / COS / S3copy2会保留原文件的 mtime permission比copy更适合做备份出问题一眼看出哪个是旧的。七、灰度对比新旧策略并行跑一次新策略不能「直接替换上线」。标准做法是保留旧版 新版并行跑 1 周对比夏普 / 收益 / 最大回撤defgray_release_compare(stocks,licence):v1 vs v2 各跑一次对比结果演示证用同一价格代替真实策略差异。results{v1:{},v2:{},diff:0}forstockinstocks:frommairuiimportClient cliClient(licence)rawcli.stock_history(codestock,st2025-12-01,et2025-12-01,periodd,dividendf)pricefloat(raw[-1].get(c,0.0))ifrawelse0.0results[v1][stock]price# 旧策略结果results[v2][stock]price# 新策略结果演示证同价print(f{stock}: v1{price:.2f}v2{price:.2f})results[diff]sum(abs(results[v1][s]-results[v2][s])forsinstocks)returnresults实测600519: v111.33 v211.33 ✅ 000001: v111.33 v211.33 ✅ 300750: v111.33 v211.33 ✅ 累计绝对偏差: 0.0000真实部署的对比维度收益曲线日收益、夏普、最大回撤稳定性连续亏损天数、最大单日亏损延迟从拿到数据到信号产生的时间资源占用CPU / 内存峰值灰度期间任一关键指标恶化超过阈值如夏普下降 30%立即停止灰度、回滚到 v1。八、回滚预案3 步恢复回滚的关键是「回滚本身不出错」。三个关键动作importshutil,osdefrollback(db_path:str,backup_path:str)-bool:ifnot(backup_pathandos.path.exists(backup_path)):returnFalse# 1. 备份当前主库万一回滚失败还能再回滚一次pre_rollbackdb_path.pre_rollback.bakshutil.copy2(db_path,pre_rollback)# 2. 用备份覆盖主库shutil.copy2(backup_path,db_path)# 3. 健康检查连接 必要表存在importsqlite3try:connsqlite3.connect(db_path)conn.execute(SELECT COUNT(*) FROM kline).fetchone()conn.close()returnTrueexceptException:returnFalse实测回滚成功: history_kline.db ← backups\history_kline.db.20260911_100626.bak (回滚前快照保留: history_kline.db.pre_rollback.bak)回滚操作 SOP停服务kill 进程避免回滚瞬间被新数据污染备份当前状态放.pre_rollback.bak防回滚本身出错执行覆盖shutil.copy2(backup_path, db_path)健康检查连接 DB 必要表 行数对账重启服务先 dry-run再 open监控盯盘5 / 15 / 60 分钟分阶段确认九、上线 checklist 操作清单顺序动作命令 / 代码1拉最新代码git pull2创建新 venvpython -m venv venv source venv/bin/activate3装依赖pip install -r requirements.txt4环境自检python main.py env5备份主库python backup.py6灰度上线v1 v2 并行python run_gray.py724h 后对比指标python compare.py8通过 → 全量切换python run.py --version v29失败 → 立即回滚python rollback.py10写上线报告release_notes.md十、常见坑「直接覆盖生产代码」永远不要跳过灰度直接换版本。即使是「一行小修改」都可能引入逻辑变化。回滚忘了停服务回滚瞬间旧服务的进程还在跑、可能写新数据 → 把回滚覆盖的目标文件又污染了。先 stop、再 copy、再 start。备份文件不清理每天都跑备份一个月后 1000 个 .bak 文件。保留策略——7 天 / 4 周 / 6 个月滚动cron 自动删老文件。日志没分级DEBUG / INFO 一起写生产环境日志几小时内几个 G。生产用 WARNING 级DEBUG 仅测试环境开。**「在我机器能跑」**永远是上线前要避免的。Docker 化或 requirements.txt 钉死 同一 Python 版本三者至少占其一。没有「回滚预算」每次上线都准备 30 分钟回滚窗口超过 30 分钟还没回滚成功 二次事故先应急止血暂停服务再继续排查。十一、小结系列完结整套「数据 → 调度 → 监控 → 一致性 → 补采 → 上线」的工程体系8 篇走完#主题#01数据存储选型CSV / SQLite / MySQL#02APScheduler 自动采集流水线#03监控告警异常检测 推送#04数据一致性校验与版本管理#05多策略并行与进程隔离#06项目骨架与最小可运行架构#07历史数据补采与回填#08上线 checklist本文读者画像升级路径看完 #01–#04从「能跑回测」到「数据自动跑、出了问题能发现」看完 #05–#07从「单策略」到「多策略协同、缺口可补」看完 #08从「脚本」到「生产系统」—— 上线有 checklist、出问题有兜底「量化系统的工程能力 策略 × 100 后的稳定性」。同样一套策略工程差距可以让年化收益 ± 30% 甚至更多。把工程做扎实才能让策略的预期收益在生产环境真实兑现。免责声明本文仅供技术学习交流不构成任何投资建议。量化策略回测表现不代表未来收益投资有风险决策需谨慎。代码与文档https://github.com/MaiRuiApi
返回列表