Files
Chan/chanlun/pipeline/builders/bsp.py
T
UbuntuandCursor 206b27fe72 refactor: 以自实现指标替换 talib 与 technical 依赖
chanlun/indicators/ta.py 接口兼容 talib.abstract,实现代码实际用到的
SMA/MA/EMA/RSI/ATR/MACD/BBANDS;chanlun/pipeline/resample.py 替代
technical.util.resample_to_interval。调用点只改 import,逻辑未动。

暖机长度与平滑种子按 TA-Lib 的约定实现,差一根 K 线就会让下游所有
笔/线段/中枢整体位移。其中 MACD 需特别处理:TA-Lib 让快慢两条 EMA
在同一根 K 线出首值,因而快线的种子取 x[slow-fast:slow] 的均值,而非
从 fastperiod-1 一路递推——两者在百元价位上相差约 0.17。

BBANDS 是有意的分歧:TA-Lib 用 sumsq/n - mean² 求方差,短窗口远离零
时灾难性抵消(timeperiod=2 误差 8.7e-7),本实现用 rolling std,对 50
位精度基准误差为 0。项目实际使用的周期两者一致到 1e-10。

顺带清理 12 个文件中 16 处从未调用的 talib/technical 导入。

验证:9440 组随机对拨;真实 K 线端到端比对 add_indicators 全部 33 个
指标列,NaN 模式一致、MACD 柱符号 100% 相同;屏蔽两个包后 60 个模块
均可导入。新增 test_ta_compat.py 将输出逐 bar 钉在 TA-Lib 上,但该文件
在 TA-Lib 缺失时静默跳过,改动 ta.py 需在装有 TA-Lib 的环境复跑。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-27 02:01:43 +08:00

411 lines
14 KiB
Python

