用户指出本机选新加坡是因为 Bitget 机房在新加坡。查证下来 ws.bitget.com 解析 到的是 CloudFront(dxotqhr62n6z4.cloudfront.net),落在新加坡 AS16509 (Amazon),即连的是 AWS 的 CDN 边缘而非 Bitget 自有机房。 腾讯云 ap-singapore(AS132203)到该边缘实测:ICMP 往返 2.1ms、TCP 握手 3.3ms、TLS 完成 9.0ms、首字节 87.8ms。首字节减 TLS 那约 79ms 是 CloudFront 回源开销,与我们的位置无关。 延迟构成(33 根样本):数据到达中位 506ms、信号计算中位 646ms、合计 1315ms。 随机房位置变化的只有那 2ms 往返,占总延迟 0.15%。而 646ms 的信号计算是每根 在 2000 根 1m 加 800 根 5m 上重建缠论结构,本机 2 核、2 个计算进程,三币同时 收盘时第三个还要排队——这才是有改善空间的一项。 因此: - 新增 deploy/netprobe.sh,把网络那一段单独量出来并入运行元数据。用到达 延迟去比两个机房等于用公斤秤称克,必须把可变的那段拿出来单独看。 - compare_sites.py 主指标改为 compute_ms,lag_data_ms 降为自检项(两站应当 接近;若差很多,先怀疑时钟而非网络)。元数据表加 nproc/cpu_model/往返。 - README 改写:第二台机器该测 CPU 规格而非地理位置,WORKERS 按核数减一给, 币数多于 worker 数时排队时间直接计入 compute_ms。 另修正站点标签:本机是腾讯云而非 Hetzner,sg-hetzner → sg-tencent,标错的 33 行数据已清掉重采。 Co-authored-by: Cursor <cursoragent@cursor.com>
212 lines
8.7 KiB
Python
212 lines
8.7 KiB
Python
"""对比两个(或多个)采集站点的数据差异。
|
||
|
||
## 该比什么(2026-08-28 实测修正)
|
||
|
||
原以为该比地理位置,实测下来不是。腾讯云新加坡的延迟构成:
|
||
|
||
数据到达 506ms 其中网络仅 2ms 往返(ws.bitget.com 是 CloudFront 边缘)
|
||
信号计算 646ms 本机 2 核,三币同时收盘还要排队
|
||
合计 1315ms
|
||
|
||
换机房只能动那 2ms。**主指标是 compute_ms,不是 lag_data_ms。**
|
||
|
||
lag_data_ms 仍然要看,但作用是**自检**:两站应当几乎相同;若差很多,先怀疑
|
||
时钟没对齐——那比网络差异的可能性大得多。
|
||
|
||
## 判读前必须先看的两件事
|
||
|
||
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)
|
||
# net 是嵌套 dict,摊平出关心的几项
|
||
if "net" in df.columns:
|
||
for k in ("icmp_min_ms", "tcp_connect_min_ms", "ttfb_min_ms",
|
||
"self_org", "edge_org"):
|
||
df[k] = df["net"].apply(
|
||
lambda v, k=k: v.get(k) if isinstance(v, dict) else None)
|
||
keep = [c for c in ("site", "clock_offset_ms", "nproc", "cpu_model",
|
||
"icmp_min_ms", "ttfb_min_ms", "git_commit",
|
||
"git_dirty", "image_digest", "mem_gb", "tz",
|
||
"started_utc") if c in df.columns]
|
||
print(df[keep].to_string(index=False))
|
||
if "icmp_min_ms" in df and df["icmp_min_ms"].notna().any():
|
||
x = df["icmp_min_ms"].astype(float)
|
||
print(f"\n 到 Bitget 边缘的往返:{x.min():.1f}~{x.max():.1f}ms。"
|
||
f"站间极差 {x.max() - x.min():.1f}ms 就是换机房的全部空间")
|
||
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
|
||
|
||
label = {"compute_ms": "信号计算耗时(主指标,取决于 CPU)",
|
||
"lag_data_ms": "数据到达延迟(自检项,两站应当接近)",
|
||
"lag_signal_ms": "合计到可下单"}.get(col, col)
|
||
print(f"\n########## {label}({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)
|
||
# compute_ms 是主指标:它是延迟里唯一有大幅改善空间的一项(646ms vs
|
||
# 网络的 2ms)。lag_data_ms 放后面,作用是自检两站是否可比
|
||
compare_latency(df, "compute_ms")
|
||
compare_latency(df, "lag_data_ms")
|
||
compare_latency(df, "lag_signal_ms")
|
||
compare_drift(drf)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|