加跨地部署脚本与站点对比,数据全表加 site 列
目的是在第二台机器(AWS)上跑一套完全相同的采集,看不同地理位置的数据差异。 滑点里最大的一项是延迟漂移,而延迟含网络传输,所以机房选址是可优化参数。 数据侧两处必需改动: - 全部输出加 site 列(三张 CSV 加 gzip 里的盘口与成交流)。没有这一列,两台 机器的数据合起来就分不清来源。为免四处 writerow 漏加一处产生静默空值, 改在 _SiteWriter 里统一注入。 - start.sh 在时钟未同步或偏移超 10ms 时**拒绝启动**。所有延迟数字都是 「本地时钟 − 交易所 K 线收盘」,时钟偏 50ms 就全部同向偏 50ms,且不报错, 只会让跨地对比得出一个干净且完全错误的结论。 deploy/ 下四个文件:setup.sh(docker + chrony + 拉镜像)、start.sh(校验时钟、 写运行元数据、起容器)、status.sh(健康速查)、README。运行元数据记 git commit、 镜像摘要、时钟偏移——两地数据对不上时,这三项任一不同都足以解释差异。 compare_sites.py 做配对对比:只取各站都有的 K 线(不取交集可能在比不同时段, 而延迟对市场活跃度敏感),并报配对差的符号占比而非两个中位数相减。已用注入 120ms 的合成数据验证能精确还原。另有一条自检:同一固定延迟点上两站漂移应当 相同——漂移是市场性质,若也差很多则先查时钟与时段对齐。 status.sh 里按列名取字段下标而非写死数字:加 site 列时字段整体右移过一次, 写死 $8 会静默变成读 lag_signal_ms 而非 lag_data_ms。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
# 影子采集器部署
|
||||
|
||||
在第二台机器上跑一套完全相同的采集,用来看不同地理位置的数据差异。
|
||||
|
||||
## 为什么要跨地采集
|
||||
|
||||
滑点里最大的一项是**延迟漂移**:从 K 线收盘到实际下单之间,价格已经走掉的
|
||||
那部分。而延迟 = 交易所出包 + 网络传输 + 本地处理。本机(新加坡)实测到达
|
||||
延迟中位 350~650ms,其中网络传输占多少、换个机房能压掉多少,只有实测。
|
||||
|
||||
延迟压下来直接等于滑点下降,所以机房选址是个可优化参数,不是固定成本。
|
||||
|
||||
## 三个必须一致,一个必须不同
|
||||
|
||||
必须一致,否则差异分不清是地理位置还是环境造成的:
|
||||
|
||||
- **代码版本**(`git_commit`)——同一个 commit
|
||||
- **镜像摘要**(`image_digest`)——同一个 hummingbot 镜像
|
||||
- **时钟**——两台都同步到 NTP,偏移都在 10ms 内
|
||||
|
||||
必须不同:
|
||||
|
||||
- **`SHADOW_SITE`**——写进每一行数据,是合并后区分来源的唯一依据
|
||||
|
||||
`start.sh` 会把这四项连同内核、核数、内存一起写进
|
||||
`research/out/run_meta_<site>.json`。两地数据对不上时先看这个文件。
|
||||
|
||||
## 时钟为什么是硬门槛
|
||||
|
||||
所有延迟数字都是「本地时钟 − 交易所 K 线收盘时间戳」。时钟偏 50ms,全部
|
||||
延迟就同向偏 50ms,而且**不会有任何报错**——只会让跨地对比得出一个干净、
|
||||
自信、且完全错误的结论。所以 `start.sh` 在时钟未同步或偏移超阈值时直接
|
||||
拒绝启动,而不是打个警告了事。
|
||||
|
||||
## 步骤
|
||||
|
||||
在新机器上:
|
||||
|
||||
```bash
|
||||
git clone ssh://jack@git.jackyu66.com:2222/jack/chan.git
|
||||
cd chan && git checkout chan
|
||||
|
||||
bash research/live/deploy/setup.sh # 装 docker + chrony,拉镜像
|
||||
SHADOW_SITE=aws-tokyo bash research/live/deploy/start.sh
|
||||
```
|
||||
|
||||
确认健康:
|
||||
|
||||
```bash
|
||||
bash research/live/deploy/status.sh
|
||||
```
|
||||
|
||||
启动日志里应当看到:
|
||||
|
||||
```
|
||||
[补丁] 覆盖生效:基类取首元素 … 本地取末元素 …
|
||||
成交流已挂 ['BTC', 'ETH', 'SOL']
|
||||
就绪 3.0s · 1m [2001, 2001, 2001] 根 · 5m [801, 801, 801] 根
|
||||
```
|
||||
|
||||
第一行尤其重要。上游 Bitget 连接器的换根解析有 bug(只取多根消息的首元素),
|
||||
补丁把它修掉拿回约 1.06 秒。补丁若失效是静默的——不崩不报错,只是延迟悄悄
|
||||
退回 1.4 秒,所以启动时做了断言。
|
||||
|
||||
## 对比
|
||||
|
||||
把两站的 `research/out/` 收到一处(同名文件会覆盖,所以分目录放):
|
||||
|
||||
```bash
|
||||
mkdir -p collected/sg collected/aws
|
||||
rsync -av sg-box:chan/research/out/ collected/sg/
|
||||
rsync -av aws-box:chan/research/out/ collected/aws/
|
||||
|
||||
python research/live/compare_sites.py \
|
||||
--glob 'collected/*/shadow_latency.csv' \
|
||||
--drift-glob 'collected/*/shadow_drift.csv' \
|
||||
--meta-dir collected/sg --meta-dir collected/aws
|
||||
```
|
||||
|
||||
对比脚本做两件事值得说明:
|
||||
|
||||
- **只取各站都有的 K 线**做配对比较。不取交集就可能在比不同时段,而延迟对
|
||||
市场活跃度敏感。
|
||||
- 报**配对差的符号占比**而不只是两个中位数相减。同根配对消掉了市场状态,
|
||||
「A 比 B 慢的根占多少」比「两个中位数差多少」更能说明有无系统性差异。
|
||||
|
||||
判读上有一条自检:**同一个固定延迟点上,两站的漂移应当几乎相同**——漂移是
|
||||
市场性质,与机器位置无关。若漂移也差很多,先怀疑时钟或时段没对齐,而不是
|
||||
急着下结论。
|
||||
|
||||
## 资源占用
|
||||
|
||||
本机实测:内存约 1.5GB(两个计算进程 + 盘口缓冲),CPU 单核不满。
|
||||
盘口与成交流落盘约 15MB/天(gzip)。一周 168 小时的量级在百 MB 内。
|
||||
|
||||
`--workers 2` 是因为信号计算走独立进程池、不能阻塞事件循环。核数少的机型
|
||||
可以给 1,但要看心跳里的 `compute_ms`:若接近 60 秒就会开始堆积。
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# 影子采集器的机器初始化。幂等,可重复跑。
|
||||
#
|
||||
# 用途:在另一台机器(如 AWS)上部署一套完全相同的采集,用来看不同地理位置
|
||||
# 的数据采集有无差异。要比的主要是延迟——「本地接收 − K线收盘」这个量直接
|
||||
# 取决于机器到交易所的网络距离。
|
||||
#
|
||||
# 时钟同步是硬前置条件,不是可选项。所有延迟数字都是本地时钟减交易所时间戳,
|
||||
# 时钟偏 50ms 就等于所有延迟凭空多(或少)50ms,而且不会有任何报错。所以这里
|
||||
# 装并启用 NTP,start.sh 里还会再校验一次、不合格拒绝启动。
|
||||
#
|
||||
# bash research/live/deploy/setup.sh
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE="${SHADOW_IMAGE:-hummingbot/hummingbot:latest}"
|
||||
|
||||
say() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
|
||||
|
||||
say "系统信息"
|
||||
uname -a
|
||||
echo "内存:$(free -g | awk '/^Mem:/{print $2"GB 总 / "$7"GB 可用"}')"
|
||||
echo "CPU:$(nproc) 核"
|
||||
|
||||
say "安装 docker"
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
echo "已有 docker $(docker --version)"
|
||||
else
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq ca-certificates curl gnupg
|
||||
sudo install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
sudo chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
. /etc/os-release
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||
https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" \
|
||||
| sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq docker-ce docker-ce-cli containerd.io
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
# Amazon Linux 2023
|
||||
sudo dnf install -y -q docker
|
||||
sudo systemctl enable --now docker
|
||||
else
|
||||
echo "不认识的包管理器,请手动装 docker" >&2
|
||||
exit 1
|
||||
fi
|
||||
sudo usermod -aG docker "$USER" || true
|
||||
echo "已装 docker。若本次 shell 无权限,重新登录后再跑 start.sh"
|
||||
fi
|
||||
|
||||
say "启用时钟同步(延迟测量的前置条件)"
|
||||
if command -v chronyc >/dev/null 2>&1; then
|
||||
echo "已有 chrony"
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get install -y -qq chrony
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
sudo dnf install -y -q chrony
|
||||
fi
|
||||
sudo systemctl enable --now chrony 2>/dev/null \
|
||||
|| sudo systemctl enable --now chronyd 2>/dev/null || true
|
||||
sleep 3
|
||||
if command -v chronyc >/dev/null 2>&1; then
|
||||
chronyc tracking | grep -E 'Reference ID|System time|Last offset' || true
|
||||
else
|
||||
timedatectl 2>/dev/null | grep -i synchron || true
|
||||
fi
|
||||
|
||||
say "拉镜像 $IMAGE"
|
||||
docker pull "$IMAGE"
|
||||
docker image inspect "$IMAGE" --format '摘要 {{index .RepoDigests 0}}' 2>/dev/null || true
|
||||
|
||||
say "准备输出目录"
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
mkdir -p "$REPO_ROOT/research/out"
|
||||
echo "$REPO_ROOT/research/out"
|
||||
|
||||
say "完成"
|
||||
cat <<'EOF'
|
||||
下一步:
|
||||
|
||||
SHADOW_SITE=aws-tokyo bash research/live/deploy/start.sh
|
||||
|
||||
SHADOW_SITE 必须显式给且两台机器不能相同——它会写进每一行数据,
|
||||
是之后区分数据来源的唯一依据。
|
||||
EOF
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/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}"
|
||||
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"
|
||||
|
||||
# 运行元数据。两地数据对不上时,先看这个文件——镜像摘要、代码版本、时钟偏移
|
||||
# 三者任一不同都足以解释差异,不必去猜。
|
||||
meta="$OUT/run_meta_${SHADOW_SITE}.json"
|
||||
cat >"$meta" <<EOF
|
||||
{
|
||||
"site": "$SHADOW_SITE",
|
||||
"hostname": "$(hostname)",
|
||||
"started_utc": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
||||
"tz": "$(date +%Z%z)",
|
||||
"clock_offset_ms": ${offset_ms:-null},
|
||||
"git_commit": "$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/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),
|
||||
"mem_gb": $(free -g | awk '/^Mem:/{print $2}')
|
||||
}
|
||||
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" \
|
||||
-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" >/dev/null
|
||||
|
||||
echo "已启动。等启动自检(补丁断言 + 历史回填,约 60 秒)…"
|
||||
sleep 45
|
||||
docker logs "$NAME" 2>&1 | tail -12
|
||||
|
||||
cat <<EOF
|
||||
|
||||
看日志: docker logs -f $NAME
|
||||
看状态: bash research/live/deploy/status.sh
|
||||
停止: docker rm -f $NAME
|
||||
|
||||
启动日志里应能看到:
|
||||
[补丁] 覆盖生效 —— 换根解析补丁有效(缺了会静默慢 1.06 秒)
|
||||
成交流已挂 [...] —— maker 成交率要用
|
||||
就绪 … 根 —— 历史回填完成
|
||||
EOF
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
# 采集器健康速查。跨地部署时两台都跑一遍,对着看。
|
||||
set -uo pipefail
|
||||
|
||||
NAME="${NAME:-shadow}"
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
OUT="$REPO_ROOT/research/out"
|
||||
|
||||
say() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
|
||||
|
||||
say "容器"
|
||||
docker ps -a --filter "name=^${NAME}$" \
|
||||
--format 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}' || true
|
||||
|
||||
say "时钟"
|
||||
if command -v chronyc >/dev/null 2>&1; then
|
||||
chronyc tracking | grep -E 'System time|Last offset|Leap status'
|
||||
fi
|
||||
|
||||
say "最近心跳"
|
||||
docker logs "$NAME" 2>&1 | grep '\[心跳\]' | tail -3 || echo "还没到第一次心跳(每 5 分钟一次)"
|
||||
|
||||
say "告警与异常"
|
||||
docker logs "$NAME" 2>&1 \
|
||||
| grep -E '⚠|错误|失效|损坏|停滞|Traceback|退化' | tail -10 \
|
||||
|| echo "无"
|
||||
|
||||
say "输出规模"
|
||||
for f in shadow_latency.csv shadow_drift.csv shadow_signals.csv; do
|
||||
p="$OUT/$f"
|
||||
if [[ -s "$p" ]]; then
|
||||
printf ' %-22s %8d 行\n' "$f" "$(( $(wc -l <"$p") - 1 ))"
|
||||
elif [[ -f "$p" ]]; then
|
||||
# 已建但表头还没冲刷:signals 只在有信号时才 flush,门控后每天仅 4~6 个
|
||||
printf ' %-22s %8s\n' "$f" "0(待首条)"
|
||||
else
|
||||
printf ' %-22s %8s\n' "$f" "无"
|
||||
fi
|
||||
done
|
||||
for f in shadow_books.jsonl.gz shadow_tape.jsonl.gz; do
|
||||
p="$OUT/$f"
|
||||
[[ -f "$p" ]] && printf ' %-22s %8s\n' "$f" "$(du -h "$p" | cut -f1)" \
|
||||
|| printf ' %-22s %8s\n' "$f" "无"
|
||||
done
|
||||
|
||||
say "各站点行数(确认 site 列生效)"
|
||||
p="$OUT/shadow_latency.csv"
|
||||
if [[ -f "$p" ]]; then
|
||||
awk -F, 'NR>1{c[$1]++} END{for(s in c) printf " %-16s %8d 行\n", s, c[s]}' "$p"
|
||||
else
|
||||
echo " 尚无数据"
|
||||
fi
|
||||
|
||||
say "到达延迟中位(本站,跨地对比的主指标)"
|
||||
# 按列名取下标,不写死数字:加了 site 列之后字段整体右移过一次,
|
||||
# 写死 $8 会静默变成读 lag_signal_ms
|
||||
if [[ -s "$p" ]]; then
|
||||
for sym in BTC ETH SOL; do
|
||||
med=$(awk -F, -v s="$sym" '
|
||||
NR==1 { for (i=1;i<=NF;i++) { if ($i=="sym") si=i; if ($i=="lag_data_ms") li=i } ; next }
|
||||
$si==s && $li!="" { print $li }' "$p" | sort -n | awk '
|
||||
{ v[NR]=$1 } END { if (NR) printf "%.0f %d", v[int((NR+1)/2)], NR }')
|
||||
[[ -n "$med" ]] && printf ' %-5s 中位 %6sms (n=%s)\n' "$sym" ${med} \
|
||||
|| printf ' %-5s 尚无数据\n' "$sym"
|
||||
done
|
||||
echo " 参考:本机(新加坡)补丁后实测 350~650ms,理论下限约 500ms"
|
||||
fi
|
||||
Reference in New Issue
Block a user