# 运维手册（Ops Runbook）

> 注意：本文件包含服务器 IP。如果仓库转为 public，先把本文件脱敏。

## 部署拓扑

- **服务器**：AWS EC2 `3.147.56.209`（Ubuntu 24.04, x86_64, 2C/4G）
  - SSH：`ssh -i ~/Downloads/aws-dylan.pem ubuntu@3.147.56.209`（pem 在本机 Downloads，勿入库）
  - 部署目录：`/home/ubuntu/predit-bot/`（`weatherbot.db` 是策略主库；
    `weatherbot-maker.db` 是 7 天滚动的候选 top-5 book/trade 流，不进入灾备）
- **GitHub**：`dylanwuzc/predit-bot`
  - `main` 分支 = 代码；`data` 分支 = 数据备份（服务器持写权限 deploy key `~/.ssh/predit_deploy`）

## systemd 服务（均开机自启）

| 单元 | 作用 | 说明 |
|---|---|---|
| `weatherbot.service` | 纸面交易循环（`weatherbot paper`） | Restart=always，RUST_LOG=info |
| `weatherbot-web.service` | 状态看板 :8080 | http://3.147.56.209:8080 ，只读，**无鉴权**（实盘前必须收紧） |
| `predit-backup.timer` | 每 6 小时数据备份 | dump SQL → git push `data` 分支，Persistent=true |
| `weatherbot-calibrate.timer` | 每日 01:30 UTC 刷新固定 lead 偏差表 | `weatherbot calibrate`（Previous Runs D+1/D+2/D+3 + IEM 实测，Phase 2 影子校正）；精确 lead 行优先、D+1 站点行回退，偏差 >7 天不再使用 |
| `weatherbot-maker-observe.service` | 公共 CLOB L2 + maker 队列纸面回放 | 无 API key/私钥/下单接口；每 10 分钟刷新候选订阅，10 秒 PING，断线自动重连 |

## 常用命令（本机执行）

```bash
SSH="ssh -i ~/Downloads/aws-dylan.pem ubuntu@3.147.56.209"
# 同一把钥匙另有副本 ~/.ssh/aws-dylan.pem（Claude Code 用：macOS TCC 挡 Downloads）

$SSH 'sudo journalctl -u weatherbot -f'                       # 实时日志
$SSH 'cd predit-bot && ./target/release/weatherbot report'    # 战绩
$SSH 'cd predit-bot && sqlite3 weatherbot.db "SELECT key,value FROM bot_meta WHERE key IN (\"fee_accounting_v1_start\",\"shadow_b_start\",\"prob_score_v1_start\",\"model_family_v1_start\",\"shadow_c_start\",\"shadow_c4_start\")"'
$SSH 'sudo systemctl restart weatherbot'                      # 重启循环
$SSH 'sudo systemctl start predit-backup.service'             # 手动触发备份
$SSH 'systemctl list-timers predit-backup.timer --no-pager'   # 查看下次备份时间
$SSH 'sudo journalctl -u weatherbot-maker-observe -f'         # maker 只读观测日志
$SSH 'cd predit-bot && sqlite3 weatherbot.db "SELECT policy, status, COUNT(*), ROUND(SUM(filled_shares),1) FROM maker_sim_orders GROUP BY policy, status"'
# fill rate 统计只用 created_ts >= bot_meta.maker_fill_v2_start 的行（v1 trade-through 口径偏乐观，不可混）
$SSH 'cd predit-bot && sqlite3 weatherbot-maker.db "SELECT event_type, COUNT(*) FROM clob_l2_events GROUP BY event_type"'
```

## 部署新代码

> **rsync 前必须先 `git pull`。** rsync 是单向覆盖：本地检出落后多少，服务器就被回退
> 多少，而且不报错。2026-07-31 就这样把服务器的 `backup.sh` 退回了修复前的版本（本地
> 检出停在 `ab9905f`，origin 上已有 `2d46926`），备份脚本本身正是那个提交修的。
> 服务器 `~/predit-bot` **不是 git 仓库**，拉不了代码，也无法靠 `git status` 发现漂移。
> 开工前先 `git fetch && git status`，改完服务器后顺手确认改动已回流到 git。

```bash
# 本机改代码 → 测试 → 提交推送
git pull --rebase
cargo test && git add -A && git commit && git push

# 同步到服务器并重启（数据库/target 不会被覆盖）
rsync -az -e "ssh -i ~/Downloads/aws-dylan.pem" \
  --exclude target --exclude .git --exclude weatherbot.db --exclude 'weatherbot-maker.db*' --exclude .cargo \
  ~/Desktop/ai/predit-bot/ ubuntu@3.147.56.209:~/predit-bot/
ssh -i ~/Downloads/aws-dylan.pem ubuntu@3.147.56.209 \
  'cd predit-bot && ~/.cargo/bin/cargo build --release && sudo systemctl restart weatherbot weatherbot-web weatherbot-maker-observe'
```

