接 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>
167 lines
8.0 KiB
Markdown
167 lines
8.0 KiB
Markdown
# 小额实盘执行器 · 部署
|
||
|
||
## 为什么是两台机
|
||
|
||
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。若
|
||
历史记录还没落库,会等下一轮,日志里打"历史未就绪,下轮再结算"。
|