跳到主要内容

部署与运维

rocky-bot 跑在 EC2 上 systemd-user 服务里,由 30 个由 mint-30.sh 一次性 mint 出来的账号驱动。本文讲完整的"从零到运行 + 持续监控 + 故障复盘"。

如果不熟悉策略本身,先看 策略详解风控与杠杆


一、初次 provisioning:mint 30 个账号

源码:rocky-bot/scripts/mint-30.sh

cd /Users/ubuntu/Desktop/Rocky/rocky-bot
bash scripts/mint-30.sh > .keys.json

脚本做的事(~1.5 分钟,全部 SSH 进 EC2):

  1. 生成 30 个 role manifest(python3 内联生成 JSON)
    • 12 个 BUY ladder(5/6/7/8/9/10 bps × L1 + 15/20/25/30 bps × L2 + 50/100 bps × L3)
    • 12 个 SELL ladder(对称)
    • 1 个 anchor
    • 5 个 taker
  2. 逐个调用 EC2 的 mint_api_key Rust binary
    • 在 rocky-backend repo 里跑 cargo run -p api-gateway --bin mint_api_key -- --new-user --label '<id>'
    • 后端会创建一个 auth.api_keys 行 + 一个新用户 user_id
    • 返回 user_id / api key / secret
  3. 每个新用户立即 seed $100 USDC
    • 调 backend /v1/deposits/seed 接口
  4. 聚合最终 .keys.json 输出到 stdout

.keys.json 结构示例:

{
"rocky_fapi_url": "https://demo.rocky.exchange",
"accounts": [
{
"id": "mm-l1-buy-05bps",
"role": "ladder",
"side": "BUY",
"offset_bps": 5,
"user_id": "5cfb031b-5936-4467-9533-cd2df576dbb8",
"key": "...",
"secret": "..."
},
...30 总数
]
}

安全注意.keys.json 含 30 对 (key, secret),是高敏感。

  • .gitignored,永不进 git
  • 仅在 deploy 时通过 scp 复制到 EC2
  • 本地需要保留一份用于重新 deploy

1.1 SSH 多路复用

mint-30.sh 用 ControlMaster 复用单个 SSH 连接,避免开 30 次 SSH 触发服务器 MaxStartups 限速:

SSH_OPTS=(-i "$SSH_KEY" -o ControlMaster=auto -o ControlPath="$CTRL_PATH" -o ControlPersist=60)

历史上没加这一行时,第 8 个连接就被 sshd 拒绝("connection reset by peer")。


二、Deploy:上线 / 升级 bot 代码

源码:rocky-bot/deploy.sh

cd /Users/ubuntu/Desktop/Rocky/rocky-bot
./deploy.sh

做的事:

  1. rsyncrocky_bot/ 源码同步到 EC2 ~/rocky-bot/
  2. scp .env(环境配置)+ scp .keys.json(账号凭证)
  3. SSH 执行 uv venv --python 3.12 --allow-existing && uv pip install -e .
  4. SSH 执行 systemctl --user restart rocky-bot

rsync--exclude .env --exclude .keys.json 让敏感文件走单独的 scp 通道。

2.1 systemd 单元

# ~/.config/systemd/user/rocky-bot.service
[Unit]
Description=rocky-bot — volume generator for demo.rocky.exchange
After=network.target

[Service]
Type=exec
WorkingDirectory=%h/rocky-bot
ExecStart=%h/rocky-bot/.venv/bin/python -m rocky_bot.main
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

控制命令:

systemctl --user start rocky-bot
systemctl --user stop rocky-bot
systemctl --user restart rocky-bot
systemctl --user is-active rocky-bot
journalctl --user -u rocky-bot --since "5 min ago" --no-pager

三、Reset:清空状态 + 重启

源码:rocky-bot/scripts/reset.sh

bash scripts/reset.sh

5 步:

  1. systemctl --user stop rocky-bot
  2. SQL reset 30 个 funnel 账号的状态
    UPDATE ledger.positions SET qty=0, locked_margin=0 WHERE user_id IN (funnel users);
    UPDATE ledger.accounts SET available = available + locked, locked = 0
    WHERE asset='USDC' AND user_id IN (funnel users);
    DELETE FROM ledger.orders_open WHERE user_id IN (funnel users);
  3. pkill -f 'target/release/matching-engine' + nohup 重启 ME(让内存订单簿与刚清空的 orders_open 同步)
  4. sleep 3
  5. systemctl --user restart rocky-bot

3.1 为什么要 restart matching-engine

历史教训:早期 reset 只动 DB 没动 ME,ME 内存里还有旧订单引用着已被 DELETE 的 orders_open 行。新 bot 一启动就和这些"幽灵订单"撮合 → backend apply.rs 找不到对应 order → 走 leverage=1 fallback → margin 计算错误。

加这一步后,ME 重启时从(已清空的)orders_open 重新装载,本来就是空的。


四、监控(运行时)

四个关键指标:

4.1 max(locked) — 是否有账号接近爆仓

ssh ... 'docker exec rocky-backend-stack-postgres-1 psql -U rocky -d rocky -c "
SELECT round(max(locked)::numeric, 2) AS max_l,
round(avg(locked)::numeric, 2) AS avg_l,
count(*) FILTER (WHERE locked > 50) AS over_50,
count(*) FILTER (WHERE locked > 80) AS over_80
FROM ledger.accounts a JOIN auth.api_keys k ON k.user_id = a.user_id
WHERE a.asset = '\''USDC'\''
AND (k.label LIKE '\''mm-%'\'' OR k.label LIKE '\''taker-%'\'')
"'