改了 `config.toml` 只需 rsync + restart，不用重编译。**`weatherbot` 和 `weatherbot-web`
都要重启**：web 是独立进程，启动时读一次 config，只重启 paper 循环的话看板会继续显示
旧的阈值和仓位上限（策略已按新值运行，页面却说旧值，比两边都旧更难发现）。若改动涉及
校准命令或 systemd unit，还要执行 `sudo systemctl daemon-reload` 并手动触发对应
service/timer 验证一次。

改完 `backup.sh` 或任何 6 小时 timer 触发的脚本，用
`sudo systemctl start predit-backup.service` 手动跑一次确认，别等下一个周期 ——
timer 失败不会报警（2026-07-09 到 07-28 备份连续失败 19 天无人发现）。

## 备份与恢复

- 备份内容：`data` 分支下 `data/weatherbot.sql.gz` + `data/config.toml`
- **不是全量 dump**（2026-07-28 起）：只导 13 张结果表 —— `bot_meta` `paper_orders`
  `paper_cycles` `settlements` `shadow_orders` `experiment_orders` `experiment_rearms`
  `calibration_observations` `station_bias` `station_lead_bias` `forecast_batches`
  `forecasts` `maker_sim_orders`。**`snapshots` 和 `clob_l2_events` 不备份**：两者
  （含索引）占库体积 90%，属原始行情流而非推导状态，丢失不影响战绩结论或重建。
  当前产物 15.3 MB，耗时 8.5 秒
- **加了 90 MB 体积守卫**：超限时脚本在 push 前 `exit 1` 并打印是哪个表撑爆了预算。
  上一次失败模式是全库 gzip 后 188 MB 被 GitHub blob 限制拒收，而错误埋在 git push
  输出里，**连续失败 32 次、断档 19 天（2026-07-09 → 07-28）才被发现**
- **data 分支永远只有一个 commit**（每次备份 force-push 无父提交）：备份定位是
  "最新状态的灾难恢复"，不是历史归档；脚本 = 仓库根目录 `backup.sh`（随 rsync 部署）
- 备份健康检查：`systemctl is-failed predit-backup.service` 应为 `inactive`；
  成功时日志末行为 `backup ok: <bytes>`
- 本机另有手动快照：`~/Desktop/ai/predit-bot-data-backup/`
- **恢复流程**（服务器丢失时）：
  ```bash
  git clone git@github.com:dylanwuzc/predit-bot.git && cd predit-bot
  git switch data && gunzip -c data/weatherbot.sql.gz | sqlite3 weatherbot.db
  git switch main   # 数据库文件在 .gitignore 中，切分支不受影响
  # 然后按上面"部署新代码"流程装机（rustup + build-essential + systemd 单元见本文附录）
  ```
  还原出的库**不含 `snapshots` 与 `clob_l2_events`**，两张表会在 bot 首次启动时按
  schema 重建为空表。战绩、结算、校准输入都完整；受影响的只有依赖历史快照的离线研究
  （`tools/replay_exits.py` 的 10 分钟快照止盈回放、maker 队列回放）
- 最坏损失窗口：6 小时（上次备份以来的快照/订单）；预报数据可从 Open-Meteo 历史归档补齐

## 健康检查

1. 看板顶部"最近扫描"时间戳落后 >30 分钟 → 循环卡死，`sudo systemctl restart weatherbot`
2. `sudo journalctl -u weatherbot --since -1h | grep -c ERROR` 应为 0 或个位数（偶发 API 超时正常，Restart 会兜底）。
   同时检查 `grep -c '429 Too Many Requests'`；非零说明 Open-Meteo 日额度或突发限流。
   交易门禁会拒绝残缺 ensemble，但持续 429 仍会让新信号停止更新。
   每轮 `cycle complete` 还会打印累计的 `forecast_memory_hits`、
   `forecast_durable_hydrations`、`forecast_fresh_fetches`、
   `forecast_stale_fallbacks`、`forecast_rate_limits`。重启后的首轮应看到
   durable hydration；若近期完整批次存在却仍全部 fresh fetch，检查 `forecasts`
   表时间戳和三个必需模型是否齐全。新写入的 `snapshots.forecast_batch_id` 与
   `paper_orders.forecast_batch_id` 应能连接到 `forecast_batches.id`；旧行 NULL 是
   有意保留，禁止按时间近邻回填。
3. GitHub `data` 分支最新 commit 时间应在 6 小时内
4. `weatherbot report` 的 `pending resolution` 正常应只短暂存在；目标日过去 >24h
   仍未结算时检查 Gamma event 的 `closed`/winning bucket。pending 金额仍计入 `$800`
   敞口上限，不能手工改成 settled 或从风险金额扣除。
