影子测量改用框架吃单原语,并补齐容量与 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>
This commit is contained in:
jack
2026-08-28 02:39:47 +08:00
co-authored by Cursor
parent 6259f7380b
commit 181bca303f
10 changed files with 1503 additions and 174 deletions
+61
View File
@@ -14,9 +14,21 @@
再把最后一根交给基类走正常的 append 流程。
已向上游反馈前,本地用子类覆盖,不改动镜像。
## 为什么必须有启动断言
子类覆盖的失效方式是**静默**的:上游若把 `_parse_websocket_message` 改名、
或改走别的钩子,我们的覆盖就成了死代码,行情悄悄退回慢 1.06 秒,不崩、
不报错、不留日志,只会让收益慢慢变差,几周后才从统计里看出来。
`assert_patch_effective()` 不做名字检查——名字对不上未必失效,名字对得上
也未必生效。它喂一条合成的两元素消息,直接验证行为:基类返回首元素(bug
仍在、覆盖仍有必要),子类返回末元素(覆盖确实生效)。再加一条源码检查
确认基类的收包循环还在调这个钩子。任一不满足就在启动时抛错。
"""
from __future__ import annotations
import inspect
from typing import Any, Dict, Optional
import numpy as np
@@ -24,6 +36,7 @@ import numpy as np
from hummingbot.data_feed.candles_feed.bitget_perpetual_candles import (
BitgetPerpetualCandles,
)
from hummingbot.data_feed.candles_feed.candles_base import CandlesBase
def _row_to_dict(row: list, ensure_s) -> Dict[str, Any]:
@@ -63,3 +76,51 @@ class PatchedBitgetPerpetualCandles(BitgetPerpetualCandles):
d["volume"], d["quote_asset_volume"], d["n_trades"],
d["taker_buy_base_volume"], d["taker_buy_quote_volume"]]
).astype(float)
# 换根时 Bitget 推的就是这个形状:[上一根, 新一根]
_PROBE = {
"action": "update",
"arg": {"instType": "USDT-FUTURES", "channel": "candle1m",
"instId": "BTCUSDT"},
"data": [
["1700000040000", "1", "1", "1", "1", "1", "1", "1"],
["1700000100000", "2", "2", "2", "2", "2", "2", "2"],
],
}
def assert_patch_effective() -> None:
"""启动即验证覆盖真的生效,否则抛错。让静默失效变成启动失败。"""
src = inspect.getsource(CandlesBase._process_websocket_messages_task)
if "_parse_websocket_message" not in src:
raise RuntimeError(
"上游收包循环已不再调用 _parse_websocket_message"
"patched_candles 的覆盖失效。需重新定位钩子后再启动。")
stock = BitgetPerpetualCandles("BTC-USDT", "1m", 20)
ours = PatchedBitgetPerpetualCandles("BTC-USDT", "1m", 20)
got_stock = stock._parse_websocket_message(_PROBE)
got_ours = ours._parse_websocket_message(_PROBE)
head_ts = stock.ensure_timestamp_in_seconds(int(_PROBE["data"][0][0]))
tail_ts = stock.ensure_timestamp_in_seconds(int(_PROBE["data"][-1][0]))
if not got_ours or int(got_ours["timestamp"]) != int(tail_ts):
raise RuntimeError(
f"覆盖未生效:子类返回 {got_ours and got_ours.get('timestamp')}"
f"应为末元素 {tail_ts}")
if got_stock and int(got_stock["timestamp"]) == int(tail_ts):
# 上游自己修好了。此时覆盖无害但已多余,明确说出来,免得以后
# 有人以为那 1.06 秒还是靠我们拿回来的
print(" [补丁] 上游已自行修正换根解析,本地覆盖现为冗余,可移除",
flush=True)
elif not got_stock or int(got_stock["timestamp"]) != int(head_ts):
raise RuntimeError(
f"基类行为与预期不符:返回 "
f"{got_stock and got_stock.get('timestamp')}"
f"既非首元素 {head_ts} 也非末元素 {tail_ts}"
f"上游改了解析逻辑,补丁的前提需重新确认。")
else:
print(f" [补丁] 覆盖生效:基类取首元素 {int(head_ts)}"
f"本地取末元素 {int(tail_ts)}", flush=True)