Files
Chan/live/deploy/README.md
T
jackandCursor 15f4aa088e 加 Telegram 通知,并修好一道死掉的闸
接 Telegram 时查出 Guard.realized() 定义了但全仓库没有调用点——pnl_day
恒为 0,MAX_DAY_LOSS 完全不生效。三条硬约束里最重要的一条是死的。

根因是止损与止盈都挂在交易所侧成交,本进程收不到通知,而 sweep 只在 48
分钟到点才查持仓。连带第二个后果:execs 条目不清,MAX_OPEN 把已出场的
仓位继续算在场,新信号被白挡到截止时刻。

补 watch() 循环(10s):持仓消失即判出场,去 history-position 取
netProfit(= pnl + 资金费 + 开平手续费)记回闸并释放名额。盈亏取交易所的
数而不自己按标记价估——估会漏掉费用且方向总偏乐观。历史未落库时留到下轮,
不会漏记。字段名按文档与官方 TS 类型的差异同时兼容 ctime/cTime。

Telegram(live/tg.py,stdlib + aiohttp):推开仓、平仓带已实现盈亏、被硬
约束挡住、报错、对账平仓、跨日结算、启动与停机。不推信号过期跳过(常态,
搬运重连会重放旧信号)与心跳,否则真事会被淹掉。启动那条兼作通道自检。
研究侧 tg_notify.send 改为复用生产的传输层,方向与 signal_bus 一致。

status.sh 增加一条判读:开过仓但 pnl 仍为 0 就是 watch() 出了问题。

实测:8 类消息渲染、_match_hist 的过早/方向不符/币不符/驼峰字段/取最近
五种情形、100 USDT 下 SOL 与 ADA 的端到端空跑(两腿等量,50/50 精确)。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 17:30:11 +08:00

