Files
Chan/chanlun/pipeline/builders/fast_bsp.py
T
jackyu66gitandCursor 9b72173285 feat: 第四类买卖点(B4/S4)融入缠论引擎与 web 展示
研究侧的 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>
2026-08-28 00:05:26 +08:00

149 lines
4.5 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.
"""第四类买卖点(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_bsp3scan / 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