Files
Chan/research/live/latency_compare.py
T
jackandCursor 181bca303f 影子测量改用框架吃单原语,并补齐容量与 maker 成交率两项测算
吃单查询换成 Hummingbot 的 OrderBook.get_vwap_for_volume:手写的 walk_book
返回的是按计价币吃单的加权均价,但框架的 get_price_for_quote_volume 返回
边际价、get_vwap_for_volume 收基础币量,两者语义不同。改为按基础币下单
(真实委托与 PositionExecutor.amount 均是基础币计价),深度不足由
query_volume/result_volume 判定,框架此时返回 nan 而非一个看似正常的
部分成交均价。

落盘完整盘口(双边 50 档)。此前只记三个固定名义额的成交价,这批数据的
寿命就等于那几个档位的寿命;存完整深度后任意资金量级的冲击都能离线重算。
仓位档同时从 1k/5k/20k 提到十万量级,此前低估真实仓位约两个数量级。

订阅成交流,按根按价位聚合。买卖分开存——多头在目标位挂卖出靠主动买盘
成交,混在一起会把成交率高估约一倍。BTC 每根总成交额中位与 210 天历史
的 volume×close 差 0.3%,可确认采集完整。

新增两项测算:
- 冲击不是绑定约束。32 万仓位单边冲击 0.19~2.39bp,对 8.58~20.64bp 的
  预算只占 1.6~14.2%,冲击反推的资金上限 100~500 万。
- maker 成交率才是。止盈位被首次触及时,限价在该根价格区间中的位置
  中位 k=0.28(63.9 万次触及,三币一致);合并每根成交额后,32 万仓位
  的全额成交率仅 30.1%/15.6%/1.5%。要 80% 全额成交,仓位须 ≤ 4.7 万
  /1.4 万/0.26 万——比冲击反推的上限低 40~370 倍。

回测把这些止盈按「全额成交在目标价」计,故预算所依据的收益流本身需重估。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 02:39:47 +08:00

126 lines
4.9 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.
"""把两侧 t_data t_close 对齐,并折算成 bp,与滑点余量对照。
为什么要折算成 bp 才有意义:延迟本身不花钱,花钱的是延迟期间价格的漂移。
按随机游走,t 秒的价格标准差是 σ_1m · √(t/60),其中 σ_1m 是本币 1m 收益
的标准差。入场方向上还有系统性追价(信号触发往往伴随同向动量),所以随机
漂移只是下限,真实成本更高——这也是为什么最终仍要用真实盘口测滑点。
预算从 `lib/shadow_budget` import,不在这里写死。曾经写死的
`{BTC: -0.13, ETH: 4.02, SOL: 2.92}` 是错的——那是 `bitget_baseline.py`
按「毛均 − 6bp 双边 taker」算的,六处口径叠加(费率档位记高、余量没除
taker 名义额、用了 5m~30m 的出场参数、只有同向没有阶梯与 ATR 门控)。
正确值是 8.58 / 20.64 / 16.83ETH 差了五倍。
.venv/bin/python research/live/latency_compare.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
HERE = Path(__file__).resolve().parent
RESEARCH = HERE.parent
sys.path.insert(0, str(RESEARCH))
from lib.shadow_budget import budget_of # noqa: E402
OUT = RESEARCH / "out"
SYMS = ("BTC", "ETH", "SOL")
def vol_bp_per_min() -> dict[str, float]:
"""从 Bitget 缓存算 1m 收益标准差,单位 bp。"""
out = {}
for s in SYMS:
f = HERE / "cache" / f"bitget_{s}_1m_30d.feather"
if not f.exists():
f = HERE / "cache" / f"bitget_{s}_1m_210d.feather"
if not f.exists():
continue
df = pd.read_feather(f)
r = np.log(df["close"].to_numpy(dtype=float))
out[s] = float(np.nanstd(np.diff(r)) * 1e4)
return out
def drift_bp(lag_ms: float, vol: float) -> float:
"""随机游走下,lag 毫秒对应的价格漂移标准差(bp)。"""
return vol * np.sqrt(max(lag_ms, 0) / 60_000.0)
def load(tag: str) -> pd.DataFrame | None:
f = OUT / f"latency_{tag}.csv"
if not f.exists():
return None
df = pd.read_csv(f)
return df if not df.empty else None
def describe(df: pd.DataFrame, name: str, vols: dict) -> None:
print(f"\n########## {name}t_data t_close ##########")
print(f"{'币':<5}{'n':>5}{'中位ms':>9}{'P90ms':>9}{'最大ms':>9}"
f"{'中位漂移bp':>12}{'余量bp':>9}{'占余量':>9}")
for s in SYMS:
v = df[df["sym"] == s]["lag_ms"].to_numpy(dtype=float)
if not len(v):
continue
med, p90 = float(np.median(v)), float(np.percentile(v, 90))
vol = vols.get(s)
d = drift_bp(med, vol) if vol else float("nan")
b = budget_of(s)
share = f"{d / b * 100:.0f}%" if np.isfinite(b) and b > 0 else "—"
print(f"{s:<5}{len(v):>5}{med:>9.0f}{p90:>9.0f}{v.max():>9.0f}"
f"{d:>12.2f}{b:>9.2f}{share:>9}")
def main() -> None:
vols = vol_bp_per_min()
print("1m 收益标准差(bp/分钟,Bitget 缓存实测):")
for s, v in vols.items():
print(f" {s}: {v:.2f}")
a, b = load("ccxt"), load("hummingbot")
if a is None or b is None:
print(f"\n数据未就绪:ccxt={'有' if a is not None else '无'} "
f"hummingbot={'有' if b is not None else '无'}")
if a is not None:
describe(a, "ccxt.pro", vols)
if b is not None:
describe(b, "Hummingbot", vols)
return
describe(a, "ccxt.pro", vols)
describe(b, "Hummingbot", vols)
# 逐根配对才能消掉「不同分钟市场活跃度不同」的干扰
m = a.merge(b, on=["kline_ts", "sym"], suffixes=("_ccxt", "_hb"))
print(f"\n########## 逐根配对(重叠 {len(m)} 根)##########")
if m.empty:
print(" 两侧无重叠 K 线,无法配对;检查采集时间窗是否错开")
return
print(f"{'币':<5}{'n':>5}{'ccxt中位':>10}{'HB中位':>10}"
f"{'差值中位':>10}{'HB更慢占比':>12}{'差值→bp':>10}")
for s in SYMS:
g = m[m["sym"] == s]
if g.empty:
continue
d = (g["lag_ms_hb"] - g["lag_ms_ccxt"]).to_numpy(dtype=float)
vol = vols.get(s)
# 差值转 bp:比较两条路径各自漂移的差,而非直接对差值开方
extra = (drift_bp(float(np.median(g["lag_ms_hb"])), vol)
- drift_bp(float(np.median(g["lag_ms_ccxt"])), vol)) if vol else float("nan")
print(f"{s:<5}{len(g):>5}{np.median(g['lag_ms_ccxt']):>10.0f}"
f"{np.median(g['lag_ms_hb']):>10.0f}{np.median(d):>10.0f}"
f"{(d > 0).mean() * 100:>11.0f}%{extra:>10.2f}")
print("\n判读:差值 → bp 若显著小于余量,说明选哪个运行时不影响结论,"
"可直接用 Hummingbot(生产路径一致);若接近或超过余量,"
"则运行时本身就是成本项,需要单独优化或放弃 1m。")
if __name__ == "__main__":
main()