生产与研究分家:实盘执行器独立成 live/ 子树,加 AWS 部署

实盘要跑在 AWS(API key 绑了 IP 白名单),而信号在新加坡那台算。借这次
把生产从研究侧摘出来,四条具体代价里第一条已经咬过:

1. live_state.json 原先落在 research/out/,而那里 shadow_hb 会自动 rename
   归档、研究脚本会写、人也手工清过。那文件装的是 MAX_DAY_LOSS 累计与已
   处理信号键,被清掉不报错,只是两道闸静默失效。改到 LIVE_HOME。
2. 采集器十币清空 300~560ms 直接叠在信号到达执行器的延迟上。
3. 研究侧探针 OOM 过一次(14.9GB),当时若有仓位在场会连坐执行器。
4. 为读两个常量 import 研究侧 step43,把 numpy/pandas/pyarrow 拖进实盘
   进程。抽出 stdlib-only 的 live/exit_params.py,install.sh 加断言挡回归。

新增 live/ship_signals.py:AWS 侧 ssh tail 拉总线,每次重连从文件头重放
+ 按幂等键去重,断线期间的信号自愈;旧信号由 staleness 闸挡掉不补做。
带时钟倒流检测——两机时钟不同步会让那道闸静默放宽。

部署件:systemd 两单元(搬运挂了执行器仍管在场仓位的超时平仓)、
install.sh、dryrun.sh(验密钥/白名单/时钟/ssh/取整)、status.sh、README。

