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

系列完结篇:前 7 篇我们解决了「数据怎么存、自动怎么跑、挂了怎么知道、缺口怎么补」。本篇把脚本变成生产系统的最后 1 公里——环境隔离、日志轮转、数据备份、灰度对比、回滚预案,每一项都是「出事时不至于半夜爬起来」的兜底。

一、从个人脚本到生产系统的差距

很多量化代码跑完回测就上线,结果第一周就出各种事故:

  • 环境:同事用的 Python 3.12,自己机器是 3.9,pandas API 行为不一致 → 报错
  • 日志:脚本跑了两周,logs/app.log 已经 8 GB,tail 命令打不开 → 排错要等 5 分钟
  • 数据:升级版 SQLite schema 后忘了备份,旧版本数据没了 → 重新拉 3 天
  • 灰度:新策略直接替换旧策略,3 天后收益率掉 30% → 不知道该不该回滚
  • 回滚:出问题想回滚,但没提前准备备份,也不知道回滚要多久

上线 checklist 的目标:任何动作都有预演,任何事故都有兜底。

二、本文你将得到什么

  1. 环境隔离自检脚本:5 行检测 venv / 依赖 / 磁盘,避免「在我机器能跑」问题
  2. 日志轮转:1 MB × 5 个文件自动切换,避免日志膨胀
  3. 数据备份:一行 shutil.copy2 给 SQLite 打时间戳快照
  4. 灰度对比:新旧策略并行跑一次,差异阈值报警
  5. 回滚预案:3 步从「出问题」恢复到「上次能用」

三、上线 checklist 总表

按重要性排序,前 3 项是「不做立马翻车」,后 2 项是「做了才稳」:

项目 风险 兜底

1 环境隔离 依赖冲突 / Python 版本错位 venv + 依赖版本锁
2 日志轮转 日志爆炸 / 排错难 RotatingFileHandler
3 数据备份 数据丢失 / 升级翻车 时间戳 .bak 副本
4 灰度对比 新策略不稳定 v1 vs v2 并行观察
5 回滚预案 出问题没法恢复 备份覆盖 + 二次回滚

四、环境隔离:5 行自检脚本

「在我机器能跑,到服务器就崩」的根因就是环境没隔离。生产环境必须用虚拟环境(venv 或 conda env),且依赖版本要钉死。

import sys, shutil

def check_env_isolation():
is_venv = (
sys.prefix != sys.base_prefix
or "envs" in sys.executable.lower()
or ".venv" in sys.executable.lower()
)
print(f"Python: {sys.executable}")
print(f"隔离环境: {'✅' if is_venv else '❌'}")
for pkg in ["mairui", "pandas", "numpy", "apscheduler"]:
try:
mod = import(pkg)
print(f"{pkg}: ✅ v{getattr(mod, 'version', '?')}")
except ImportError:
print(f"{pkg}: ❌")
du = shutil.disk_usage(".")
free_mb = round(du.free / 1024 / 1024, 1)
print(f"磁盘剩余: {free_mb} MB {'✅' if free_mb > 500 else '⚠️'}")

实测:

Python: <your_python>
隔离环境: ✅
mairui: ✅ v1.0.0
pandas: ✅ v2.2.3
numpy: ✅ v2.0.2
apscheduler: ✅ v3.10.4
磁盘剩余: 58760.6 MB ✅

额外动作:

  • requirements.txt 钉死版本(mairui==1.0.0 不要写 mairui)
  • pyproject.toml 或 Pipfile.lock 锁整个依赖图
  • 生产环境加 pip-audit 跑漏洞扫描
  • Docker 化可直接跳过 Python 版本问题(但 Dockerfile 本身也要版本锁)

五、日志轮转:RotatingFileHandler

策略跑一个月,logs/app.log 单文件能从 0 涨到 10 GB,tail 都打不开,必须轮转:

import logging
from logging.handlers import RotatingFileHandler
import os

def setup_logging(log_path: str = "logs/app.log"):
os.makedirs(os.path.dirname(log_path), exist_ok=True)
root = logging.getLogger()
root.setLevel(logging.INFO)
root.handlers.clear() # 重要:避免重复挂载
fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s")
# 1 MB 切一次,保留 5 个历史文件
fh = RotatingFileHandler(
log_path, maxBytes=1024 * 1024, backupCount=5, encoding="utf-8"
)
fh.setFormatter(fmt)
root.addHandler(fh)
# 同时输出 console 便于调试
sh = logging.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 单文件数据库备份成本最低:

import shutil, os
from datetime import datetime

