Files
Chan/strategies/ChanMomentumBayes.py
2025-08-10 19:55:26 +08:00

567 lines
34 KiB
Python
Raw Permalink 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.
from __future__ import annotations
from freqtrade.strategy import IStrategy
from technical.util import resample_to_interval, resampled_merge
from pandas import DataFrame
import pandas as pd
from functools import reduce
from typing import Dict, Tuple
import talib.abstract as ta
import numpy as np
class MomentumConcepts:
"""
将《K线动能理论》的关键概念实现为可复用的判定函数。
输出以列的形式添加到 DataFrame,列名统一为 concept_*。
"""
def __init__(self, macd_fast: int = 12, macd_slow: int = 26, macd_signal: int = 9,
ema_short: int = 24, ema_long: int = 52) -> None:
self.macd_fast = macd_fast
self.macd_slow = macd_slow
self.macd_signal = macd_signal
self.ema_short = ema_short
self.ema_long = ema_long
def _ensure_indicators(self, df: DataFrame) -> DataFrame:
macd = ta.MACD(df, fastperiod=self.macd_fast, slowperiod=self.macd_slow, signalperiod=self.macd_signal)
df['macd'] = macd['macd']
df['macdsignal'] = macd['macdsignal']
df['macdhist'] = macd['macdhist']
df['ema_24'] = ta.EMA(df, timeperiod=self.ema_short)
df['ema_52'] = ta.EMA(df, timeperiod=self.ema_long)
df['atr_14'] = ta.ATR(df, timeperiod=14)
# 基础量化
df['macd_abs'] = np.abs(df['macd'])
df['macdsignal_abs'] = np.abs(df['macdsignal'])
df['zero_dist'] = np.sqrt(np.square(df['macd']) + np.square(df['macdsignal']))
df['zero_dist_ema'] = df['zero_dist'].ewm(span=50, adjust=False).mean()
df['zero_eps'] = (df['zero_dist_ema'] * 0.2).clip(lower=1e-8)
df['histogram_decreasing'] = df['macdhist'] < df['macdhist'].shift(1)
df['histogram_increasing'] = df['macdhist'] > df['macdhist'].shift(1)
df['above_zero'] = (df['macd'] > 0) & (df['macdsignal'] > 0)
df['below_zero'] = (df['macd'] < 0) & (df['macdsignal'] < 0)
return df
def compute_all_features(self, df: DataFrame) -> DataFrame:
df = self._ensure_indicators(df.copy())
# 归零轴 | 高位 | 离开/穿越零轴
# “归零轴”的两种情形:价格触碰EMA52;或快线DIF无限接近零轴(文档1-6,21-27
df['concept_price_touch_ema52'] = (np.abs(df['close'] - df['ema_52']) / df['ema_52'] < 0.005)
df['concept_fastline_near_zero'] = (df['macd_abs'] < df['zero_eps'])
df['concept_near_zero'] = (df['macd_abs'] < df['zero_eps']) & (df['macdsignal_abs'] < df['zero_eps'])
df['concept_zero_axis_return'] = df['concept_price_touch_ema52'] | df['concept_fastline_near_zero']
# 价格先触EMA52而黄白线尚未归零轴
df['concept_zero_touch_price_first'] = df['concept_price_touch_ema52'] & (~df['concept_near_zero'])
df['concept_high_position'] = df['zero_dist'] > (df['zero_dist_ema'] * 1.5)
df['concept_cross_zero_up'] = (
((df['macd'].shift(1) <= 0) & (df['macd'] > 0)) |
((df['macdsignal'].shift(1) <= 0) & (df['macdsignal'] > 0))
)
df['concept_cross_zero_down'] = (
((df['macd'].shift(1) >= 0) & (df['macd'] < 0)) |
((df['macdsignal'].shift(1) >= 0) & (df['macdsignal'] < 0))
)
df['concept_leave_zero'] = (df['concept_cross_zero_up'] | df['concept_cross_zero_down']) & df['histogram_increasing']
# “有效穿越零轴/EMA52”(文档10-13,23-27):当前穿越,且下一根仍保持在同侧
def _effective_break(price: DataFrame, ma: DataFrame, direction: str) -> DataFrame:
if direction == 'down':
cross_now = (price < ma) & (price.shift(1) >= ma.shift(1))
hold_next = price.shift(-1) < ma.shift(-1)
else:
cross_now = (price > ma) & (price.shift(1) <= ma.shift(1))
hold_next = price.shift(-1) > ma.shift(-1)
return (cross_now & hold_next).fillna(False)
df['concept_effective_break_ema52_down'] = _effective_break(df['close'], df['ema_52'], 'down')
df['concept_effective_break_ema52_up'] = _effective_break(df['close'], df['ema_52'], 'up')
def _effective_cross_zero(series: DataFrame, direction: str) -> DataFrame:
if direction == 'down':
cross_now = (series < 0) & (series.shift(1) >= 0)
hold_next = series.shift(-1) < 0
else:
cross_now = (series > 0) & (series.shift(1) <= 0)
hold_next = series.shift(-1) > 0
return (cross_now & hold_next).fillna(False)
df['concept_effective_dea_cross_down'] = _effective_cross_zero(df['macdsignal'], 'down')
df['concept_effective_dea_cross_up'] = _effective_cross_zero(df['macdsignal'], 'up')
df['concept_cross_zero_effective_down'] = df['concept_effective_break_ema52_down'] & df['concept_effective_dea_cross_down']
df['concept_cross_zero_effective_up'] = df['concept_effective_break_ema52_up'] & df['concept_effective_dea_cross_up']
# 高位空(高位 + 能量柱衰减 + 黄白线横盘 + 价格横盘)
df['macd_flat_3'] = (np.abs(df['macd'] - df['macd'].shift(3)) < df['zero_eps'])
df['macdsignal_flat_3'] = (np.abs(df['macdsignal'] - df['macdsignal'].shift(3)) < df['zero_eps'])
df['concept_high_position_empty'] = (
df['concept_high_position'] & df['histogram_decreasing'] &
(np.abs(df['close'] - df['close'].shift(3)) / df['close'].shift(3) < 0.02) &
df['macd_flat_3'] & df['macdsignal_flat_3']
)
# 隐形形态(无能量配合,文档32-51)
# 高位隐形:远离零轴的高位价格延续而能量未同向释放 → 归零轴/穿零轴需求
df['concept_hidden_high'] = (
df['concept_high_position'] & (
((df['close'] > df['close'].shift(1)) & (df['macdhist'] <= 0)) |
((df['close'] < df['close'].shift(1)) & (df['macdhist'] >= 0))
)
)
# 归零轴隐形:零轴支撑期应有正向能量却无 → 后续易穿零
df['concept_hidden_zero'] = (
df['concept_zero_axis_return'] & (np.abs(df['close'] - df['ema_52']) / df['ema_52'] < 0.01) & (df['macdhist'] <= 0)
)
# 兼容旧列名(用于卖出特征列表)
df['concept_hidden_high_bull'] = df['concept_hidden_high']
df['concept_hidden_zero_bull_fail'] = df['concept_hidden_zero']
# 顶/底分型
df['concept_fractal_top'] = (df['high'] > df['high'].shift(1)) & (df['high'] > df['high'].shift(-1))
df['concept_fractal_bottom'] = (df['low'] < df['low'].shift(1)) & (df['low'] < df['low'].shift(-1))
# 强势/超强势/弱势结构(修正:强调“趋于归零轴”,而非“已在零轴附近”)
# 零轴距离连续收敛,仍在高位,尚未到近零
df['zero_dist_decr_3'] = (
(df['zero_dist'] < df['zero_dist'].shift(1)) &
(df['zero_dist'].shift(1) < df['zero_dist'].shift(2)) &
(df['zero_dist'].shift(2) < df['zero_dist'].shift(3))
)
highpos_recent = df['concept_high_position'].rolling(8, min_periods=1).max() > 0
price_sideways_3 = (np.abs(df['close'] - df['close'].shift(3)) / df['close'].shift(3) < 0.02)
ema24_slope_up = df['ema_24'] > df['ema_24'].shift(3)
near_ema24 = (np.abs(df['close'] - df['ema_24']) / df['ema_24'] < 0.01)
# 强势结构:高位横盘 + MACD黄白线趋于零轴(距离收敛)+ 仍在零轴上方,且未到近零
df['concept_trend_strong'] = (
(df['close'] >= df['ema_24']) & price_sideways_3 & highpos_recent & df['zero_dist_decr_3'] & df['above_zero'] & (~df['concept_near_zero'])
)
# 超强势结构:贴近EMA24缓慢上行 + 趋于零轴(距离收敛)+ 仍未到近零,倾向于破前高
df['concept_trend_super_strong'] = (
(df['close'] > df['ema_24']) & near_ema24 & ema24_slope_up & df['zero_dist_decr_3'] & (~df['concept_near_zero'])
)
# 弱势结构:价格快速靠近/跌至EMA52附近,同时MACD距离快速收敛(可接近近零)
zero_dist_fast_drop = (df['zero_dist'] < df['zero_dist'].shift(1)) & (df['zero_dist'].shift(1) < df['zero_dist'].shift(2))
df['concept_trend_weak'] = (
(df['close'] <= df['ema_52']) & zero_dist_fast_drop
)
# 强势反弹触发:强势结构之后,黄白线真正“归零轴”触发有效反弹(文档53-56)
strong_ctx = df['concept_trend_strong'].rolling(5, min_periods=1).max() > 0
df['concept_strong_rebound_trigger'] = strong_ctx & df['concept_near_zero'] & (df['close'] >= df['ema_24'])
# 超强势破高触发:超强势结构后,归零轴并突破近期高点(速度快、力度强且破前高)
prev_high_10 = df['close'].rolling(10).max().shift(1)
df['concept_super_strong_break_trigger'] = (df['concept_trend_super_strong'].rolling(8, min_periods=1).max() > 0) & df['concept_near_zero'] & (df['close'] > prev_high_10)
# 线段(以慢线穿零轴划分)与单位调整周期(near_zero触发 id
seg_change = (
((df['macdsignal'] <= 0) & (df['macdsignal'].shift(1) > 0)) |
((df['macdsignal'] >= 0) & (df['macdsignal'].shift(1) < 0))
)
df['concept_segment_id'] = seg_change.cumsum().fillna(0).astype(int)
df['concept_unit_cycle_id'] = (df['concept_near_zero'].astype(int).diff().fillna(0) > 0).cumsum().astype(int)
# 当前线段内的第一个单位周期(用于限制“最佳买卖点”发生在首次离零后的周期)
seg_first_cycle = df.groupby('concept_segment_id')['concept_unit_cycle_id'].transform('min')
df['first_cycle_in_segment'] = (df['concept_unit_cycle_id'] == seg_first_cycle).fillna(False)
# 当前线段内的第二个单位周期
def _second_cycle_id(s):
uniq = np.sort(s.unique())
return uniq[1] if len(uniq) > 1 else np.nan
seg_second_cycle_id = df.groupby('concept_segment_id')['concept_unit_cycle_id'].transform(_second_cycle_id)
df['second_cycle_in_segment'] = (df['concept_unit_cycle_id'] == seg_second_cycle_id).fillna(False)
# 当前线段内的第三个单位周期
def _third_cycle_id(s):
uniq = np.sort(s.unique())
return uniq[2] if len(uniq) > 2 else np.nan
seg_third_cycle_id = df.groupby('concept_segment_id')['concept_unit_cycle_id'].transform(_third_cycle_id)
df['third_cycle_in_segment'] = (df['concept_unit_cycle_id'] == seg_third_cycle_id).fillna(False)
# 零轴粘合 / 倒挂(文档67-75,77-79
small_lines = (df['macd_abs'] < df['zero_eps'] * 1.2) & (df['macdsignal_abs'] < df['zero_eps'] * 1.2)
same_side_hist = (
((df['macdhist'] >= 0) & df['concept_cross_zero_up']) |
((df['macdhist'] <= 0) & df['concept_cross_zero_down'])
)
df['concept_zero_adhesion'] = small_lines & same_side_hist
macd_cross_event = (
((df['macd'] > df['macdsignal']) & (df['macd'].shift(1) <= df['macdsignal'].shift(1))) |
((df['macd'] < df['macdsignal']) & (df['macd'].shift(1) >= df['macdsignal'].shift(1)))
)
hist_flip = np.sign(df['macdhist']) != np.sign(df['macdhist'].shift(1))
df['concept_zero_inverted'] = df['concept_near_zero'] & df['histogram_decreasing'] & macd_cross_event & hist_flip
# 零轴缠绕/纠缠:近零或小幅上下缠绕,代表本级别调整结束倾向(文档28-31)
small_hist = df['macdhist'].abs() < df['zero_eps']
near_or_flip_small = (df['concept_near_zero'] | (small_hist & (df['macd_abs'] < df['zero_eps'] * 1.5) & (df['macdsignal_abs'] < df['zero_eps'] * 1.5)))
df['concept_zero_entanglement'] = (near_or_flip_small.rolling(5, min_periods=1).max() > 0)
# 顶/底背离(线段内比较)
df['concept_top_div'] = False
df['concept_bottom_div'] = False
g = df.groupby('concept_segment_id', group_keys=False)
seg_close_max_prev = g['close'].apply(lambda s: s.cummax().shift(1))
seg_macd_max_prev = g['macd'].apply(lambda s: s.cummax().shift(1))
seg_close_min_prev = g['close'].apply(lambda s: s.cummin().shift(1))
seg_macd_min_prev = g['macd'].apply(lambda s: s.cummin().shift(1))
cond_top = (df['close'] > seg_close_max_prev) & (df['macd'] < seg_macd_max_prev) & df['above_zero']
cond_bottom = (df['close'] < seg_close_min_prev) & (df['macd'] > seg_macd_min_prev) & df['below_zero']
df.loc[cond_top.fillna(False), 'concept_top_div'] = True
df.loc[cond_bottom.fillna(False), 'concept_bottom_div'] = True
# 跳空/分立跳空(文档101-139,近似实现)
df['concept_continuous_gap'] = False
df['concept_separate_gap'] = False
hist_within = df['macdhist'].abs() <= np.maximum(df['macd_abs'], df['macdsignal_abs'])
for i in range(6, len(df)):
# 连续跳空:单位周期内,能量衰减→转增,能量柱包含在黄白线之内,且未放出反向能量
decr_then_incr = (df['macdhist'].iloc[i-4:i-1].diff().dropna() < 0).all() and (df['macdhist'].iloc[i-1:i+1].diff().dropna() > 0).all()
within_lines = hist_within.iloc[i-3:i+1].all()
no_opposite = not ((df['macdhist'].iloc[i-6:i] < 0).any() and (df['macdhist'].iloc[i] > 0)) and not ((df['macdhist'].iloc[i-6:i] > 0).any() and (df['macdhist'].iloc[i] < 0))
same_cycle = df['concept_unit_cycle_id'].iloc[i] == df['concept_unit_cycle_id'].iloc[i-3]
if decr_then_incr and within_lines and no_opposite and same_cycle:
df.iloc[i, df.columns.get_loc('concept_continuous_gap')] = True
# 分立跳空:同向能量堆之间被反向能量隔开,再次出现同向能量堆且处高位
recent = df['macdhist'].iloc[i-12:i+1]
if len(recent) >= 8:
pos = (recent > 0).astype(int)
neg = (recent < 0).astype(int)
has_pos_sep = (pos.diff().abs().sum() >= 2) and (recent.iloc[-1] > 0)
has_neg_sep = (neg.diff().abs().sum() >= 2) and (recent.iloc[-1] < 0)
if (has_pos_sep or has_neg_sep) and df['concept_high_position'].iloc[i]:
df.iloc[i, df.columns.get_loc('concept_separate_gap')] = True
# 最佳买卖点(原文:单位周期之内 + 隐形 + 分立跳空 + 背离 + 黄白线高位空)
# 在分立跳空计算之后再做周期聚合,避免列不存在
cycle_id = df['concept_unit_cycle_id']
cycle_hidden_high = df['concept_hidden_high'].groupby(cycle_id).cummax().fillna(False).astype(bool)
cycle_hidden_zero = df['concept_hidden_zero'].groupby(cycle_id).cummax().fillna(False).astype(bool)
cycle_sep_gap = df['concept_separate_gap'].groupby(cycle_id).cummax().fillna(False).astype(bool)
cycle_top_div = df['concept_top_div'].groupby(cycle_id).cummax().fillna(False).astype(bool)
cycle_bottom_div = df['concept_bottom_div'].groupby(cycle_id).cummax().fillna(False).astype(bool)
hpe_above_series = (df['concept_high_position_empty'] & df['above_zero']).astype(bool)
hpe_below_series = (df['concept_high_position_empty'] & df['below_zero']).astype(bool)
cycle_hpe_above = hpe_above_series.groupby(cycle_id).cummax().fillna(False).astype(bool)
cycle_hpe_below = hpe_below_series.groupby(cycle_id).cummax().fillna(False).astype(bool)
df['concept_best_sell_by_doc'] = (cycle_hidden_high & cycle_sep_gap & cycle_top_div & cycle_hpe_above)
df['concept_best_buy_by_doc'] = (cycle_hidden_zero & cycle_sep_gap & cycle_bottom_div & cycle_hpe_below)
# 连续背离:单位周期内,价格与能量柱方向反复背离(粗略识别:近零后两次及以上背离触发)
g2 = df.groupby('concept_unit_cycle_id')
df['concept_divergence_series'] = (g2['concept_top_div'].transform('sum') + g2['concept_bottom_div'].transform('sum')) >= 2
# 单位周期之间的背离 / 线段背离(粗略量化)
df['concept_cycle_between_div'] = False
df['concept_segment_between_div'] = False
# 周期间离开零轴距离比较
cycle_peak = g2['zero_dist'].transform('max')
df['concept_cycle_between_div'] = (cycle_peak < cycle_peak.shift(1)) & (df['concept_unit_cycle_id'] == df['concept_unit_cycle_id'])
# 线段之间:比较两个线段 DIF 峰值
seg_peak = g['macd'].transform('max')
df['concept_segment_between_div'] = (seg_peak < seg_peak.shift(1))
# 动能不足(价格不破新高/新低 + 能量衰减)
df['concept_momentum_lack_up'] = (df['close'] <= df['close'].shift(1)) & df['histogram_decreasing'] & df['above_zero']
df['concept_momentum_lack_down'] = (df['close'] >= df['close'].shift(1)) & df['histogram_decreasing'] & df['below_zero']
# 底部形态四阶段 & V字反转(近似)
df['concept_bottom_phase1'] = df['below_zero'] & (df['close'] < df['close'].shift(3))
df['concept_bottom_phase2'] = df['concept_near_zero'] & (df['close'] > df['close'].shift(1))
df['concept_bottom_phase3'] = df['concept_bottom_div'] | df['concept_momentum_lack_down']
df['concept_bottom_phase4'] = df['concept_zero_adhesion'] | (df['concept_near_zero'] & (np.sign(df['macd']) != np.sign(df['macd'].shift(1))))
price_break = df['close'] > df['close'].rolling(10).max().shift(1)
macd_converge = df['histogram_decreasing'].rolling(4).sum() >= 3
df['concept_v_reversal'] = df['concept_near_zero'] & (df['close'] >= df['ema_52']) & macd_converge & price_break
# 最佳买/卖点(组合特征)
df['concept_best_buy'] = (
df['concept_bottom_div'] |
df['concept_v_reversal'] |
(df['concept_zero_adhesion'] & (df['close'] >= df['ema_52'])) |
(df['concept_zero_axis_return'] & df['concept_trend_strong'])
)
df['concept_best_sell'] = (
df['concept_top_div'] | df['concept_high_position_empty'] | df['concept_zero_inverted'] | df['concept_separate_gap']
)
return df
class ChanMomentumBayes(IStrategy):
INTERFACE_VERSION: int = 3
# 以15分钟为基础K线,仅合并上级周期30m/60m
timeframe = '15m'
minimal_roi = {"0": 0.004, "30": 0.006, "120": 0.01}
stoploss = -0.015
trailing_stop = True
trailing_stop_positive = 0.003
trailing_stop_positive_offset = 0.006
trailing_only_offset_is_reached = True
position_adjustment_enable = True
macd_fast = 24
macd_slow = 52
macd_signal = 18
ema_short = 24
ema_long = 52
# 贝叶斯阈值(放宽买入阈值)
buy_threshold = 0.65
sell_threshold = 0.55
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
concepts = MomentumConcepts(self.macd_fast, self.macd_slow, self.macd_signal, self.ema_short, self.ema_long)
df = concepts.compute_all_features(dataframe)
# 多时间周期重采样:次周期 3m/5m,高周期 30m/60m(基于15m交易)
def build_htf(src: DataFrame, minutes: int, suffix: str) -> DataFrame:
dfr = resample_to_interval(src, minutes)
macd_htf = ta.MACD(dfr, fastperiod=self.macd_fast, slowperiod=self.macd_slow, signalperiod=self.macd_signal)
dfr[f'macd_{suffix}'] = macd_htf['macd']
dfr[f'macdsignal_{suffix}'] = macd_htf['macdsignal']
dfr[f'macdhist_{suffix}'] = macd_htf['macdhist']
dfr[f'ema_24_{suffix}'] = ta.EMA(dfr, timeperiod=self.ema_short)
dfr[f'ema_52_{suffix}'] = ta.EMA(dfr, timeperiod=self.ema_long)
# 动态零轴度量
zero_dist = np.sqrt(np.square(dfr[f'macd_{suffix}']) + np.square(dfr[f'macdsignal_{suffix}']))
zero_ema = zero_dist.ewm(span=50, adjust=False).mean()
zero_eps = (zero_ema * 0.2).clip(lower=1e-8)
dfr[f'above_zero_{suffix}'] = (dfr[f'macd_{suffix}'] > 0) & (dfr[f'macdsignal_{suffix}'] > 0)
dfr[f'below_zero_{suffix}'] = (dfr[f'macd_{suffix}'] < 0) & (dfr[f'macdsignal_{suffix}'] < 0)
dfr[f'near_zero_{suffix}'] = (np.abs(dfr[f'macd_{suffix}']) < zero_eps) & (np.abs(dfr[f'macdsignal_{suffix}']) < zero_eps)
dfr[f'hist_decr_{suffix}'] = dfr[f'macdhist_{suffix}'] < dfr[f'macdhist_{suffix}'].shift(1)
dfr[f'high_position_{suffix}'] = zero_dist > (zero_ema * 1.5)
return dfr[['date', f'macd_{suffix}', f'macdsignal_{suffix}', f'macdhist_{suffix}', f'ema_24_{suffix}', f'ema_52_{suffix}',
f'above_zero_{suffix}', f'below_zero_{suffix}', f'near_zero_{suffix}', f'hist_decr_{suffix}', f'high_position_{suffix}']].copy()
# 合并上级周期特征(resampled_merge会添加列前缀 resample_{minutes}_
for minutes, suf in [(30, 'x30'), (60, 'x60')]:
htf = build_htf(df, minutes, suf)
df = resampled_merge(df, htf)
# 60m均线金叉(EMA24由下穿上EMA52
ema24_60 = df.get('resample_60_ema_24_x60', None)
ema52_60 = df.get('resample_60_ema_52_x60', None)
if ema24_60 is not None and ema52_60 is not None:
df['resample_60_ema_bull_cross'] = (
(ema24_60 >= ema52_60) & (ema24_60.shift(1) < ema52_60.shift(1))
).fillna(False)
else:
df['resample_60_ema_bull_cross'] = False
# 30m对齐窗口:仅在新30m的前15分钟(即第一根15mK线)允许入场
d30 = df.get('resample_30_date')
df['is_new_30m'] = d30.ne(d30.shift(1)).fillna(False)
# 新30m的前30分钟窗口(覆盖第一根与第二根15m)
if 'date' in df.columns and d30 is not None:
dt_delta_30 = (df['date'] - d30)
df['within_30m_first30m'] = dt_delta_30.dt.total_seconds().div(60).between(0, 30).fillna(False)
else:
df['within_30m_first30m'] = df['is_new_30m']
# 60m窗口:新60m的前30分钟
d60 = df.get('resample_60_date')
df['is_new_60m'] = d60.ne(d60.shift(1)).fillna(False) if d60 is not None else False
if 'date' in df.columns and d60 is not None:
dt_delta_60 = (df['date'] - d60)
df['within_60m_first30m'] = dt_delta_60.dt.total_seconds().div(60).between(0, 30).fillna(False)
else:
df['within_60m_first30m'] = df['is_new_60m']
# 高位空多周期确认(加强:要求60m能量柱走弱)
# 15m本级出现高位空 + 30m高位且能量衰减 + 60m多头或高位 + 次级别(5m/3m)出现近零或能量衰减
df['concept_high_position_empty_mtf'] = (
df.get('concept_high_position_empty', False) &
df.get('resample_30_high_position_x30', False) & df.get('resample_30_hist_decr_x30', False) &
(df.get('resample_60_above_zero_x60', False) | df.get('resample_60_high_position_x60', False)) &
df.get('resample_60_hist_decr_x60', False) &
(df.get('resample_5_near_zero_x5', False) | df.get('resample_5_hist_decr_x5', False))
).fillna(False)
# 简化版:按零轴侧别区分“高位空”方向性(上方看空、下方看多)
df['concept_hpe_above'] = df['concept_high_position_empty'] & df['above_zero']
df['concept_hpe_below'] = df['concept_high_position_empty'] & df['below_zero']
# 上级周期近似HPE(高位 + 直方图衰减),并区分上下零轴
for m, s in [(30, 'x30'), (60, 'x60')]:
df[f'resample_{m}_hpe_{s}'] = df.get(f'resample_{m}_high_position_{s}', False) & df.get(f'resample_{m}_hist_decr_{s}', False)
df[f'resample_{m}_hpe_above_{s}'] = df.get(f'resample_{m}_hpe_{s}', False) & df.get(f'resample_{m}_above_zero_{s}', False)
df[f'resample_{m}_hpe_below_{s}'] = df.get(f'resample_{m}_hpe_{s}', False) & df.get(f'resample_{m}_below_zero_{s}', False)
# 1小时门控:买入弱势背景(<=0, ema24<=ema52, 30m收敛);卖出强势背景(>=0, ema24>=ema52, 30m衰减)
def as_bool_series(val):
if val is None or isinstance(val, (bool, int, float)):
return pd.Series(False, index=df.index)
return val.fillna(False).astype(bool)
s60_hist_decr = as_bool_series(df.get('resample_60_hist_decr_x60'))
s60_near_zero = as_bool_series(df.get('resample_60_near_zero_x60'))
s30_hist_decr = as_bool_series(df.get('resample_30_hist_decr_x30'))
s30_near_zero = as_bool_series(df.get('resample_30_near_zero_x30'))
# 30m 直方图是否增加,用于定义“走平/不增”
s30_hist = df.get('resample_30_macdhist_x30', pd.Series(np.nan, index=df.index))
s30_hist_incr = (s30_hist > s30_hist.shift(1)).fillna(False)
# 买入门控(弱势背景)
df['htf_buy_gate'] = (
(df.get('resample_60_ema_24_x60', np.nan) <= df.get('resample_60_ema_52_x60', np.nan)) &
(df.get('resample_60_macdsignal_x60', np.nan) <= 0) &
(s30_near_zero | (~s30_hist_incr))
).fillna(False)
# 卖出门控(强势背景)
df['htf_sell_gate'] = (
(df.get('resample_60_ema_24_x60', np.nan) >= df.get('resample_60_ema_52_x60', np.nan)) &
((df.get('resample_60_macdsignal_x60', np.nan) >= 0) | s60_near_zero) &
(s30_hist_decr | (~s30_hist_incr))
).fillna(False)
# 波动过滤(ATR百分比)与均线邻近(放宽入场)
df['atr_pct'] = (df['atr_14'] / df['close']).fillna(0)
df['vol_ok'] = (df['atr_pct'] > 0.0005)
df['near_ema24'] = (np.abs(df['close'] - df['ema_24']) / df['ema_24'] < 0.002).fillna(False)
df['near_ema52'] = (np.abs(df['close'] - df['ema_52']) / df['ema_52'] < 0.002).fillna(False)
# 简化:贝叶斯/事件链关闭
self._buy_features = []
self._sell_features = []
return df
def _naive_bayes(self, row: Dict[str, float], feature_list: Tuple[str, ...], prior: float = 0.5,
p_true_given_class: float = 0.7, p_true_given_not: float = 0.3) -> float:
"""
朴素贝叶斯:假设各概念在类条件下相互独立。
- feature=True 时,使用 P(feature|Class)=p_true_given_classFalse 时用 1-p_true_given_class。
- 非该类时对应为 p_true_given_not。
返回后验 P(Class|features)。
"""
# 使用对数似然避免下溢
log_p_class = np.log(prior)
log_p_not = np.log(1 - prior)
for f in feature_list:
v = bool(row.get(f, False))
if v:
log_p_class += np.log(p_true_given_class)
log_p_not += np.log(p_true_given_not)
else:
log_p_class += np.log(1 - p_true_given_class)
log_p_not += np.log(1 - p_true_given_not)
# 归一化
max_log = max(log_p_class, log_p_not)
p_class = np.exp(log_p_class - max_log)
p_not = np.exp(log_p_not - max_log)
return float(p_class / (p_class + p_not))
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata.get('pair', '')
is_eth = pair.startswith('ETH/') or pair.startswith('ETH:')
# 使用原文“最佳买点”简化:单位周期之内 + 隐形(零轴)+ 分立跳空 + 底背离 + 黄白线高位空(零轴下)
# 门控:30/60m非上方HPE强压制
no_htf_bear = ~(dataframe.get('resample_60_hpe_above_x60', False) | dataframe.get('resample_30_hpe_above_x30', False))
price_dev_ema52 = (np.abs(dataframe['close'] - dataframe['ema_52']) / dataframe['ema_52'])
price_ok = (price_dev_ema52 < 0.04)
# 允许发生在当前线段的第1或第2个单位周期,且买入时要求本级零轴下方(同向一致)
first_cycle = (
dataframe.get('first_cycle_in_segment', False) |
dataframe.get('second_cycle_in_segment', False)
)
below_zero_now = dataframe['below_zero']
# 兜底分支:底背离 + 60m near_zero + ATR/EMA过滤
atr_pct = (dataframe['atr_14'] / dataframe['close']).fillna(0)
ema24_up = dataframe['ema_24'] > dataframe['ema_24'].shift(1)
fallback_buy = dataframe.get('concept_bottom_div', False) & dataframe.get('resample_60_near_zero_x60', False) & (atr_pct > (0.01 if is_eth else 0.007)) & ema24_up
# 穿越确认:本级 macd 与 macdsignal 金叉(或从负转平)后第1根
golden_cross = ((dataframe['macd'] > dataframe['macdsignal']) & (dataframe['macd'].shift(1) <= dataframe['macdsignal'].shift(1))).fillna(False)
macdsignal_flat_up = ((dataframe['macdsignal'] >= 0) & (dataframe['macdsignal'].shift(1) < 0)).fillna(False)
cross_now = golden_cross | macdsignal_flat_up
cross_ok = cross_now | cross_now.shift(1).fillna(False)
# 价格贴均线约束
near_ema24 = (np.abs(dataframe['close'] - dataframe['ema_24']) / dataframe['ema_24'] < 0.03).fillna(False)
near_ema52 = (np.abs(dataframe['close'] - dataframe['ema_52']) / dataframe['ema_52'] < 0.03).fillna(False)
# 价格贴近或站上EMA24(保持EMA52约束不变)
near_ma = near_ema24 | near_ema52 | (dataframe['close'] >= dataframe['ema_24'])
# 入场窗口:新30m或新60m前30分钟
in_window = dataframe.get('within_30m_first30m', False) | dataframe.get('within_60m_first30m', False)
# ETH额外门控 + 更严格的价格偏离
price_ok_eff = price_ok & (~is_eth | (price_dev_ema52 < 0.02))
eth_gate = (~is_eth) | (
dataframe.get('resample_60_near_zero_x60', False) &
dataframe.get('resample_30_near_zero_x30', False) &
(dataframe.get('resample_60_ema_bull_cross', False) | dataframe.get('resample_60_ema_24_x60', 0) >= dataframe.get('resample_60_ema_52_x60', 0))
)
buy_cond = (dataframe.get('concept_best_buy_by_doc', False) | fallback_buy) & eth_gate & no_htf_bear & price_ok_eff & first_cycle & below_zero_now & cross_ok & near_ma & in_window
dataframe.loc[buy_cond & dataframe['htf_buy_gate'], 'enter_long'] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# 使用原文“最佳卖点”简化:单位周期之内 + 隐形(高位)+ 分立跳空 + 顶背离 + 黄白线高位空(零轴上)
# 或风险触发(跌破EMA52/零轴倒挂/HTF强压制)
htf_bear = (dataframe.get('resample_60_hpe_above_x60', False) | dataframe.get('resample_30_hpe_above_x30', False))
# 卖出同理:发生在首个单位周期,且处于零轴上方(同向一致)
first_cycle = dataframe.get('first_cycle_in_segment', False) | dataframe.get('second_cycle_in_segment', False)
above_zero_now = dataframe['above_zero']
# 穿越确认:死叉或从正转平
dead_cross = ((dataframe['macd'] < dataframe['macdsignal']) & (dataframe['macd'].shift(1) >= dataframe['macdsignal'].shift(1))).fillna(False)
macdsignal_flat_down = ((dataframe['macdsignal'] <= 0) & (dataframe['macdsignal'].shift(1) > 0)).fillna(False)
cross_now_s = dead_cross | macdsignal_flat_down
cross_ok_s = cross_now_s | cross_now_s.shift(1).fillna(False)
# 退出窗口:新30m或新60m前30分钟
in_window = dataframe.get('within_30m_first30m', False) | dataframe.get('within_60m_first30m', False)
# 卖出形态“二选一” + 跌破EMA24
pattern_two = (dataframe.get('concept_top_div', False) | dataframe.get('concept_high_position_empty', False))
price_break_ema24 = (dataframe['close'] < dataframe['ema_24'])
sell_doc = dataframe.get('concept_best_sell_by_doc', False) & first_cycle & above_zero_now & cross_ok_s & in_window & pattern_two & price_break_ema24
risk_exit = (dataframe['close'] < dataframe['ema_52']) | dataframe.get('concept_zero_inverted', False) | htf_bear
dataframe.loc[(sell_doc & dataframe['htf_sell_gate']) | risk_exit, 'exit_long'] = 1
return dataframe
@property
def protections(self):
return [
{
"method": "CooldownPeriod",
"stop_duration_candles": 8,
},
{
"method": "MaxDrawdown",
"lookback_period_candles": 96,
"trade_limit": 20,
"stop_duration_candles": 24,
"max_allowed_drawdown": 0.2,
"only_per_pair": False,
},
{
"method": "StoplossGuard",
"lookback_period_candles": 24,
"trade_limit": 1,
"stop_duration_candles": 24,
"only_per_pair": False,
"only_per_side": False,
},
]
def custom_stoploss(self, pair: str, trade, current_time, current_rate: float, current_profit: float, **kwargs) -> float:
# 动态ATR止损,避免大额亏损:不低于-0.5%,不高于-2%
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last = df.iloc[-1]
atr_pct = float((last['atr_14'] / last['close']).clip(lower=0.0005, upper=0.02))
dyn_sl = -max(0.005, min(0.02, 1.5 * atr_pct))
# 若价格跌破EMA52则收紧
if last['close'] < last['ema_52']:
dyn_sl = min(dyn_sl, -0.008)
return float(dyn_sl)
def adjust_trade_position(self, trade, current_time, current_rate: float, current_profit: float, **kwargs):
try:
df, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
last = df.iloc[-1]
prev = df.iloc[-2] if len(df) > 1 else last
hist_turn_weak = (last.get('macdhist', 0) < prev.get('macdhist', 0))
price_above_ema24 = last['close'] > (1.01 * last['ema_24'])
# 条件:已盈利且动能转弱且价格偏离EMA24>1% → 减仓50%
if current_profit is not None and current_profit > 0.004 and hist_turn_weak and price_above_ema24 and trade.amount is not None and trade.amount > 0:
return -float(trade.amount) * 0.5
except Exception:
return None
return None