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_class;False 时用 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