# 小额实盘执行器 · 部署 ## 为什么是两台机 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 /tmp/chan && cd /tmp/chan sudo ./live/deploy/install.sh # 或让它自己克隆:sudo 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 && 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` 负责让它变成一次可见的断开。 ## 停机与回滚 ```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 && 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、 剩余半仓状态和已过根数,任一项猜错就跑成另一个收益结构,所以选择平掉。