def backup_database(db_path: str, backup_dir: str = "backups") -> str:
if not os.path.exists(db_path):
return ""
os.makedirs(backup_dir, exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = os.path.join(backup_dir, f"{os.path.basename(db_path)}.{ts}.bak")
shutil.copy2(db_path, backup_path)
return backup_path

实测:

备份成功: backups\history_kline.db.20260911_100626.bak (24576 bytes)

生产备份三重门:

  1. 本地快照(每次升级 / 重大操作前)—— 上节代码
  2. 每日定时全量:cron 0 2 * * * python backup.py,保留 7 天 / 4 周 / 6 月滚动
  3. 异地容灾:每天 rsync 到另一台机器或对象存储(OSS / COS / S3)

copy2 会保留原文件的 mtime + permission,比 copy 更适合做备份(出问题一眼看出哪个是旧的)。

七、灰度对比:新旧策略并行跑一次

新策略不能「直接替换上线」。标准做法是保留旧版 + 新版并行跑 1 周,对比夏普 / 收益 / 最大回撤:

def gray_release_compare(stocks, licence):
"""v1 vs v2 各跑一次,对比结果(演示证用同一价格代替真实策略差异)。"""
results = {"v1": {}, "v2": {}, "diff": 0}
for stock in stocks:
from mairui import Client
cli = Client(licence)
raw = cli.stock_history(
code=stock, st="2025-12-01", et="2025-12-01",
period="d", dividend="f"
)
price = float(raw[-1].get("c", 0.0)) if raw else 0.0
results["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]) for s in stocks
)
return results

实测:

600519: v1=11.33 v2=11.33 ✅
000001: v1=11.33 v2=11.33 ✅
300750: v1=11.33 v2=11.33 ✅
累计绝对偏差: 0.0000

真实部署的对比维度:

  • 收益曲线:日收益、夏普、最大回撤
  • 稳定性:连续亏损天数、最大单日亏损
  • 延迟:从拿到数据到信号产生的时间
  • 资源占用:CPU / 内存峰值

灰度期间任一关键指标恶化超过阈值(如夏普下降 30%),立即停止灰度、回滚到 v1。

八、回滚预案:3 步恢复

回滚的关键是「回滚本身不出错」。三个关键动作:

import shutil, os

def rollback(db_path: str, backup_path: str) -> bool:
if not (backup_path and os.path.exists(backup_path)):
return False
# 1. 备份当前主库(万一回滚失败还能再回滚一次)
pre_rollback = db_path + ".pre_rollback.bak"
shutil.copy2(db_path, pre_rollback)
# 2. 用备份覆盖主库
shutil.copy2(backup_path, db_path)
# 3. 健康检查(连接 + 必要表存在)
import sqlite3
try:
conn = sqlite3.connect(db_path)
conn.execute("SELECT COUNT(*) FROM kline").fetchone()
conn.close()
return True
except Exception:
return False

实测:

回滚成功: history_kline.db ← backups\history_kline.db.20260911_100626.bak
(回滚前快照保留: history_kline.db.pre_rollback.bak)

回滚操作 SOP:

  1. 停服务:kill 进程(避免回滚瞬间被新数据污染)
  2. 备份当前状态:放 .pre_rollback.bak 防回滚本身出错
  3. 执行覆盖:shutil.copy2(backup_path, db_path)
  4. 健康检查:连接 DB + 必要表 + 行数对账
  5. 重启服务:先 dry-run,再 open
  6. 监控盯盘:5 / 15 / 60 分钟分阶段确认

九、上线 checklist 操作清单

顺序 动作 命令 / 代码
1 拉最新代码 git pull
2 创建新 venv python -m venv venv && source venv/bin/activate
3 装依赖 pip install -r requirements.txt
4 环境自检 python main.py env
5 备份主库 python backup.py
6 灰度上线(v1 + v2 并行) python run_gray.py
7 24h 后对比指标 python compare.py
8 通过 → 全量切换 python run.py --version v2
9 失败 → 立即回滚 python rollback.py
10 写上线报告 release_notes.md

十、常见坑

  1. 「直接覆盖生产代码」:永远不要跳过灰度直接换版本。即使是「一行小修改」,都可能引入逻辑变化。
  2. 回滚忘了停服务:回滚瞬间,旧服务的进程还在跑、可能写新数据 → 把回滚覆盖的目标文件又污染了。先 stop、再 copy、再 start。
  3. 备份文件不清理:每天都跑备份,一个月后 1000 个 .bak 文件。保留策略——7 天 / 4 周 / 6 个月滚动,cron 自动删老文件。
  4. 日志没分级:DEBUG / INFO 一起写,生产环境日志几小时内几个 G。生产用 WARNING 级,DEBUG 仅测试环境开。
  5. 「在我机器能跑」永远是上线前要避免的。Docker 化或 requirements.txt 钉死 + 同一 Python 版本,三者至少占其一。
  6. 没有「回滚预算」:每次上线都准备 30 分钟回滚窗口;超过 30 分钟还没回滚成功 = 二次事故,先应急止血(暂停服务)再继续排查。

十一、小结(系列完结)

整套「数据 → 调度 → 监控 → 一致性 → 补采 → 上线」的工程体系,8 篇走完:

主题

01 数据存储选型:CSV / SQLite / MySQL

02 APScheduler 自动采集流水线

03 监控告警:异常检测 + 推送

04 数据一致性校验与版本管理

05 多策略并行与进程隔离

06 项目骨架与最小可运行架构

07 历史数据补采与回填

08 上线 checklist(本文)

读者画像升级路径:

  • 看完 #01–#04:从「能跑回测」到「数据自动跑、出了问题能发现」
  • 看完 #05–#07:从「单策略」到「多策略协同、缺口可补」
  • 看完 #08:从「脚本」到「生产系统」—— 上线有 checklist、出问题有兜底

「量化系统的工程能力 = 策略 × 100 后的稳定性」。 同样一套策略,工程差距可以让年化收益 ± 30% 甚至更多。把工程做扎实,才能让策略的预期收益在生产环境真实兑现。


免责声明:本文仅供技术学习交流,不构成任何投资建议。量化策略回测表现不代表未来收益,投资有风险,决策需谨慎。

代码与文档:https://github.com/MaiRuiApi

posted @ 2026-10-07 13:40  量化分析码农  阅读(7)  评论(0)    收藏  举报