README 里原先写「执行器收到 SIGINT 会先平掉在场仓位再退出」,但代码里 完全没有信号处理——asyncio.run 外面连 except KeyboardInterrupt 都没有。 断言了一个不存在的行为,现在把它实现。 为什么停机要平仓而崩溃不用:崩溃后 systemd/docker 几秒内重启,reconcile 接着清掉遗留仓位,空窗期有交易所侧止损兜着。而主动停机后没人重启,仓位会 一直挂到止损或止盈,48 分钟超时腿丢了,跑的就不是回测那个出场结构。 必须显式挂 SIGTERM:docker stop 与 systemd stop 默认发的都是它,而 Python 对 SIGTERM 不抛 KeyboardInterrupt,不挂就是直接消失、没有任何清理。挂上后 systemd 单元里 KillSignal=SIGINT 那个绕法也去掉了。 主循环因异常退出时同样走停机流程,不把仓位留给已经没人管的进程。 顺带修掉 "Unclosed client session"(bitget_rest 本有 close() 但没人调, Restart=always 下会漏 socket)。 实测:发 SIGTERM 后走完停机流程、无泄漏警告。 Co-authored-by: Cursor <cursoragent@cursor.com>
小额实盘执行器 · 部署
为什么是两台机
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)。 理由不是洁癖,是四条具体代价:
live_state.json原先落在research/out/,而那个目录shadow_hb.py会在 CSV 表头变化时自动 rename 归档、研究脚本会写、人也会手工清数据。那个文件装 的是MAX_DAY_LOSS累计与已处理信号键,被清掉不报错,只是两道闸静默 失效。现在改到/var/lib/chan-live。- 采集器十币清空 300~560ms,直接叠在信号到达执行器的延迟上。
- 研究侧的探针 OOM 过一次(14.9 GB、负载 12)。当时若有仓位在场,执行器会被 一起杀掉,只剩交易所侧止损兜着。
- 依赖面:执行器只需标准库 +
aiohttp。原先为读两个常量 import 研究侧的step43,把 numpy/pandas/pyarrow 全拖进实盘进程。
install.sh 里有一条断言会真的挡住第 4 条回归。
机器要求
CPU 无所谓(执行器几乎不算东西,信号 6.8 个/天)。要的是:
- 出口 IP 固定,且已加进 Bitget 该 key 的白名单
chrony能同步。这一条是硬要求:LIVE_STALE_S那道闸靠两机时钟一致才 有意义,采集机时钟快 5 分钟就等于把闸放宽 5 分钟,一个早已失效的参考价会被 当成新鲜的照做- 能 ssh 到采集机(拉总线用)
一、生产机(AWS)
# 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。密钥权限只勾只读 + 交易,不要勾提币。
# 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 通不通、对端总线有没有信号、数量与价位
取整合不合交易所规则。不要跳过。
# 5. 真跑
sudo systemctl enable --now chan-live-ship chan-live-exec
./live/deploy/status.sh
二、采集机(新加坡)
采集器要重启一次才会开始往总线写——signal_bus.emit 是后加的,跑着的进程没有
加载。重启会丢已采的几分钟,数据本身不受影响(CSV 是追加的)。
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 动。
确认在写:
ls -la ~/chan-live/state/
# 等一个信号(6.8 个/天,可能要等几小时)
tail -f ~/chan-live/state/signals_live.jsonl
日常
./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
负责让它变成一次可见的断开。
停机与回滚
# 停新开仓,但保留在场仓位的管理(48 分钟超时平仓在执行器进程里)
sudo systemctl stop chan-live-ship
# 全停。执行器收到 SIGINT 会先平掉在场仓位再退出,给了 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 根,不是失去保护。 - 断线超过 20s 就等于漏掉那期间的信号。 重连会把总线重放上来,但旧信号会被 staleness 闸挡掉。这是对的——参考成交价是次根开盘价,过了就不是回测那个价。
- 重启时会平掉交易所上已有的仓位(
reconcile)。接管要重建入场价、ATR、 剩余半仓状态和已过根数,任一项猜错就跑成另一个收益结构,所以选择平掉。