#!/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 "要 root:sudo $0" # ── --sync-env:把模板新增的项追加到已有配置 ──────────────────────── # 独立成一个模式而不是塞进安装流程:追加会改一个装着密钥的文件,这种事应当 # 由你显式发起。只追加缺的项(连它上面的注释一起),**不动**已有任何一行。 if [[ "${1:-}" == "--sync-env" ]]; then EX="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/live.env.example" [[ -f "$EX" ]] || die "找不到模板 $EX" [[ -f "$CONF/live.env" ]] || die "$CONF/live.env 还不存在,先跑一次 sudo $0" cp -a "$CONF/live.env" "$CONF/live.env.bak.$(date +%Y%m%d%H%M%S)" python3 - "$EX" "$CONF/live.env" <<'PY' import re import sys ex_path, live_path = sys.argv[1], sys.argv[2] ex = open(ex_path, encoding="utf-8").read().splitlines() live = open(live_path, encoding="utf-8").read() KEY = re.compile(r"^([A-Z_][A-Z0-9_]*)=") have = set(KEY.match(x).group(1) for x in live.splitlines() if KEY.match(x)) add, block = [], [] for line in ex: if KEY.match(line): k = KEY.match(line).group(1) if k not in have: add += block + [line] block = [] elif line.startswith("#") or (not line.strip() and block): block.append(line) else: block = [] if not add: print(" 配置已是最新,无需追加") raise SystemExit(0) with open(live_path, "a", encoding="utf-8") as f: f.write("\n\n# ── 以下由 install.sh --sync-env 追加 ──\n") f.write("\n".join(add) + "\n") n = sum(1 for x in add if KEY.match(x)) print(f" 追加了 {n} 项:" + " ".join(KEY.match(x).group(1) for x in add if KEY.match(x))) PY say "原文件已备份为 $CONF/live.env.bak.*" say "追加的项多半是空值,逐项填完再重启:sudo systemctl restart chan-live-exec" exit 0 fi # ── 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 已存在,不覆盖(里面是密钥)" # 但要报出模板新增的项。不报的话,以后往 example 里加配置,已有部署会 # **永远拿不到且毫无提示**——静默漂移,等到出事才发现某个开关根本没生效 keys() { grep -oE '^[A-Z_][A-Z0-9_]*=' "$1" | tr -d '=' | sort -u; } MISSING="$(comm -23 <(keys "$APP/live/deploy/live.env.example") \ <(keys "$CONF/live.env") | tr '\n' ' ')" if [[ -n "${MISSING// }" ]]; then say "⚠ 模板比你的配置多了这些项:$MISSING" say " 逐项看说明:$APP/live/deploy/live.env.example" say " 要把缺的那几行连注释一起追加过去,跑:" say " sudo $APP/live/deploy/install.sh --sync-env" NEED_FILL=1 fi OBSOLETE="$(comm -13 <(keys "$APP/live/deploy/live.env.example") \ <(keys "$CONF/live.env") | tr '\n' ' ')" [[ -n "${OBSOLETE// }" ]] && \ say " 另有模板里已没有的项(可能已废弃):$OBSOLETE" 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 echo " 还要写 known_hosts,否则报 Host key verification failed —— 服务跑" echo " 起来会撞同一个墙,因为 BatchMode=yes 不允许交互确认:" echo " sudo -u $USER_NAME ssh-keyscan -t ed25519 <采集机IP> \\" echo " | sudo -u $USER_NAME tee -a $STATE/home/.ssh/known_hosts" echo " sudo -u $USER_NAME ssh-keygen -lf $STATE/home/.ssh/known_hosts" echo " # 把指纹跟采集机上 ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub" echo " # 的输出比一遍。**不要**图省事用 StrictHostKeyChecking=no," echo " # 那等于放弃中间人防护,而这条链路上跑的是下单信号" 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"