Files
Chan/research/live/compare_sites.py
T
jackandCursor 631d97e493 加跨地部署脚本与站点对比,数据全表加 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>
2026-08-28 02:39:47 +08:00

185 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""对比两个(或多个)采集站点的数据差异。
部署第二台机器的目的就是这个:延迟是「本地接收 − 交易所 K 线收盘」,直接
取决于机器到交易所的网络距离,换个地理位置这个数会变。而滑点里最大的一项
是延迟漂移,所以站点选址本身就是一个可优化的参数。
## 判读前必须先看的两件事
1. **时钟。** 两台机器的时钟偏移差多少,延迟对比就凭空差多少,且不报错。
run_meta_*.json 里有各站启动时的 chrony 偏移,先确认都在 10ms 内。
2. **同期。** 只比两站都有数据的那些 kline_ts。不取交集的话,比的可能是
不同时段的市场状态,而延迟对市场活跃度是敏感的。
## 用法
把各站的 research/out/ 收到一处(文件名相同会覆盖,所以先按站点改名或
分目录放),然后:
python research/live/compare_sites.py --glob 'collected/*/shadow_latency.csv'
"""
from __future__ import annotations
import argparse
import glob
import json
from pathlib import Path
import numpy as np
import pandas as pd
def load(patterns: list[str]) -> pd.DataFrame:
paths: list[str] = []
for p in patterns:
paths.extend(sorted(glob.glob(p)))
if not paths:
raise SystemExit(f"没有匹配到文件:{patterns}")
frames = []
for p in paths:
df = pd.read_csv(p)
if "site" not in df.columns:
raise SystemExit(
f"{p} 没有 site 列。这是 2026-08-28 之前采的旧数据,"
f"无法确定来源,不能用于跨地对比")
df["_src"] = p
frames.append(df)
out = pd.concat(frames, ignore_index=True)
print(f"读入 {len(paths)} 个文件、{len(out):,} 行、"
f"站点 {sorted(out['site'].unique())}")
return out
def show_meta(out_dirs: list[Path]) -> None:
print("\n########## 一、运行元数据 ##########")
metas = []
for d in out_dirs:
metas.extend(sorted(d.glob("run_meta_*.json")))
if not metas:
print(" 没找到 run_meta_*.json。时钟偏移与代码版本无法核对——")
print(" 两站数据若有差异,分不清是地理位置还是环境不同造成的")
return
rows = []
for m in metas:
try:
rows.append(json.loads(m.read_text()))
except Exception as e:
print(f" {m.name} 读取失败:{e!r}")
if not rows:
return
df = pd.DataFrame(rows)
keep = [c for c in ("site", "clock_offset_ms", "git_commit", "git_dirty",
"image_digest", "nproc", "mem_gb", "tz",
"started_utc") if c in df.columns]
print(df[keep].to_string(index=False))
if "clock_offset_ms" in df and df["clock_offset_ms"].notna().any():
o = df["clock_offset_ms"].astype(float)
spread = float(o.max() - o.min())
flag = "" if spread < 5 else " ⚠ 这个差会直接叠加到延迟对比上"
print(f"\n 站点间时钟偏移极差 {spread:.3f}ms{flag}")
if "git_commit" in df and df["git_commit"].nunique() > 1:
print(" ⚠ 各站代码版本不同,差异可能来自代码而非地理位置")
if "git_dirty" in df and df["git_dirty"].any():
print(" ⚠ 有站点带未提交改动,无法复现")
def compare_latency(df: pd.DataFrame, col: str = "lag_data_ms") -> None:
"""延迟对比。只取各站都有的 kline_ts,避免比到不同时段。"""
if col not in df.columns:
print(f"\n没有 {col} 列")
return
sites = sorted(df["site"].unique())
if len(sites) < 2:
print(f"\n只有一个站点({sites[0]}),无从对比。"
f"等第二台机器的数据到齐")
return
print(f"\n########## 二、到达延迟({col} ##########")
print("\n 全量(各站各自的样本,时段可能不同)")
for s in sites:
x = df[df["site"] == s][col].dropna().astype(float)
print(f" {s:<16} n={len(x):>6} 中位 {x.median():>7.0f}ms "
f"P90 {np.percentile(x, 90):>7.0f}ms "
f"P99 {np.percentile(x, 99):>7.0f}ms")
# 取交集:同一根 K 线在各站都有记录
key = ["sym", "kline_ts"]
piv = df.pivot_table(index=key, columns="site", values=col,
aggfunc="first")
both = piv.dropna()
if both.empty:
print("\n 各站没有共同的 K 线。可能是采集时段不重叠,")
print(" 或 kline_ts 对不上(先查两站时区与时钟)")
return
print(f"\n 同根对比({len(both):,} 根 K 线,各站都有)")
for s in sites:
x = both[s].astype(float)
print(f" {s:<16} 中位 {x.median():>7.0f}ms "
f"P90 {np.percentile(x, 90):>7.0f}ms")
base = sites[0]
for s in sites[1:]:
d = (both[s] - both[base]).astype(float)
# 配对差的符号检验:同根配对消掉了市场状态,比两个中位数相减干净
n_pos = int((d > 0).sum())
print(f"\n {s} {base}:中位差 {d.median():+.0f}ms "
f"· 均值差 {d.mean():+.0f}ms")
print(f" {s} 更慢的根占 {n_pos / len(d) * 100:.1f}%"
f"50% 表示无系统性差异)")
for sym in sorted(both.index.get_level_values("sym").unique()):
ds = d.xs(sym, level="sym")
print(f" {sym:<5} 中位差 {ds.median():+7.0f}ms (n={len(ds)})")
def compare_drift(patterns: list[str]) -> None:
"""漂移对比。延迟差若能兑换成漂移差,才是钱上的差别。"""
paths: list[str] = []
for p in patterns:
paths.extend(sorted(glob.glob(p)))
if not paths:
return
frames = []
for p in paths:
d = pd.read_csv(p)
if "site" in d.columns:
frames.append(d)
if not frames:
return
df = pd.concat(frames, ignore_index=True)
if df["site"].nunique() < 2:
return
print("\n########## 三、延迟漂移(无条件,每根都记) ##########")
for label in sorted(df["delay_label"].dropna().unique()):
sub = df[df["delay_label"] == label]
line = f" {label:>7}"
for s in sorted(sub["site"].unique()):
x = sub[sub["site"] == s]["drift_bp_long"].dropna().astype(float)
if len(x):
line += f" · {s} {x.abs().median():.3f}bp(n={len(x)})"
print(line)
print("\n 同一个固定延迟点上,两站的漂移应当几乎相同——漂移是市场性质,")
print(" 与机器位置无关。若差异明显,先查时钟与采集时段是否对齐")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--glob", action="append", default=None,
help="shadow_latency.csv 的路径模式,可给多次")
ap.add_argument("--drift-glob", action="append", default=None)
ap.add_argument("--meta-dir", action="append", default=None)
a = ap.parse_args()
lat = a.glob or ["research/out/shadow_latency.csv",
"collected/*/shadow_latency.csv"]
drf = a.drift_glob or ["research/out/shadow_drift.csv",
"collected/*/shadow_drift.csv"]
metas = [Path(p) for p in (a.meta_dir or ["research/out", "collected"])]
show_meta([p for p in metas if p.is_dir()])
df = load(lat)
compare_latency(df)
compare_drift(drf)
if __name__ == "__main__":
main()