目的是在第二台机器(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>
185 lines
7.4 KiB
Python
185 lines
7.4 KiB
Python
"""对比两个(或多个)采集站点的数据差异。
|
||
|
||
部署第二台机器的目的就是这个:延迟是「本地接收 − 交易所 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()
|