番外 | 量化系统上线第一周怎么盯?7 个关键动作的告警阈值与日报节奏

系列完结特典:#08 我们把脚本变成了生产系统,但这只是出问题的「概率降到最低」。上线第一周(5 个交易日),还要靠「盯」守住最后一公里——本文给 7 个必备盯盘动作的代码实现、阈值参考与日报节奏。

一、上线第一周最容易翻车的几件事

「上线」不等于「稳」。前 5 个交易日是真刀真枪的考验期:

  • D1:定时任务首次在真实环境跑,可能因为系统时区 / 编码 / 路径出错
  • D2-D3:灰度发布两个版本并行跑,监控指标对比才知道新版本是否更稳
  • D4-D5:策略第一次「经历了完整的情绪周期」(周一周五、月中、尾盘)

但新人最容易犯的错是:上线完就放手。没人盯的策略,跑挂了也只会在 3 天后看日志时才被发现——损失已经发生。

解决思路:用「日报」和「自动告警」替代「人肉盯盘」。人盯 7×24 不现实,自动看板 + 周报足够覆盖 80% 的事故。

二、本文你将得到什么

  1. 7 个盯盘动作:任务成功率、数据新鲜度、持仓敞口、信号频率、错误日志、API 延迟、最大回撤
  2. 阈值参考:每个动作给出「正常 / 警告 / 失败」三档阈值
  3. 一键 Demo:8 行聚合检查 + JSON 落盘,写入日报
  4. 看板节奏:上线 D1-D7 的日报/Demo 节奏表

三、7 个盯盘动作清单

动作 正常 警告 失败

1 任务成功率(7 天) ≥99% 95%-99% <95%
2 数据新鲜度 ≤1 天 1-3 天 >3 天
3 持仓敞口 P95 <80% 80%-95% ≥95%
4 信号频率(日均) 5-30 1-5 / 30-100 <1 / >100
5 错误日志数(24h) <10 10-50 ≥50
6 API 延迟 P95 <5s 5-10s ≥10s
7 最大回撤(7d) ≥-5% -5%~-10% <-10%

任意一个动作陷入「失败」,必须在下一个交易日开盘前定位;「警告」可在当周内处理。

四、7 个动作的代码实现

from datetime import datetime
import json, sqlite3, os, random

1. 任务成功率(最近 7 天 task_log 表聚合)

def watch_task_success_rate(conn) -> dict:
rows = conn.execute("""
SELECT status, COUNT(*) FROM task_log
WHERE run_at >= date('now', '-7 days')
GROUP BY status
""").fetchall()
total = sum(c for _, c in rows)
ok = next((c for s, c in rows if s == "ok"), 0)
rate = ok / total if total else 0
status = "✅" if rate >= 0.99 else ("⚠️" if rate >= 0.95 else "❌")
return {"name": "task_success_rate_7d", "value": f"{ok}/{total} = {rate:.1%}",
"threshold": ">=99%", "status": status}

2. 数据新鲜度(最新一条 vs 当前时间差)

def watch_data_freshness(conn) -> dict:
last = conn.execute("SELECT MAX(date) FROM kline").fetchone()[0]
if not last:
return {"name": "data_freshness", "value": "N/A",
"threshold": "<=1d", "status": "❌"}
last_dt = datetime.strptime(last, "%Y-%m-%d")
fresh_min = (datetime.now() - last_dt).total_seconds() / 60
status = "✅" if fresh_min < 1440 else "❌"
return {"name": "data_freshness", "value": f"{fresh_min:.0f} 分钟",
"threshold": "<=1440min", "status": status}

3. 持仓敞口 P95(最近 100 个 bar)

def watch_position_exposure(conn) -> dict:
rows = conn.execute("""
SELECT position_pct FROM exposure_log
ORDER BY log_at DESC LIMIT 100
""").fetchall()
samples = sorted(r[0] for r in rows)
p95 = samples[int(len(samples) * 0.95) - 1] if samples else 0
status = "✅" if p95 < 0.8 else ("⚠️" if p95 < 0.95 else "❌")
return {"name": "position_exposure_p95", "value": f"{p95:.0%}",
"threshold": "<80%", "status": status}