167 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 小额实盘执行器 · 部署
## 为什么是两台机
Bitget 的 API key 绑了 IP 白名单,只能从 AWS 那台发单;信号是新加坡那台采集器
算出来的。于是分工固定成:
```
新加坡(采集/研究机) AWS(生产机)
shadow_hb.py 算信号 ship_signals.py 拉总线
└→ ~/chan-live/state/ └→ /var/lib/chan-live/state/
signals_live.jsonl ──ssh tail──→ signals_live.jsonl
live_exec.py 读总线 → Bitget REST
```
**生产机上不装采集侧的任何东西**Docker / Hummingbot / pandas / chanlun)。
理由不是洁癖,是四条具体代价:
1. `live_state.json` 原先落在 `research/out/`,而那个目录 `shadow_hb.py` 会在
CSV 表头变化时自动 rename 归档、研究脚本会写、人也会手工清数据。那个文件装
的是 `MAX_DAY_LOSS` 累计与已处理信号键,**被清掉不报错,只是两道闸静默
失效**。现在改到 `/var/lib/chan-live`
2. 采集器十币清空 300~560ms,直接叠在信号到达执行器的延迟上。
3. 研究侧的探针 OOM 过一次(14.9 GB、负载 12)。当时若有仓位在场,执行器会被
一起杀掉,只剩交易所侧止损兜着。
4. 依赖面:执行器只需标准库 + `aiohttp`。原先为读两个常量 import 研究侧的
`step43`,把 numpy/pandas/pyarrow 全拖进实盘进程。
`install.sh` 里有一条断言会真的挡住第 4 条回归。
## 机器要求
CPU 无所谓(执行器几乎不算东西,信号 6.8 个/天)。要的是:
- 出口 IP 固定,且已加进 Bitget 该 key 的白名单
- `chrony` 能同步。**这一条是硬要求**`LIVE_STALE_S` 那道闸靠两机时钟一致才
有意义,采集机时钟快 5 分钟就等于把闸放宽 5 分钟,一个早已失效的参考价会被
当成新鲜的照做
- 能 ssh 到采集机(拉总线用)
## 一、生产机(AWS
```bash
# 1. 取代码。只需要 live/ 这一个子树,但整仓克隆更省事
git clone -b chan <repo> /tmp/chan && cd /tmp/chan
sudo ./live/deploy/install.sh
# 或让它自己克隆:sudo REPO=<repo> ./live/deploy/install.sh
# 2. 填密钥与参数
sudo vi /etc/chan-live/live.env
```
`live.env` 里必须改的四项:`BITGET_API_KEY` / `SECRET` / `PASSPHRASE` /
`SHIP_FROM`。密钥权限**只勾只读 + 交易,不要勾提币**。
```bash
# 3. 装拉总线用的 ssh key
sudo -u chan ssh-keygen -t ed25519 -N '' \
-f /var/lib/chan-live/home/.ssh/id_ed25519
sudo cat /var/lib/chan-live/home/.ssh/id_ed25519.pub
# 把这一行加到采集机的 ~/.ssh/authorized_keys
# 4. 空跑验全链(不下真单)
sudo ./live/deploy/dryrun.sh
```
`dryrun.sh` 验的是那些"上线才暴露、且暴露方式是花钱"的环节:密钥能不能用、
IP 白名单对不对、chrony 同步没有、ssh 通不通、对端总线有没有信号、数量与价位
取整合不合交易所规则。**不要跳过。**
```bash
# 5. 真跑
sudo systemctl enable --now chan-live-ship chan-live-exec
./live/deploy/status.sh
```
## 二、采集机(新加坡)
采集器要重启一次才会开始往总线写——`signal_bus.emit` 是后加的,跑着的进程没有
加载。重启会丢已采的几分钟,数据本身不受影响(CSV 是追加的)。
```bash
cd <repo> && git pull
docker stop shadow && docker rm shadow
SHADOW_SITE=sg-tencent SYMS=BTC,ETH,SOL,BNB,XRP,DOGE,ADA,AVAX,LINK,LTC \
bash research/live/deploy/start.sh
```
`start.sh` 会把 `$HOME/chan-live/state` 挂进容器成 `/bus`,总线落在
`$HOME/chan-live/state/signals_live.jsonl`。**总线刻意放在仓库外**,因为它是
交给另一台机的交接点,而仓库会被 git 动。
确认在写:
```bash
ls -la ~/chan-live/state/
# 等一个信号(6.8 个/天,可能要等几小时)
tail -f ~/chan-live/state/signals_live.jsonl
```
## 日常
```bash
./live/deploy/status.sh # 一屏体检
journalctl -u chan-live-exec -f # 执行器日志
journalctl -u chan-live-ship -f # 搬运日志
```
**怎么判健康:** 信号 6.8 个/天,所以"很久没有新信号"是正常的,不能当健康
指标。要看的是 `chan-live-ship` 的心跳(每 5 分钟一条),里面报 ssh 在线时长与
重连次数。管道死了但进程还活着是这里最危险的状态——`ServerAliveInterval=15`
负责让它变成一次可见的断开。
**Telegram**`live.env` 里填 `TG_TOKEN` / `TG_CHAT` 就开。启动时会推一条
"执行器启动",兼作通道自检——配错了当场就知道,而不是等几小时后第一个真信号
来时才发现。推开仓、平仓(带已实现盈亏)、被硬约束挡住、报错、对账平仓、跨日
结算;不推信号过期跳过(常态)和心跳。约 30 条/天上限。
## 停机与回滚
```bash
# 停新开仓,但保留在场仓位的管理(48 分钟超时平仓在执行器进程里)
sudo systemctl stop chan-live-ship
# 全停。执行器收到 SIGTERM 会**撤挂单 + 市价平掉在场仓位**再退出
# (内部平仓上限 60s,systemd 给了 90s 停机窗口)
sudo systemctl stop chan-live-exec
# 代码回滚
cd /opt/chan && sudo git reset --hard <sha> && sudo systemctl restart chan-live-exec
```
⚠️ **状态目录 `/var/lib/chan-live` 不要跟着回滚。** 它存的是当日计数与已处理
信号键;清掉等于日上限归零、且可能重开已经做过的仓。
## 故障处理
| 症状 | 大概率原因 |
|---|---|
| 启动即 `40018` / 签名错 | 出口 IP 不在白名单,或密钥抄错。`curl https://api.ipify.org` 对一下 |
| 搬运日志 `Permission denied (publickey)` | 第 3 步的 pubkey 没加到采集机 |
| 搬运在线但一直没信号 | 采集机没重启过(`signal_bus.emit` 没加载),或对端总线路径不对 |
| 日志 `⛔ 时间倒流 Xs` | 两机时钟不同步,**staleness 闸已不可信**。查两边 `chronyc tracking` |
| 信号收到但都被跳过 | `age > LIVE_STALE_S`。看是搬运慢还是时钟偏;也可能是重连重放的旧信号(这种跳过是对的) |
| `systemctl status` 显示 start-limit-hit | 5 分钟内重启 5 次,systemd 停手了。先看 journal 找真因,再 `systemctl reset-failed` |
| 执行器起不来,报缺 numpy/pandas | 有人给生产侧加了研究侧的 import。`install.sh` 的依赖断言就是挡这个 |
## 已知的退化边界
- **进程死掉不会变成裸仓。** 止损与止盈都挂在交易所侧(`presetStopLossPrice`
与 post-only reduce-only 限价单),只有 48 分钟超时平仓在本进程。所以进程死
掉的后果是持仓超过 48 根,不是失去保护。
- **崩溃与主动停机的处理不同,是刻意的。** 崩溃后 systemd 几秒内重启,
`reconcile` 接着撤挂单 + 平掉遗留仓位,空窗期由交易所侧止损兜着。主动
`systemctl stop` 则在退出前就平掉——因为停机后没人重启,仓位会一直挂到止损
或止盈,超时腿丢了就不是回测那个出场结构了。
- **断线超过 20s 就等于漏掉那期间的信号。** 重连会把总线重放上来,但旧信号会被
staleness 闸挡掉。这是对的——参考成交价是次根开盘价,过了就不是回测那个价。
- **重启时会平掉交易所上已有的仓位**(`reconcile`)。接管要重建入场价、ATR、
剩余半仓状态和已过根数,任一项猜错就跑成另一个收益结构,所以选择平掉。
- **日亏损上限依赖 `watch()` 循环**。止损与止盈在交易所侧成交,本进程收不到
通知,所以有个 10s 轮询去 `history-position``netProfit`(含手续费与资金
费)记回闸。这个循环停了,`pnl_day` 会恒为 0`MAX_DAY_LOSS` 静默失效。
心跳里会打 `当日 PnL x/-20`,值一直是 0.00 而又确实有平仓,就是它出了问题。
- **平仓后 `MAX_OPEN` 名额由 `watch()` 释放**,不是立刻。最坏延迟 10s。若
历史记录还没落库,会等下一轮,日志里打"历史未就绪,下轮再结算"。