567 lines
34 KiB
Python
567 lines
34 KiB
Python
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
|
||
|
||
|