5. `shadow A forward-only` 以本版本首次打开生产 DB 时写入 `bot_meta` 的时间为边界；
   不要删除/改写 `shadow_no_08_20_start`，否则会污染前瞻验证样本。
   同理不得改写 `fee_accounting_v1_start`、`shadow_b_start`、`prob_score_v1_start`、
   `shadow_c_start`、`shadow_c4_start`。
   `report` 的 Phase 门槛只看 fee-aware forward：新订单必须有 `fee_rate`、
   `fee_exponent`、`entry_fee` 和 `net_edge`；CLOB 费率接口失败时该候选应被拒单，
   不能用零费继续。Shadow B 已停止新增（历史行仍正常结算）；Shadow C 在独立
   `shadow_orders` 表，每事件只能有一行，报告只显示原策略与 C3。
   C3 的 `reference_bid/reference_ask` 必须在 `shadow_c_start` 后自然积累，禁止用旧
   `best_bid/best_ask` 回填。上线后前 180 分钟 `shadow_c_signals=0` 属正常预热；之后每轮
   只看聚合的 signals/executable/opened，避免逐 bucket 日志噪音。
   C4 使用独立 `experiment_orders` 与 `experiment_rearms`：每事件最多两轮，首轮退出后
   连续 30 分钟无趋势才重新武装。健康日志为 `shadow_c4_signals/executable/opened/closed`；
   不得修改边界或手工把 cooling 状态设为 armed。2026-07-21 起
   `enable_trend_entries=false`：C3/C4 只管理已有仓位的退出/结算，健康日志中的
   `signals/executable/opened` 应持续为 0；未来新趋势规则必须使用新实验名和边界。
6. 校准健康检查：`station_lead_bias` 应包含 D+1/D+2/D+3，`validation_n>0` 才能用
   `validation_raw_mae` 与 `corrected_mae` 判断去偏是否真正改善；两者是同一组
   walk-forward 日期。`calibration_observations` 的主键保证每站点×kind×lead×目标日
   只有一条，不得把 paper 的 10 分钟刷新当独立样本。
   F1 模型家族影子从 `model_family_v1_start` 起计分；`snapshots.model_prob_family` 应在
   每轮对必需 ensemble 完整的 bucket 非空，`model_prob_family_corr` 仅在新鲜 bias 可用
   时非空。看板“预报批次可追溯”在首次新批次刷新后应逐步趋近 100%；若长期为 0，
   检查 paper/web 是否部署了同一版本。F1 只读，不得用于 `signal_side`、候选排序或开仓。
7. maker 健康检查：独立 `weatherbot-maker.db` 的 `clob_l2_events` 应持续增长；
   主库 `maker_sim_orders` 的 filled/expired
   只能解释为保守 replay。队列只被 SELL trades 消耗，撤单不减少 queue_ahead；没有
   user channel 的真实 `size_matched` 前，不得把该 fill rate 直接写进实盘收益预测。
   price_change 只在内存更新，book/trade 保留 7 天后自动清理且不备份；
   `maker_sim_orders` 不清理。
8. Hold 暂停健康检查：`bot_meta.hold_pause_v1_start` 必须存在；paper cycle 日志应显示
   `hold_entries_enabled=false`、`new_orders=0`，但 `candidate_count`、snapshots、forecasts
   和 maker replay 仍持续增长。新 forecast batch 应逐步出现
   `google_weathernext2_ensemble`（64 个成员）；该 selector 不得改变 live raw/corrected/F1
   probability，恢复 Hold 前必须另立前瞻边界并满足 STRATEGY 的 Brier 门槛。

## 提前退出回放

主库 `snapshots` 每 10 分钟记录一次可成交 top-of-book，可用只读脚本复验固定止盈：

```bash
python3 tools/replay_exits.py weatherbot.db --sample shadow-b-proxy
python3 tools/replay_exits.py weatherbot.db --sample shadow-b-v2-proxy
# --ignore-depth 仅是乐观敏感性分析，不可作为可实现收益
```

脚本用首次触发时的 bid、双边 taker fee，并默认要求顶层深度足以全平；不会使用事后
峰值。生产库已超过两百万快照，分析时先用 SQLite `.backup` 制作一致性副本再跑，避免
长查询影响常驻写入。当前回放不支持自动止盈，生产只保留 METAR bucket 已决定后的
knockout salvage。

过期市场仍未 resolve 时，paper 循环只调用 Gamma 结算轮询，不再向 Open-Meteo 请求
该历史日期。若日志继续出现 `no forecast hours for <过去日期>`，说明部署版本未生效。

## 安全清单

- 看板无鉴权（当前只有纸面统计，可接受）；**Phase 3 实盘前**：关闭 8080 安全组入站，改走 SSH 隧道
- 服务器/deploy key 只碰纸面数据，不碰资金；实盘钱包私钥永远不上这台机器
- `aws-dylan.pem` / `*.pem` 已在 .gitignore，勿提交

## 附录：systemd 单元位置

`/etc/systemd/system/{weatherbot.service, weatherbot-web.service, weatherbot-maker-observe.service, predit-backup.service, predit-backup.timer, weatherbot-calibrate.service, weatherbot-calibrate.timer}`，
备份脚本 `/home/ubuntu/predit-bot/backup.sh`。重装时从本文复原或找会话记录。
