吃单查询换成 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>
161 lines
6.9 KiB
Python
161 lines
6.9 KiB
Python
"""影子交易器的信号函数——在子进程里跑,不碰事件循环。
|
||
|
||
单次调用约 0.26s 的纯 CPU,且 chanlun 是纯 Python 受 GIL 限制,放进
|
||
Hummingbot 的 asyncio 循环里会把行情处理一起卡住,所以必须隔离到独立进程。
|
||
|
||
## 口径必须与预算同源,缺一项数就不可比
|
||
|
||
预算(`lib/shadow_budget.BUDGET_BP`)算在 step42 的这套滤网上,
|
||
本文件逐行对齐 `step42_exit_tp_1m.run_one`:
|
||
|
||
同向 h1_agree == 1
|
||
中枢阶梯 多头要求当前中枢整体高于前一个(zd > 前 zg),空头反之
|
||
ATR 门控 atr_pct ≥ ATR_GATE_BP(当前 8bp)
|
||
|
||
早先这里只有 h1_agree。缺阶梯与门控测的就不是我们要交易的那批信号,
|
||
而这一项改常数解决不了——必须改信号路径本身。
|
||
|
||
门控阈值是**费率的函数**不是市场常数(低 ATR 信号的毛质量反而最好,
|
||
断崖只在扣费后出现),换 VIP 档或换交易所要重扫,不要抄 8bp。
|
||
|
||
## 与回测的两点差别
|
||
|
||
其一,这里只关心**最后一根已收盘 K 线**上有没有信号——实盘只能在当下下单。
|
||
其二,`atr_pct` 的分母取次根开盘价,与 `exit_model.walk_exits` 一致,
|
||
所以调用方必须把次根开盘价传进来。
|
||
|
||
未通过滤网的信号也一并返回并打上标志:过滤后样本很稀(门控后 8 个币
|
||
合计约 38 笔/周),未过滤的可作提前读数。但**统计主口径只能用 pass_all**,
|
||
在我们根本不会下单的根上测滑点会把判据算宽。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import warnings
|
||
|
||
warnings.filterwarnings("ignore")
|
||
for _v in ("OMP_NUM_THREADS", "OPENBLAS_NUM_THREADS", "MKL_NUM_THREADS"):
|
||
os.environ.setdefault(_v, "1")
|
||
|
||
|
||
def compute(df_l, df_h, entry_px: float | None = None) -> dict:
|
||
"""在 df_l 的最后一根上找信号。df_l/df_h 都只含已收盘 K 线。
|
||
|
||
entry_px 是次根开盘价(回测 entry_delay=1 的成交价),用作 atr_pct 的
|
||
分母。取不到时退回用信号根收盘价,并在返回里标 atr_ref="close"。
|
||
|
||
返回 dict:
|
||
last_idx 最后一根在 chanlun 处理后 dataframe 里的下标
|
||
n_bars 实际参与计算的根数
|
||
atr_pct 信号根 ATR / 次根开盘价
|
||
hits 命中列表,每项含方向与三个滤网标志、pass_all
|
||
error 出错时的说明,正常为 None
|
||
"""
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
try:
|
||
from chanlun import TF_DF
|
||
from lib.fast_bsp3 import find_fast_bsp3
|
||
from lib.fx_signal import extract_fx_signals, signals_to_frame
|
||
from lib.nested_bsp import attach_htf_context, htf_fx_timeline
|
||
from lib.nested_level import build_htf_zones
|
||
from lib.shadow_budget import ATR_GATE_BP
|
||
|
||
chan_l = TF_DF(df_l, 1, "1m")
|
||
cdf = chan_l.dataframe
|
||
last = len(cdf) - 1
|
||
base = {"last_idx": last, "n_bars": int(len(df_l)), "hits": [],
|
||
"atr_pct": None, "atr_ref": None, "error": None}
|
||
|
||
# ATR 门控。分母与 exit_model.walk_exits 一致,取次根开盘价。
|
||
# 放在任何早退之前——无信号的根也要记,才能在线看到门控的真实刷除率
|
||
atr = float(cdf["atr"].to_numpy(dtype=float)[last]) \
|
||
if "atr" in cdf.columns else float("nan")
|
||
ref = entry_px if (entry_px and np.isfinite(entry_px)) else \
|
||
float(cdf["close"].to_numpy(dtype=float)[last])
|
||
atr_pct = atr / ref if (np.isfinite(atr) and ref) else float("nan")
|
||
gate_ok = bool(np.isfinite(atr_pct) and atr_pct * 1e4 >= ATR_GATE_BP)
|
||
base["atr_pct"] = None if not np.isfinite(atr_pct) else float(atr_pct)
|
||
base["atr_ref"] = "next_open" if (entry_px and np.isfinite(entry_px)) \
|
||
else "close"
|
||
|
||
zones = build_htf_zones(cdf, "1m", chan=chan_l).reset_index(drop=True)
|
||
if zones.empty:
|
||
return base
|
||
|
||
# 中枢阶梯:当前中枢是否整体脱离前一个。与 step42 同一算法
|
||
z = zones.copy()
|
||
prev_zg, prev_zd = z["zg"].shift(), z["zd"].shift()
|
||
z["z_above"], z["z_below"] = z["zd"] > prev_zg, z["zg"] < prev_zd
|
||
z["zone_i"] = np.arange(len(z))
|
||
|
||
sig = find_fast_bsp3(cdf, zones)
|
||
if sig is None or sig.empty:
|
||
return base
|
||
sig = sig.merge(z[["zone_i", "z_above", "z_below"]],
|
||
on="zone_i", how="left")
|
||
|
||
# 5m 同向。算不出时 h1_agree 记 0,该信号自然不会通过 pass_all
|
||
if df_h is not None and len(df_h) > 0:
|
||
chan_h = TF_DF(df_h, 1, "5m")
|
||
hdf = chan_h.dataframe
|
||
tl = htf_fx_timeline(
|
||
signals_to_frame(extract_fx_signals(chan_h, hdf)), hdf)
|
||
sig = attach_htf_context(sig, cdf, tl, "h1")
|
||
else:
|
||
sig["h1_agree"] = 0
|
||
|
||
cur = sig[sig["entry_idx"].astype(int) == last]
|
||
if cur.empty:
|
||
return base
|
||
|
||
hits = []
|
||
for _, r in cur.iterrows():
|
||
d = int(r["direction"])
|
||
push = r["z_above"] if d == 1 else r["z_below"]
|
||
ladder_ok = bool(pd.notna(push) and bool(push))
|
||
# attach_htf_context 在入场时刻之前没有大级别分型时写 NaN。
|
||
# 不能写成 `int(x or 0)`——NaN 是真值,会走到 int(nan) 抛异常,
|
||
# 整根的信号就此丢掉,只留一行报错
|
||
raw = r.get("h1_agree", 0)
|
||
agree = int(raw) if pd.notna(raw) else 0
|
||
hits.append({"direction": d, "h1_agree": agree,
|
||
"ladder_ok": int(ladder_ok), "gate_ok": int(gate_ok),
|
||
"pass_all": int(agree == 1 and ladder_ok and gate_ok)})
|
||
base["hits"] = hits
|
||
return base
|
||
except Exception as e: # 子进程里异常必须带回主进程,否则只见超时不见原因
|
||
import traceback
|
||
return {"last_idx": -1, "n_bars": int(len(df_l)) if df_l is not None else 0,
|
||
"hits": [], "atr_pct": None, "atr_ref": None,
|
||
"error": f"{type(e).__name__}: {e}",
|
||
"traceback": traceback.format_exc()}
|
||
|
||
|
||
NUM_COLS = ("timestamp", "open", "high", "low", "close", "volume")
|
||
|
||
|
||
def _rebuild(rows) -> "object":
|
||
"""只传数值列,date 在这里按 lib/data.py 的同一规则重建。
|
||
|
||
跨进程传 tz-aware 的 datetime 既慢又容易在字符串往返中丢时区,
|
||
而时区若与回测不一致,chanlun 的 K 线标签就会错位。
|
||
"""
|
||
import pandas as pd
|
||
|
||
df = pd.DataFrame(rows, columns=list(NUM_COLS))
|
||
df["timestamp"] = df["timestamp"].astype("int64")
|
||
date = pd.to_datetime(df["timestamp"], unit="ms", utc=True) \
|
||
.dt.tz_convert("Asia/Shanghai")
|
||
df.insert(1, "date", date)
|
||
return df
|
||
|
||
|
||
def compute_packed(payload: tuple) -> dict:
|
||
"""ProcessPoolExecutor 的入口:收 (l_rows, h_rows, entry_px)。"""
|
||
l_rows, h_rows, entry_px = payload
|
||
df_l = _rebuild(l_rows)
|
||
df_h = _rebuild(h_rows) if h_rows else None
|
||
return compute(df_l, df_h, entry_px)
|