实盘首日丢掉 4 个信号、白跑 5 小时,根因是 40774,但**没有任何推送**。 日志里一直在报,可外在表现和"没信号"一模一样——这正是整套通知设计要防的 那类静默经济损失,却恰好漏了下单失败这一条。 现在:入场被拒或止盈挂不上都推。按信号聚合成一条,不按腿推(避免一个信号 两条)。两腿全失败时说明"这个信号丢了"并带累计失败次数与常见原因;部分腿 失败时说明"收益结构已偏离设计"——那种情况仓位是半的,不是设计的两腿结构。 心跳原先只把数字并排列出来:"已做 4 · 在场 0/3" 那 5 小时一直在打,但没有 一处说这是异常。现在分开计 n_built 与 n_order_fail,做了却一次没建上直接 打 ⛔。只看 n_took 分不出"做了"和"建上了"。 status.sh 同样加判读:统计 entry_fail 次数并打出最后一条错误与码表。 tp_fail 现在也记进 live_trades.jsonl,原先只打日志不落盘。 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
负责让它变成一次可见的断开。
Telegram 在 live.env 里填 TG_TOKEN / TG_CHAT 就开。启动时会推一条
"执行器启动",兼作通道自检——配错了当场就知道,而不是等几小时后第一个真信号
来时才发现。推开仓、平仓(带已实现盈亏)、被硬约束挡住、报错、对账平仓、跨日
结算;不推信号过期跳过(常态)和心跳。约 30 条/天上限。
停机与回滚
# 停新开仓,但保留在场仓位的管理(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。若 历史记录还没落库,会等下一轮,日志里打"历史未就绪,下轮再结算"。