健康标准(基于 leverage-fix 后的实测):

  • max_l < 50 USDC
  • over_80 == 0
  • avg_l ~ 20-30 USDC

异常:max_l 接近 100 表示 cap 没起作用 → 严重事故。

4.2 -2010 错误数 — backend 拒绝挂单的频率

ssh ... 'journalctl --user -u rocky-bot --since "30 min ago" --no-pager 2>&1 | grep -c "\-2010"'

-2010 = "insufficient balance",发生在 backend 余额不足以锁定挂单 margin 时。健康标准:

  • 30 分钟内 < 30(平均 1/min 以下)
  • 完美状态:0

异常:> 100 表示账号被锁满 → 一般伴随 max_l 接近 100。

4.3 recent trades — 是否还有成交活动

ssh ... 'docker exec rocky-backend-stack-postgres-1 psql -U rocky -d rocky -c "
SELECT symbol, side, price, qty, ts FROM ledger.trades ORDER BY ts DESC LIMIT 3
"'

健康:最新一笔 ts 在 1 分钟内。 异常:ts 超过 5 分钟 → bot 没在工作(可能挂了 / cap 锁满了)。

4.4 invariant logger — backend 计算正确性

ssh ... 'grep -c "invariant violated" /tmp/rocky-services/internal-ledger.log'

健康:0 或稳定低数值(仅 startup 噪声)。 异常:持续增长 → backend 撮合层有 bug。


五、5 个 margin-leak 修复轮次

完整的事故复盘(编年史),帮助理解为什么当前架构是这个样子:

Round 1:bot-side position cap(commits 077887d / 526cac5 / 97d23a6 / 1ae6f1a)

症状:30 分钟内 max(locked) 涨到 $99。 修复:给 LadderMakerLoop / AnchorMakerLoop 加 position-cap gate(策略详解 §2.4)。 结果:减缓但未根治。

Round 2:phantom-trade refusal(commits e67b63f / fd6b9e2)

症状:cap gate 生效后,仍有 leak。诊断发现 apply_trade_matched 在找不到 orders_open 行时 fallback 到 leverage=1修复:apply.rs 早期 return + 发 OrderCancelled NATS 让 ME 删除幽灵订单。 结果:减少但仍有 leak。

Round 3:invariant instrumentation(commit e32ae17)

症状:仍 leak,但找不到具体在哪一笔交易出错。 修复:在 apply_trade_matched 末尾加 tracing::error! 检查 accounts.locked == sum(positions.locked_margin) + sum(orders_open.margin_locked)结果:30 分钟收集 956 条违规记录。数据特征:pos_sum 中含 142857 循环(即 1/7)→ leverage 被错算成 7。

Round 4:reset.sh ops 修复

症状:reset 流程经常半失败(matching-engine 没被重启 / SQL heredoc 因 ssh 引号被吞)。 修复:reset.sh 完整重写,加 ME pkill + restart + stdin pipe SQL。 结果:清理变可重复,但 leak 本身还没修。

Round 5:LEVERAGE_V1 常量(commit dd653e6)

根因:apply.rs 用 notional / order_margin 反推 leverage,partial fill + price drift 后会 round 到 7/9/11。 修复:用 const LEVERAGE_V1: u32 = 10; 替代推导,与 api-gateway 早就 hardcode 的 leverage: 10 对齐。 结果:100% 修复。部署后 63 分钟运行 max_locked 稳定在 $27,0 个 invariant violation,0 个 -2010 错误。


六、故障排查 checklist

bot 似乎没在工作

  1. systemctl --user is-active rocky-bot — 服务还活着吗
  2. journalctl --user -u rocky-bot --since "1 min ago" — 看最近日志
  3. 看 max(locked) 是否接近 $98 — 如是,账号锁满
  4. 看 recent trades 是否新鲜 — 如不,可能 BinanceFeed 断了

CB 反复跳闸

  1. 看 journal 里 "CircuitBreaker opened: reason=..." — API errors / max loss
  2. 如是 API errors:backend 是否 healthy?curl https://demo.rocky.exchange/api/perp/markets
  3. 如是 max loss:人为 reset CB(重启 bot)OR 增加 RiskCaps.max_loss_usdc

invariant violation 重新出现

  1. 查具体哪一笔:grep "invariant violated" /tmp/rocky-services/internal-ledger.log | head -3
  2. 检查 violation 数据里的 diff 数值 + leverage 推断
  3. 如果是 leverage 相关,回到 round 5 的修复路径
  4. 如果是 new pattern,可能是 backend 新代码引入新 leak → 开 spec 重新走诊断流程

七、相关阅读

  • 做市机器人概览 — 整体架构
  • 策略详解 — 三种策略的内部逻辑
  • 风控与杠杆 — RiskCaps / CircuitBreaker / position cap / LEVERAGE_V1
  • 仓库 spec 文档(完整事故复盘):
    • rocky.interface/docs/superpowers/specs/2026-05-25-rocky-bot-position-cap-fix-design.md
    • rocky.interface/docs/superpowers/specs/2026-05-25-phantom-trade-fix-design.md
    • rocky.interface/docs/superpowers/specs/2026-05-25-margin-leak-instrumentation-design.md
    • rocky.interface/docs/superpowers/specs/2026-05-25-leverage-derivation-fix-design.md