#!/usr/bin/env bash # 启动影子采集器。跑之前先 setup.sh。 # # SHADOW_SITE=aws-tokyo bash research/live/deploy/start.sh # SHADOW_SITE=aws-tokyo HOURS=168 WORKERS=2 bash research/live/deploy/start.sh # # 为什么 SHADOW_SITE 必填:它写进每一行数据,是两台机器的数据合起来之后 # 唯一的来源区分。缺了就只能靠文件路径猜,一合并就分不清了。 # # 为什么时钟不同步就拒绝启动:所有延迟数字都是「本地时钟 − 交易所 K 线收盘 # 时间戳」。时钟偏 50ms,全部延迟就凭空偏 50ms,而这**不会有任何报错**, # 只会让跨地对比得出一个完全错误的结论。这类静默错误必须在启动就挡掉。 set -euo pipefail NAME="${NAME:-shadow}" # lean 模式让 TF_DF 只构建到中枢,跳过线段/走势中枢/MACD 状态机。等价性由 # verify_lean_parity.py 在影子这条路径上逐根验过(180 窗口 / 90 命中零分歧)。 # 设 0 可退回 full,用来复量两模式的耗时差。 SHADOW_LEAN="${SHADOW_LEAN:-1}" # 币池。默认三个流动性最好的做滑点测量;十币池是实际要交易的那批(TRX 剔除, # ATR 门控几乎全刷掉)。币数直接决定排队:所有币同一秒收盘,2 核上 10 个币 # 需要约 640ms 墙钟才算完,最后一个币的信号会落在 800ms 哨兵线之外。 SYMS="${SYMS:-BTC,ETH,SOL}" IMAGE="${SHADOW_IMAGE:-hummingbot/hummingbot:latest}" HOURS="${HOURS:-168}" WORKERS="${WORKERS:-2}" MAX_OFFSET_MS="${MAX_OFFSET_MS:-10}" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" OUT="$REPO_ROOT/research/out" die() { printf '\033[31m错误:%s\033[0m\n' "$*" >&2; exit 1; } say() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } [[ -n "${SHADOW_SITE:-}" ]] || die "必须设 SHADOW_SITE,例如 SHADOW_SITE=aws-tokyo。 它写进每一行数据,是跨地对比时区分来源的唯一依据。" say "站点 $SHADOW_SITE" say "校验时钟同步" offset_ms="" if command -v chronyc >/dev/null 2>&1; then if ! chronyc tracking >/dev/null 2>&1; then die "chrony 没在跑。先 sudo systemctl start chrony(或 chronyd)" fi # System time 那行形如 "0.000058703 seconds fast of NTP time" line="$(chronyc tracking | grep '^System time' || true)" secs="$(awk '{print $4}' <<<"$line")" offset_ms="$(awk -v s="$secs" 'BEGIN{printf "%.3f", s*1000}')" echo "$line" leap="$(chronyc tracking | awk -F': *' '/Leap status/{print $2}')" [[ "$leap" == "Normal" ]] || die "chrony leap status = $leap,尚未收敛。等几分钟再试" over="$(awk -v o="$offset_ms" -v m="$MAX_OFFSET_MS" 'BEGIN{print (o>m)?1:0}')" [[ "$over" == "0" ]] || die "时钟偏移 ${offset_ms}ms 超过阈值 ${MAX_OFFSET_MS}ms。 所有延迟测量都会同向偏这么多且不报错,跨地对比会得出错误结论。 先等 chrony 收敛,或调 MAX_OFFSET_MS(不建议)。" echo "偏移 ${offset_ms}ms,在 ${MAX_OFFSET_MS}ms 阈值内" elif command -v timedatectl >/dev/null 2>&1; then timedatectl | grep -qi 'synchronized: yes' \ || die "系统时钟未同步。装 chrony:见 setup.sh" echo "timedatectl 报已同步(无 chronyc,拿不到具体偏移)" else die "既无 chronyc 也无 timedatectl,无法确认时钟。装 chrony 后再启动" fi say "检查镜像" docker image inspect "$IMAGE" >/dev/null 2>&1 || die "没有镜像 $IMAGE,先跑 setup.sh" digest="$(docker image inspect "$IMAGE" --format '{{if .RepoDigests}}{{index .RepoDigests 0}}{{end}}' 2>/dev/null || true)" say "停掉旧容器" docker rm -f "$NAME" >/dev/null 2>&1 || true mkdir -p "$OUT" say "量网络距离" netprobe="$(bash "$(dirname "${BASH_SOURCE[0]}")/netprobe.sh" 2>/dev/null || echo '{}')" echo "$netprobe" | grep -E 'edge_org|self_org|self_city|icmp_min_ms|tcp_connect' || true # 运行元数据。两地数据对不上时,先看这个文件——镜像摘要、代码版本、时钟偏移 # 三者任一不同都足以解释差异,不必去猜。网络探测并在这里,因为「换机房能省 # 多少」这个问题只有它能回答(到达延迟里网络只占约 2ms,用它比等于用公斤秤称克)。 meta="$OUT/run_meta_${SHADOW_SITE}.json" cat >"$meta" </dev/null || echo unknown)", "git_dirty": $(git -C "$REPO_ROOT" diff --quiet 2>/dev/null && echo false || echo true), "image": "$IMAGE", "image_digest": "${digest:-unknown}", "hours": $HOURS, "workers": $WORKERS, "kernel": "$(uname -r)", "nproc": $(nproc), "cpu_model": "$(awk -F': ' '/model name/{print $2; exit}' /proc/cpuinfo 2>/dev/null || echo unknown)", "mem_gb": $(free -g | awk '/^Mem:/{print $2}'), "net": $netprobe } EOF echo "元数据已写 $meta" say "启动容器 $NAME" docker run -d --name "$NAME" -w /home/hummingbot \ --restart unless-stopped \ -e PYTHONPATH=/home/hummingbot:/repo/research:/repo/research/live:/repo \ -e SHADOW_SITE="$SHADOW_SITE" \ -e SHADOW_LEAN="$SHADOW_LEAN" \ -v "$REPO_ROOT:/repo:ro" \ -v "$OUT:/out" \ --entrypoint /opt/conda/envs/hummingbot/bin/python \ "$IMAGE" /repo/research/live/shadow_hb.py \ --hours "$HOURS" --workers "$WORKERS" --syms "$SYMS" >/dev/null echo "已启动。等启动自检(补丁断言 + 历史回填,约 60 秒)…" sleep 45 docker logs "$NAME" 2>&1 | tail -12 cat <