"""TF_DF builder mixin — 由 split_tfdf_builders 自动生成,逻辑与原 TF_DF 一致。"""
from __future__ import annotations
from datetime import timedelta
from decimal import Decimal
import numpy as np
import pandas as pd
from pandas import DataFrame
from chanlun.core.ChanBI import ChanBI
from chanlun.core.ChanBIZS import ChanBIZS
from chanlun.core.ChanBSP import ChanBSP
from chanlun.core.ChanEnum import (
Chan_BI_DIR,
Chan_BSP_DIR,
Chan_BSP_TYPE,
Chan_FX_TYPE,
Chan_K_DIR,
Chan_KLC_FX,
Chan_KLC_STATE,
Chan_KLINE_DIR,
Chan_KLU_PATTERN,
Chan_PRICE_TREND,
Chan_SEG_DIR,
Chan_ZS_DIR,
)
from chanlun.core.ChanKLC import ChanKLC
from chanlun.core.ChanKLU import ChanKLU
from chanlun.core.ChanSBI import ChanSBI
from chanlun.core.ChanSEG import ChanSEG
from chanlun.core.ChanZS import ChanZS, ChanZS_Big
from chanlun.indicators.ChanMACD import ChanMACD
class BspBuilderMixin:
def get_bsp_state(self, dataframe):
klu_list = self.get_klu_list(dataframe)
klc_list = self.get_klc_list(klu_list)
bi_list = self.cal_bi_list(klc_list)
seg_list = self.get_seg_list(bi_list)
bi_zs_list = self.cal_bi_zs(seg_list)
bsp_list = self.find_all_bsp(bi_list, bi_zs_list)
bsp_state_list = [0] * len(dataframe)
klc_index = 0
for index in range(0, len(dataframe)):
if klc_index == len(klc_list):
klc_index = len(klc_list) - 1
klc = klc_list[klc_index]
if klc.end_klu and klc.end_klu.idx == index:
if klc.klc_fx_type == Chan_KLC_FX.TOP2:
bi = klc.bi.pre
if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.B3:
# 第三类买点
bsp_state_list[index] = -1
#print(klc.end_time, "B3")
else:
bsp_state_list[index] = 0
elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM2:
bi = klc.bi.pre
if bi and bi.is_sure and bi.end_klc.bsp_type == Chan_BSP_TYPE.S3:
# 第三类卖点
bsp_state_list[index] = 1
#print(klc.end_time, "S3")
else:
bsp_state_list[index] = 0
klc_index += 1
else:
bsp_state_list[index] = 0
return bsp_state_list
def get_above_zero_bsp(self, klc_list):
buy_bsp_list = []
sell_bsp_list = []
above_zero = False
buy_bsp = None
sell_bsp = None
for klc in klc_list:
if klc.pre and klc.pre.signal < 0 and klc.signal > 0:
above_zero = True
if klc.pre and klc.pre.signal > 0 and klc.signal < 0:
above_zero = False
if above_zero and klc.klc_fx_type == Chan_KLC_FX.BOTTOM2 and klc.macd > 0:
buy_bsp = klc
buy_bsp_list.append(klc)
#print(klc.end_time, "MACD 0轴上穿,回调笔底分型做多")
if buy_bsp and klc.pre and klc.pre.macdhist > 0 and klc.macdhist < 0:
sell_bsp = klc
sell_bsp_list.append(klc)
buy_bsp = None
#print(klc.end_time, "Sell BSP Found")
return buy_bsp_list
def find_all_bsp(self, bi_list, bi_zs_list):
"""
笔中枢的三类买卖点识别
三类买点:中枢形成后,一笔向上离开中枢(低点 > zg),
随后回拉的一笔低点不跌回中枢(低点 >= zg),确认支撑有效。
三类卖点:中枢形成后,一笔向下离开中枢(高点 < zd),
随后反弹的一笔高点不回到中枢(高点 <= zd),确认压力有效。
参数:
bi_list: 笔列表
bi_zs_list: 笔中枢列表(二维列表,每个seg内的中枢列表)
返回:
bsp_list: ChanBSP 列表,包含所有识别到的三类买卖点
"""
bsp_list = []
if len(bi_list) < 4 or len(bi_zs_list) == 0:
return bsp_list
for zs in bi_zs_list:
if not zs.is_sure or len(zs.bi_list) < 3:
continue
#print(zs.start_time, zs.end_time, zs.dir, zs.is_sure, len(zs.bi_list))
# 中枢结束后的第一笔(离开笔)
last_zs_bi = zs.bi_list[-1]
if last_zs_bi.dir == Chan_BI_DIR.UP:
if last_zs_bi.is_sure and last_zs_bi.end_klc.high <= zs.zg or (last_zs_bi.next and last_zs_bi.next.is_sure and last_zs_bi.next.end_klc.low < zs.zd):
leave_bi = last_zs_bi.next
else:
leave_bi = last_zs_bi
else:
if last_zs_bi.is_sure and last_zs_bi.end_klc.low >= zs.zd or (last_zs_bi.next and last_zs_bi.next.is_sure and last_zs_bi.next.end_klc.high > zs.zg):
leave_bi = last_zs_bi.next
else:
leave_bi = last_zs_bi
#print(zs.zg, zs.zd)
if leave_bi is None or not leave_bi.is_sure:
continue
if (zs.dir == Chan_ZS_DIR.UP and leave_bi.dir == Chan_BI_DIR.UP and leave_bi.end_klc.high < zs.zg and leave_bi.end_klc.high > zs.zd) or (zs.dir == Chan_ZS_DIR.DOWN and leave_bi.dir == Chan_BI_DIR.DOWN and leave_bi.end_klc.low < zs.zg and leave_bi.end_klc.low > zs.zd):
#print("--------------------", leave_bi.dir, leave_bi.end_klc.high, leave_bi.end_klc.low, zs.zg, zs.zd)
leave_bi = leave_bi.next
# 三类买点:向上离开中枢后回拉不破 zg
#print("Leave bi:", leave_bi.start_time, leave_bi.end_time, leave_bi.dir, leave_bi.is_sure, leave_bi.low, leave_bi.high)
if leave_bi.dir == Chan_BI_DIR.UP:
first_bsp_bi_div = self.check_bi_div(zs, leave_bi)
# 确认一类卖点:离开断能量小于进入段能量
if first_bsp_bi_div:
bsp = ChanBSP(
leave_bi, len(bsp_list),
Chan_BSP_TYPE.S1,
Chan_BSP_DIR.SELL,
leave_bi.sure_time,
zs.index+1, zs, None
)
leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S1)
bsp_list.append(bsp)
# 回拉笔
pullback_bi = leave_bi.next
#print(pullback_bi.start_klc.start_time, pullback_bi.dir, pullback_bi.is_sure, pullback_bi.low, pullback_bi.high)
if pullback_bi and pullback_bi.is_sure and pullback_bi.dir == Chan_BI_DIR.DOWN:
if pullback_bi.low >= zs.zg:
# 确认三类买点:回拉笔的低点不跌回中枢
bsp = ChanBSP(
pullback_bi, len(bsp_list),
Chan_BSP_TYPE.B3,
Chan_BSP_DIR.BUY,
pullback_bi.sure_time,
zs.index+1, zs, None
)
pullback_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B3)
bsp_list.append(bsp)
# 二类卖点
if first_bsp_bi_div:
second_bsp_bi = pullback_bi.next
if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.high < leave_bi.end_klc.high:
# 确认二类卖点:一类卖点后回拉不超过一类卖点高点
bsp = ChanBSP(
second_bsp_bi, len(bsp_list),
Chan_BSP_TYPE.S2,
Chan_BSP_DIR.SELL,
second_bsp_bi.sure_time,
zs.index+1, zs, None
)
second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2)
bsp_list.append(bsp)
# 三类卖点:向下离开中枢后反弹不破 zd
elif leave_bi.dir == Chan_BI_DIR.DOWN:
first_bsp_bi_div = self.check_bi_div(zs, leave_bi)
# 确认一类买点:离开段能量小于进入段
if first_bsp_bi_div:
bsp = ChanBSP(
leave_bi, len(bsp_list),
Chan_BSP_TYPE.B1,
Chan_BSP_DIR.BUY,
leave_bi.sure_time,
zs.index+1, zs, None
)
leave_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B1)
bsp_list.append(bsp)
# 反弹笔
bounce_bi = leave_bi.next
#print(bounce_bi.start_klc.start_time, bounce_bi.dir, bounce_bi.is_sure, bounce_bi.low, bounce_bi.high)
if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP:
if bounce_bi.high <= zs.zd:
# 确认三类卖点:反弹笔的高点不回到中枢
bsp = ChanBSP(
bounce_bi, len(bsp_list),
Chan_BSP_TYPE.S3,
Chan_BSP_DIR.SELL,
bounce_bi.sure_time,
zs.index+1, zs, None
)
bounce_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.S3)
bsp_list.append(bsp)
# 二类卖点
if first_bsp_bi_div:
second_bsp_bi = bounce_bi.next
if second_bsp_bi and second_bsp_bi.is_sure and second_bsp_bi.end_klc.low > leave_bi.end_klc.low:
# 确认二类买点:一类买点后回拉不超过一类卖点高点
bsp = ChanBSP(
second_bsp_bi, len(bsp_list),
Chan_BSP_TYPE.B2,
Chan_BSP_DIR.BUY,
second_bsp_bi.sure_time,
zs.index+1, zs, None
)
second_bsp_bi.end_klc.set_bsp_type(Chan_BSP_TYPE.B2)
bsp_list.append(bsp)
return bsp_list
def check_bi_div(self, zs, leave_bi):
enter_bi = zs.bi_list[0].pre
macdhist_div = 0
if enter_bi and enter_bi.dir == leave_bi.dir:
macdhist_div = abs(leave_bi.macd_hist) - abs(enter_bi.macd_hist)
#print(enter_bi.end_time, leave_bi.end_time, macdhist_div < 0)
return macdhist_div < 0
def find_first_bsp(self, bi_list, bi_zs_list):
"""
笔中枢的一类买卖点识别
一类买点:下跌趋势中,最后一个中枢完成后,向下离开中枢的笔创新低,
但该笔与进入中枢前的最后一笔下跌形成底背驰(力度减弱),
即趋势力竭的转折点。
一类卖点:上涨趋势中,最后一个中枢完成后,向上离开中枢的笔创新高,
但该笔与进入中枢前的最后一笔上涨形成顶背驰(力度减弱),
即趋势力竭的转折点。
简化判断:中枢形成后,离开中枢的笔(突破笔)本身即为一类买卖点的触发笔。
参数:
bi_list: 笔列表
bi_zs_list: 笔中枢列表(扁平列表,每个元素是一个中枢对象)
返回:
bsp_list: ChanBSP 列表,包含所有识别到的一类买卖点
"""
bsp_list = []
if len(bi_list) < 4 or len(bi_zs_list) == 0:
return bsp_list
for zs in bi_zs_list:
if not zs.is_sure or len(zs.bi_list) < 3:
continue
# 找到中枢的最后一笔
last_zs_bi = zs.bi_list[-1]
# 确定离开笔:中枢最后一笔之后的第一笔
if last_zs_bi.dir == Chan_BI_DIR.UP:
# 中枢最后一笔向上,如果没有真正离开中枢,取下一笔
if last_zs_bi.is_sure and last_zs_bi.end_klc.high <= zs.zg:
leave_bi = last_zs_bi.next
else:
leave_bi = last_zs_bi
else:
# 中枢最后一笔向下,如果没有真正离开中枢,取下一笔
if last_zs_bi.is_sure and last_zs_bi.end_klc.low >= zs.zd:
leave_bi = last_zs_bi.next
else:
leave_bi = last_zs_bi
if leave_bi is None or not leave_bi.is_sure:
continue
# 一类买点:向下离开中枢(leave_bi向下,低点 < zd),趋势力竭
if leave_bi.dir == Chan_BI_DIR.DOWN and leave_bi.low < zs.zd:
# 背驰判断:比较离开笔与中枢内最后一笔同向笔的MACD柱状累积面积
# 缠论原文:两段同向走势的MACD柱状面积比较,面积缩小即为背驰
compare_bi = None
for bi in reversed(zs.bi_list):
if bi.dir == Chan_BI_DIR.DOWN and bi is not leave_bi:
compare_bi = bi
break
is_divergence = False
if compare_bi:
# 笔的macd_hist是该笔内所有KLU的macdhist累积面积
leave_macd_area = abs(leave_bi.macd_hist)
compare_macd_area = abs(compare_bi.macd_hist)
# 价格创新低但MACD面积缩小 = 底背驰
if leave_bi.low <= compare_bi.low and leave_macd_area < compare_macd_area:
is_divergence = True
# 即使没创新低,MACD面积明显缩小也算背驰
elif leave_macd_area < compare_macd_area * 0.5:
is_divergence = True
else:
# 没有对比笔时,只要离开中枢就算一类买点
is_divergence = True
if is_divergence:
bsp = ChanBSP(
leave_bi, len(bsp_list),
Chan_BSP_TYPE.T1,
Chan_BSP_DIR.BUY,
leave_bi.sure_time,
1, zs, None
)
bsp_list.append(bsp)
# 一类卖点:向上离开中枢(leave_bi向上,高点 > zg),趋势力竭
elif leave_bi.dir == Chan_BI_DIR.UP and leave_bi.high > zs.zg:
# 背驰判断:比较离开笔与中枢内最后一笔同向笔的MACD柱状累积面积
compare_bi = None
for bi in reversed(zs.bi_list):
if bi.dir == Chan_BI_DIR.UP and bi is not leave_bi:
compare_bi = bi
break
is_divergence = False
if compare_bi:
leave_macd_area = abs(leave_bi.macd_hist)
compare_macd_area = abs(compare_bi.macd_hist)
# 价格创新高但MACD面积缩小 = 顶背驰
if leave_bi.high >= compare_bi.high and leave_macd_area < compare_macd_area:
is_divergence = True
# 即使没创新高,MACD面积明显缩小也算背驰
elif leave_macd_area < compare_macd_area * 0.5:
is_divergence = True
else:
is_divergence = True
if is_divergence:
bsp = ChanBSP(
leave_bi, len(bsp_list),
Chan_BSP_TYPE.T1,
Chan_BSP_DIR.SELL,
leave_bi.sure_time,
1, zs, None
)
bsp_list.append(bsp)
return bsp_list
def find_second_bsp(self, bi_list, first_bsp_list):
"""
笔中枢的二类买卖点识别
二类买点:一类买点出现后,价格向上反弹一笔,再回落一笔,
回落笔的低点不跌破一类买点的低点,确认底部成立。
二类卖点:一类卖点出现后,价格向下回落一笔,再反弹一笔,
反弹笔的高点不超过一类卖点的高点,确认顶部成立。
参数:
bi_list: 笔列表
first_bsp_list: 一类买卖点列表(find_first_bsp 的返回值)
返回:
bsp_list: ChanBSP 列表,包含所有识别到的二类买卖点
"""
bsp_list = []
if not first_bsp_list or len(bi_list) < 4:
return bsp_list
for first_bsp in first_bsp_list:
trigger_bi = first_bsp.bi # 一类买卖点的触发笔
if first_bsp.dir == Chan_BSP_DIR.BUY:
# 一买之后:trigger_bi 向下 -> 反弹笔(向上) -> 回落笔(向下)
# 回落笔的低点 > trigger_bi 的低点 => 二类买点
bounce_bi = trigger_bi.next # 反弹笔(向上)
if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP:
pullback_bi = bounce_bi.next # 回落笔(向下)
if pullback_bi and pullback_bi.is_sure and pullback_bi.dir == Chan_BI_DIR.DOWN:
if pullback_bi.low > trigger_bi.low:
bsp = ChanBSP(
pullback_bi, len(bsp_list),
Chan_BSP_TYPE.T2,
Chan_BSP_DIR.BUY,
pullback_bi.sure_time,
1, first_bsp.zs, None
)
bsp_list.append(bsp)
elif first_bsp.dir == Chan_BSP_DIR.SELL:
# 一卖之后:trigger_bi 向上 -> 回落笔(向下) -> 反弹笔(向上)
# 反弹笔的高点 < trigger_bi 的高点 => 二类卖点
drop_bi = trigger_bi.next # 回落笔(向下)
if drop_bi and drop_bi.is_sure and drop_bi.dir == Chan_BI_DIR.DOWN:
bounce_bi = drop_bi.next # 反弹笔(向上)
if bounce_bi and bounce_bi.is_sure and bounce_bi.dir == Chan_BI_DIR.UP:
if bounce_bi.high < trigger_bi.high:
bsp = ChanBSP(
bounce_bi, len(bsp_list),
Chan_BSP_TYPE.T2,
Chan_BSP_DIR.SELL,
bounce_bi.sure_time,
1, first_bsp.zs, None
)
bsp_list.append(bsp)
return bsp_list