研究侧的 fast_bsp3 一直只活在 research/lib/ 里,web 端看不到,回测与目视 两条线对不上。这次把它搬进引擎,作为独立的第四类买卖点。 之所以单独立类而不是当作 B3/S3 的低滞后版:step30/31 显示引擎原生的 B3/S3 统计上呈逆势、显著亏损(胜率 27.4%、PF 0.66、t −18.76),而同一组 过滤器把 B4 从 PF 1.59 提到 2.26 却对它无效(0.66→0.71)。两者选的是 不同的交易群体,不是同一信号的早晚两版。 - chanlun/analysis/fast_bsp.py 原样搬入 find_fast_bsp3 与 build_htf_zones, 另加 add_zone_ladder / htf_fx_timeline / attach_htf_agree - research/lib/ 两个模块改为转发,所有 step 脚本导入不变,信号逐条比对一致 - 大级别上下文用 resample 从同一份 df 构建,不额外拉数据,因此与界面上选的 周期和时间范围无关 - 前端三个复选框 + 过滤模式下拉;未过滤的原始信号用浅色,避免与主口径混淆 Co-authored-by: Cursor <cursoragent@cursor.com>
149 lines
4.5 KiB
Python
149 lines
4.5 KiB
Python
"""第四类买卖点(B4/S4)接入 TF_DF。
|
||
|
||
判定逻辑全在 chanlun/analysis/fast_bsp.py,这里只负责把引擎的中枢/K线喂进去,
|
||
再把结果包成 ChanFastBSP。
|
||
|
||
刻意不在 init_TF_DF 里默认计算:现有构造路径的开销保持不变,由调用方按需触发。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
|
||
import pandas as pd
|
||
|
||
from chanlun.analysis.fast_bsp import (
|
||
add_zone_ladder,
|
||
attach_htf_agree,
|
||
attach_zone_ladder,
|
||
ensure_timestamp,
|
||
find_fast_bsp3,
|
||
htf_fx_timeline,
|
||
zones_from_zs_list,
|
||
)
|
||
from chanlun.core.ChanEnum import Chan_BSP_DIR
|
||
from chanlun.core.ChanFastBSP import ChanFastBSP
|
||
|
||
# 区间套配对:小级别出中枢与买卖点,大级别只出分型定方向。
|
||
# 取值来自 research/HANDOFF.md §1.5,是回测里实际用过的组合。
|
||
FAST_BSP_HTF_PAIR = {
|
||
'1m': '5m',
|
||
'5m': '30m',
|
||
'15m': '1h',
|
||
'30m': '2h',
|
||
}
|
||
|
||
# 未列入配对表的周期回落到这个倍数
|
||
FAST_BSP_HTF_FALLBACK_RATIO = 4
|
||
|
||
_TF_UNIT_MINUTES = {'m': 1, 'h': 60, 'd': 1440, 'w': 10080}
|
||
|
||
|
||
def timeframe_minutes(tf: str) -> int | None:
|
||
"""'30m' -> 30,'2h' -> 120。无法解析时返回 None。"""
|
||
if not tf:
|
||
return None
|
||
m = re.fullmatch(r'(\d+)\s*([mhdw])', str(tf).strip().lower())
|
||
if not m:
|
||
return None
|
||
return int(m.group(1)) * _TF_UNIT_MINUTES[m.group(2)]
|
||
|
||
|
||
def resolve_htf(tf: str) -> tuple[str, int] | None:
|
||
"""给小级别找配套的大级别,返回 (标签, 分钟数)。"""
|
||
minutes = timeframe_minutes(tf)
|
||
if minutes is None:
|
||
return None
|
||
paired = FAST_BSP_HTF_PAIR.get(str(tf).strip().lower())
|
||
if paired:
|
||
return paired, timeframe_minutes(paired)
|
||
return f'{minutes * FAST_BSP_HTF_FALLBACK_RATIO}m', minutes * FAST_BSP_HTF_FALLBACK_RATIO
|
||
|
||
|
||
class FastBspBuilderMixin:
|
||
def build_fast_bsp_htf(self, df, timeframe=None):
|
||
"""对同一份 df 重采样得到大级别,不额外拉数据。
|
||
|
||
大级别只用来取分型方向,样本太少就没有过滤意义,故重采样后不足 60 根时放弃。
|
||
"""
|
||
tf = timeframe or getattr(self, 'timeframe', None)
|
||
htf = resolve_htf(tf)
|
||
ltf_minutes = timeframe_minutes(tf)
|
||
if htf is None or not ltf_minutes:
|
||
return None
|
||
label, minutes = htf
|
||
if not minutes or len(df) * ltf_minutes < minutes * 60:
|
||
return None
|
||
try:
|
||
from chanlun.pipeline.timeframe import TF_DF
|
||
|
||
return TF_DF(df, minutes, label)
|
||
except Exception:
|
||
return None
|
||
|
||
def cal_fast_bsp(self, df=None, bi_zs_list=None, htf_chan=None, with_htf=True,
|
||
timeframe=None, **kw):
|
||
"""算第四类买卖点,返回 ChanFastBSP 列表。
|
||
|
||
bi_zs_list 传入已算好的 pure 笔中枢可免去重复计算。
|
||
with_htf=False 时跳过大级别构建,只留 ladder_ok 这一个过滤标志。
|
||
kw 透传给 find_fast_bsp3(scan / pullback_win / tol / require_touch 等)。
|
||
"""
|
||
src = df if df is not None else getattr(self, 'dataframe', None)
|
||
if src is None or len(src) == 0:
|
||
self.fast_bsp_list = []
|
||
return self.fast_bsp_list
|
||
|
||
src = ensure_timestamp(src)
|
||
if bi_zs_list is None:
|
||
bi_zs_list = getattr(self, 'bi_zs_list', None)
|
||
if not bi_zs_list:
|
||
bi_zs_list = self.cal_bi_zs_list_pure(self.cal_bi_list(self.get_klc_list(self.cal_kl_data(src))))
|
||
|
||
zones = zones_from_zs_list(bi_zs_list, src)
|
||
if zones.empty:
|
||
self.fast_bsp_list = []
|
||
return self.fast_bsp_list
|
||
|
||
zones = add_zone_ladder(zones)
|
||
sig = find_fast_bsp3(src, zones, **kw)
|
||
if sig.empty:
|
||
self.fast_bsp_list = []
|
||
return self.fast_bsp_list
|
||
|
||
sig = attach_zone_ladder(sig, zones)
|
||
|
||
if with_htf:
|
||
if htf_chan is None:
|
||
htf_chan = self.build_fast_bsp_htf(src, timeframe)
|
||
sig = attach_htf_agree(sig, src, htf_fx_timeline(htf_chan) if htf_chan is not None else pd.DataFrame())
|
||
else:
|
||
sig['htf_dir'] = None
|
||
sig['htf_agree'] = None
|
||
|
||
times = src['date'].dt.strftime('%Y-%m-%d %H:%M:%S').to_numpy()
|
||
close = src['close'].to_numpy(dtype=float)
|
||
|
||
out = []
|
||
for r in sig.itertuples(index=False):
|
||
entry_idx = int(r.entry_idx)
|
||
agree = getattr(r, 'htf_agree', None)
|
||
htf_dir = getattr(r, 'htf_dir', None)
|
||
out.append(ChanFastBSP(
|
||
time=times[entry_idx],
|
||
price=close[entry_idx],
|
||
ddir=Chan_BSP_DIR.BUY if r.direction == 1 else Chan_BSP_DIR.SELL,
|
||
entry_idx=entry_idx,
|
||
bo_time=times[int(r.bo_idx)],
|
||
pb_time=times[int(r.pb_idx)] if r.pb_idx == r.pb_idx else None,
|
||
lag=r.lag,
|
||
depth=r.depth,
|
||
zg=r.zg,
|
||
zd=r.zd,
|
||
occ=r.occ,
|
||
htf_dir=None if htf_dir is None or htf_dir != htf_dir else int(htf_dir),
|
||
htf_agree=None if agree is None or agree != agree else bool(agree),
|
||
ladder_ok=bool(r.ladder_ok),
|
||
))
|
||
self.fast_bsp_list = out
|
||
return out
|