吃单查询换成 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>
126 lines
4.9 KiB
Python
126 lines
4.9 KiB
Python
"""把两侧 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.83,ETH 差了五倍。
|
||
|
||
.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()
|