4. 信号频率(日均信号数,最近 5 日)

def watch_signal_frequency(conn) -> dict:
rows = conn.execute("""
SELECT DATE(signal_at), COUNT(*) FROM signals
WHERE signal_at >= date('now', '-5 days')
GROUP BY DATE(signal_at)
""").fetchall()
counts = [c for _, c in rows]
avg = sum(counts) / len(counts) if counts else 0
status = "✅" if 5 <= avg <= 30 else "❌"
return {"name": "signal_freq_avg_5d", "value": f"{avg:.1f}/日",
"threshold": "5-30/日", "status": status}

5. 错误日志数(最近 24h)

def watch_log_errors(log_path="logs/app.log") -> dict:
err = 0
if os.path.exists(log_path):
with open(log_path, "r", encoding="utf-8") as f:
err = sum(1 for line in f
if "[ERROR]" in line or "[CRITICAL]" in line)
status = "✅" if err < 10 else ("⚠️" if err < 50 else "❌")
return {"name": "log_errors_24h", "value": str(err),
"threshold": "<10", "status": status}

6. API 延迟 P95(最近 100 次)

def watch_latency(metric_path="metrics/latency.jsonl") -> dict:
samples = []
if os.path.exists(metric_path):
with open(metric_path, "r", encoding="utf-8") as f:
for line in f:
try:
samples.append(float(line.strip()))
except ValueError:
pass
samples = samples[-100:]
samples.sort()
p95 = samples[int(len(samples) * 0.95) - 1] if samples else 0
status = "✅" if p95 < 5 else ("⚠️" if p95 < 10 else "❌")
return {"name": "latency_p95", "value": f"{p95:.2f} s",
"threshold": "<5s", "status": status}

7. 最大回撤(最近 7 日净值序列)

def watch_drawdown(nav_path="nav.csv") -> dict:
nav = []
if os.path.exists(nav_path):
with open(nav_path, "r", encoding="utf-8") as f:
next(f) # skip header
for line in f:
try:
nav.append(float(line.strip().split(",")[-1]))
except (ValueError, IndexError):
pass
if not nav:
return {"name": "max_drawdown_7d", "value": "N/A",
"threshold": ">=-5%", "status": "❌"}
peak = nav[0]
max_dd = 0
for v in nav:
peak = max(peak, v)
max_dd = min(max_dd, (v - peak) / peak)
status = "✅" if max_dd > -0.05 else ("⚠️" if max_dd > -0.10 else "❌")
return {"name": "max_drawdown_7d", "value": f"{max_dd:.2%}",
"threshold": ">=-5%", "status": status}

主流程:一次性跑 7 项检查 + 写 JSON 报告

def run_all_checks():
conn = sqlite3.connect("history_kline.db")
checks = [
watch_task_success_rate(conn),
watch_data_freshness(conn),
watch_position_exposure(conn),
watch_signal_frequency(conn),
watch_log_errors(),
watch_latency(),
watch_drawdown(),
]
conn.close()

print("=" * 70)
print(" 量化系统上线第一周 · 7 个盯盘动作")
print("=" * 70)
for c in checks:
print(f" {c['status']} {c['name']:30s} "
f"{c['value']:>15s} 阈值:{c['threshold']}")

n_pass = sum(1 for c in checks if c["status"] == "✅")
n_warn = sum(1 for c in checks if c["status"] == "⚠️")
n_fail = sum(1 for c in checks if c["status"] == "❌")
print(f"\n通过 {n_pass}/7 警告 {n_warn}/7 失败 {n_fail}/7")

overall = "[绿] 全绿" if n_fail == 0 and n_warn == 0 else (
"[黄] 有警告" if n_fail == 0 else "[红] 有失败"
)
print(f"整体: {overall}")

with open("watch_report.json", "w", encoding="utf-8") as f:
json.dump({
"report_time": datetime.now().isoformat(),
"checks": checks,
"summary": {"pass": n_pass, "warn": n_warn, "fail": n_fail},
}, f, ensure_ascii=False, indent=2)

if name == "main":
run_all_checks()

五、实测运行