验证:live_exec 重构后端到端空跑,SOL 多头与 ADA 空头的止损/两级止盈/
数量取整逐项核对正确,isolated + post_only + reduceOnly 都在;搬运的去重、
重启不重复追加、脏数据跳过、断线重连重放均已测。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jack
2026-08-28 17:03:03 +08:00
co-authored by Cursor
parent af029bc26e
commit 335d891478
16 changed files with 884 additions and 15 deletions
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# 在生产机(有 Bitget API key 白名单的那台)上装实盘执行器。
#
# 只装执行侧:标准库 + aiohttp。**不装** Docker / Hummingbot / pandas /
# chanlun 引擎——那些是采集与研究侧的依赖,理由见 live/live_exec.py 文件头。
#
# sudo ./install.sh # 从当前 checkout 安装
# sudo REPO=git@host:jack/chan.git ./install.sh # 或指定远程克隆
#
# 幂等:重复跑只会更新代码与依赖,不动 /etc/chan-live/live.env 和状态目录。
set -euo pipefail
APP=/opt/chan
STATE=/var/lib/chan-live
CONF=/etc/chan-live
USER_NAME=chan
BRANCH="${BRANCH:-chan}"
REPO="${REPO:-}"
die() { echo "$*" >&2; exit 1; }
say() { echo " $*"; }
[[ $EUID -eq 0 ]] || die "要 rootsudo $0"
# ── 1. 系统依赖 ────────────────────────────────────────────────────
say "装系统包"
if command -v apt-get >/dev/null; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
# chrony 不是可选项:staleness 闸靠两机时钟一致才有意义,
# 采集机时钟快 5 分钟就等于把闸放宽 5 分钟(见 ship_signals.py
apt-get install -y -qq python3-venv python3-pip git chrony openssh-client
elif command -v dnf >/dev/null; then
dnf install -y -q python3 python3-pip git chrony openssh-clients
else
die "只认 apt/dnf,其他发行版请手工装 python3-venv git chrony"
fi
systemctl enable --now chrony 2>/dev/null || systemctl enable --now chronyd
# ── 2. 专用用户与目录 ──────────────────────────────────────────────
if ! id -u "$USER_NAME" >/dev/null 2>&1; then
say "建系统用户 $USER_NAME(无登录 shell"
useradd --system --home-dir "$STATE/home" --create-home \
--shell /usr/sbin/nologin "$USER_NAME"
fi
install -d -o "$USER_NAME" -g "$USER_NAME" -m 750 "$STATE" "$STATE/state" "$STATE/home"
install -d -o "$USER_NAME" -g "$USER_NAME" -m 700 "$STATE/home/.ssh"
install -d -o root -g "$USER_NAME" -m 750 "$CONF"
# ── 3. 代码 ────────────────────────────────────────────────────────
if [[ -n "$REPO" ]]; then
if [[ -d "$APP/.git" ]]; then
say "更新已有 checkout"
git -C "$APP" fetch --quiet origin "$BRANCH"
git -C "$APP" checkout --quiet "$BRANCH"
git -C "$APP" reset --hard --quiet "origin/$BRANCH"
else
say "克隆 $REPO"
rm -rf "$APP"; git clone --quiet --branch "$BRANCH" "$REPO" "$APP"
fi
else
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
[[ -f "$SRC/live/live_exec.py" ]] || die "$SRC 不像仓库根(缺 live/live_exec.py"
if [[ "$SRC" != "$APP" ]]; then
say "$SRC 同步代码到 $APP"
install -d "$APP"
# 只同步生产要的那一个子树。研究侧的 research/ 不上生产机:
# 它带 pandas/pyarrow/hummingbot,而且探针 OOM 过一次(14.9GB)
cp -a "$SRC/live" "$APP/"
fi
fi
chown -R root:root "$APP/live"
find "$APP/live" -type f -exec chmod 644 {} + ; chmod 755 "$APP/live" "$APP/live/deploy"
chmod 755 "$APP"/live/deploy/*.sh
# ── 4. venv ───────────────────────────────────────────────────────
say "建 venv 并装依赖(应当只有 aiohttp"
[[ -d "$APP/.venv" ]] || python3 -m venv "$APP/.venv"
"$APP/.venv/bin/pip" install --quiet --upgrade pip
"$APP/.venv/bin/pip" install --quiet -r "$APP/live/requirements.txt"
# 断言生产进程没被拖进重量级依赖。这条会真挡住——曾经为读两个常量
# import 研究侧的 step43,把 numpy/pandas/pyarrow 全拉进实盘进程
say "验依赖面"
"$APP/.venv/bin/python" - <<'PY' || die "生产进程拖进了重量级依赖,看上面输出"
import sys
sys.path.insert(0, "/opt/chan/live")
import live_exec
live_exec.assert_decomposable()
heavy = [m for m in ("numpy", "pandas", "pyarrow", "hummingbot", "scipy")
if m in sys.modules]
if heavy:
print(f" ⛔ 启动路径加载了 {heavy}")
raise SystemExit(1)
print(f" ✓ 只有标准库 + aiohttp · 出场结构 "
f"{live_exec.SL_ATR}/{live_exec.SCALE_ATR}/{live_exec.RUNNER_ATR} ATR "
f"/{live_exec.MAXB} 根")
PY
# ── 5. 配置模板 ────────────────────────────────────────────────────
if [[ ! -f "$CONF/live.env" ]]; then
say "写配置模板 $CONF/live.env(密钥要你手工填)"
install -o root -g "$USER_NAME" -m 640 \
"$APP/live/deploy/live.env.example" "$CONF/live.env"
NEED_FILL=1
else
say "$CONF/live.env 已存在,不动"
fi
# ── 6. systemd ────────────────────────────────────────────────────
say "装 systemd 单元"
install -m 644 "$APP"/live/deploy/chan-live-*.service /etc/systemd/system/
systemctl daemon-reload
echo
echo "装好了。接下来按顺序做(**不要**跳过空跑那步):"
echo
if [[ -n "${NEED_FILL:-}" ]]; then
echo " 1. 填密钥与参数:sudo vi $CONF/live.env"
echo " BITGET_API_KEY / SECRET / PASSPHRASE 用只读+交易权限,"
echo " **不要开提币权限**。SHIP_FROM 填采集机的 ssh 目标。"
echo
fi
echo " 2. 装拉总线用的 ssh key,并确认能连上采集机:"
echo " sudo -u $USER_NAME ssh-keygen -t ed25519 -N '' -f $STATE/home/.ssh/id_ed25519"
echo " # 把 $STATE/home/.ssh/id_ed25519.pub 加到采集机的 authorized_keys"
echo " sudo -u $USER_NAME ssh -o BatchMode=yes <采集机> 'echo ok'"
echo
echo " 3. 空跑验全链(不下真单,跑够看到一次心跳再停):"
echo " sudo $APP/live/deploy/dryrun.sh"
echo
echo " 4. 真跑:"
echo " sudo systemctl enable --now chan-live-ship chan-live-exec"
echo " $APP/live/deploy/status.sh"