======================================================================
量化系统上线第一周 · 7 个盯盘动作 Demo
报告时间: 2026-09-11 10:08:57
======================================================================

❌ task_success_rate_7d 7/8 = 87.5% 阈值:>=99%
❌ data_freshness 2010 分钟 阈值:<=1440 min (1d)
✅ position_exposure_p95 78% 阈值:<80%
✅ signal_freq_avg_5d 15.0/日 阈值:5-30/日
✅ log_errors_24h 0 阈值:<10
✅ latency_p95 1.86 s 阈值:<5s
❌ max_drawdown_7d -11.43% 阈值:>=-5%

通过 4/7 警告 0/7 失败 3/7
整体: [红] 有失败

[ok] 报告 → watch_report.json

解读:

  • 4 项 ✅:仓位 / 信号频率 / 日志错误 / 延迟,全部在安全区
  • 3 项 ❌:
    1. 任务成功率 87.5%:演示证失效导致一次任务失败,需要切回正式证书
    2. 数据新鲜度 2010 分钟:超过 1 天未更新,拉取任务卡住了
    3. 最大回撤 -11.43%:模拟的净值序列显示组合出现了大幅回撤,需要立即复盘

生产环境不会同时 3 个 ❌,但任何一个 ❌ 出现就代表「明天开盘前必修」。

六、上线第一周日报节奏表

日 重点 必做动作
D1 灰度监控 跑 7 项检查 + 手写日报「上线后第一日运行报告」
D2 指标对比 v1 vs v2 各项指标对比,提交「灰度对比表」
D3 失败案例 抓 1 笔最差交易 / 最大回撤段,复盘决策链
D4 延迟优化 拉 7 项检查 + 重点关注 #6 API 延迟
D5 全量切换 灰度指标通过 → v2 替换 v1,备份 v1 保留 7 天
D6 周末巡检 跑 7 项检查 + 看假期前是否需要暂停任务
D7 首周总结 出「首周复盘报告」:跑赢/跑输基准、是否扩容、数据质量评估

日报格式(建议):

【日期】周一-周日
【当日 P&L】+1.5% / -0.3% …
【信号次数】18 次 / 当日
【7 项监控】4 ✅ / 0 ⚠️ / 3 ❌
【关键事故】1 次(已修复)
【明日重点】盯 v2 vs v1 夏普对比

七、常见坑

  1. 「上线即放手」:上线第一周最常翻车,因为现实环境总会缺一些没测到的边界条件(凌晨 cron 时区、节假日证书断供、并发 race condition)。人盯才能早期发现。
  2. 所有 ❌ 都堆到周五:D1-D5 的失败没人处理,到 D5 一看 5 个 ❌,周末再补救 → 损失已经定型。日报节奏是为了每天都处理 1-2 个,不让问题累积。
  3. 阈值太严:阈值参考是「参考」不是「绝对」,按自己策略特性调整。如日内高频策略延迟阈值应 < 1s;月度再平衡策略延迟 > 30s 也 OK。
  4. 没记录「本周基线」:第一周跑完后要把「基线值」定下来,作为长期监控的对照。比如「日均信号 15 次」是基线,< 5 或 > 30 立即告警。

八、小结

上线第一周不是「上线完」而是「真正开始」:

  • 7 个盯盘动作:任务 / 数据 / 仓位 / 信号 / 日志 / 延迟 / 回撤,全方位覆盖
  • 阈值参考:正常 / 警告 / 失败三档,按策略特性微调
  • 日报节奏:D1-D7 每天重点不同,问题不累积
  • 看板化:watch_report.json 自动落盘 → Grafana / Streamlit 长期可视化

整套「量化系统工程实战」8 + 1(番外)篇,从数据存储 → 自动调度 → 监控告警 → 一致性 → 多策略 → 项目骨架 → 数据补采 → 上线 checklist → 第一周盯盘,从脚本到稳态生产的全工程链路就闭环了。

下一期新选题即将启动(市场调研方向:Python 实盘交易工程:从回测信号到委托下单),聚焦「怎么把信号变成真实的买入/卖出」。敬请期待。


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

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

posted @ 2026-10-08 09:08  量化分析码农  阅读(11)  评论(0)    收藏  举报