diff --git a/strategy.py b/strategy.py index 83b9c1d..3c08a4d 100644 --- a/strategy.py +++ b/strategy.py @@ -1,612 +1,542 @@ -""" -多时间框架价格行为策略 — ETH/USDT 中低频交易 -============================================== - -设计理念 (v0.3): - -1. 反转大多会失败 → 不做反转预测,只做趋势延续。 - 在 S/R 位入场不是赌反弹,是赌"回调结束、趋势恢复"。 - -2. 移动止损优先 → 放弃固定止盈,用 ATR 追踪止损让利润在趋势中奔跑。 - -3. 多时间框架自上而下分析: - D1 → 判断宏观方向(能不能做) - 1H → 识别中期结构 + S/R 区域(在哪做) - 5M → 确认入场时机(什么时候做) - -核心原则:只在大趋势方向上,在关键位置,等确认信号入场。 - -版本:v0.3.0 — v0.2 回测后优化 -""" - -from functools import reduce -from typing import Optional +# ============================================================================ +# Structure Flow Strategy v1.0 +# 纯价格结构策略 — 零技术指标,价格行为学驱动 +# +# 设计哲学: +# 趋势不由 EMA 定义,而由 HH/HL(Higher High / Higher Low)定义 +# 支撑阻力不由百分比定义,而由历史 Swing Point 定义 +# 止损不由 ATR 定义,而由结构失效点定义 +# 出场不由固定盈亏比定义,而由结构反转定义 +# +# 多时间框架: +# D1 → 宏观结构方向 +# 1H → 中期结构位 + 入场区域判定 +# 5M → K线形态确认入场时机 +# ============================================================================ +from datetime import datetime import numpy as np import pandas as pd -import talib.abstract as ta from pandas import DataFrame -from freqtrade.strategy import IStrategy, merge_informative_pair -from freqtrade.strategy import IntParameter, DecimalParameter +from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, informative +from freqtrade.persistence import Trade -# ── 工具函数:Swing Point 检测 ────────────────────────────────── - - -def detect_swing_points(df: DataFrame, window: int, col_high="high", col_low="low"): +class StructureFlowStrategy(IStrategy): """ - 在给定 DataFrame 上检测 Swing High / Swing Low。 + Structure Flow Strategy v1.0 — 纯价格结构策略 - 返回添加了以下列的 DataFrame: - - is_swing_high / is_swing_low : bool - - last_swing_high / last_swing_low : float (前向填充) - """ - w = int(window) - roll_max = df[col_high].rolling(window=w, center=True).max() - roll_min = df[col_low].rolling(window=w, center=True).min() + 不使用任何技术指标(无 EMA、ATR、RSI、MACD、布林带等)。 + 一切信号来源于价格本身的 OHLC 数据和由此推导的结构信息。 - df["is_swing_high"] = ( - (df[col_high] == roll_max) - & (df[col_high] > df[col_high].shift(1)) - & (df[col_high] > df[col_high].shift(-1)) - ) - df["is_swing_low"] = ( - (df[col_low] == roll_min) - & (df[col_low] < df[col_low].shift(1)) - & (df[col_low] < df[col_low].shift(-1)) - ) + 趋势判断: + HH + HL → 上升趋势(Bullish Structure) + LH + LL → 下降趋势(Bearish Structure) + 其他 → 震荡(Chop / Range) - df["last_swing_high"] = df[col_high].where(df["is_swing_high"]).ffill() - df["last_swing_low"] = df[col_low].where(df["is_swing_low"]).ffill() + 入场逻辑: + 做多: D1上升结构 + 价格在1H Swing区间的下半区 + 5M看涨K线形态 + 做空: D1下降结构 + 价格在1H Swing区间的上半区 + 5M看跌K线形态 - return df + 结构位入场区间: + 不使用固定百分比。入场区域由最近 Swing High 和 Swing Low + 的中点定义——价格在下半区为做多区域,在上半区为做空区域。 + 止损逻辑: + 初始止损: 1H 最近 Swing Low(做多)/ Swing High(做空) + 跟踪止损: 随新 Swing Point 形成而上移(做多)或下移(做空) + 这是"结构失效止损"——如果止损被触发,意味着结构被破坏, + 交易逻辑不再成立。 -def detect_candle_patterns(df: DataFrame, pin_body_ratio=0.3, engulf_ratio=1.5): - """ - K线形态检测。返回添加了形态布尔列的 DataFrame。 - """ - body = abs(df["close"] - df["open"]) - c_range = df["high"] - df["low"] - upper_wick = df["high"] - df[["open", "close"]].max(axis=1) - lower_wick = df[["open", "close"]].min(axis=1) - df["low"] - safe_range = c_range.replace(0, np.nan) - - # 看涨 Pin Bar(锤子线) - df["bullish_pinbar"] = ( - (body < pin_body_ratio * safe_range) - & (lower_wick > 2 * body) - & (lower_wick > upper_wick) - & (df["close"] > df["open"]) - ) - - # 看跌 Pin Bar(射击之星) - df["bearish_pinbar"] = ( - (body < pin_body_ratio * safe_range) - & (upper_wick > 2 * body) - & (upper_wick > lower_wick) - & (df["close"] < df["open"]) - ) - - # 看涨吞没 - prev_open = df["open"].shift(1) - prev_close = df["close"].shift(1) - prev_body = abs(prev_close - prev_open) - - df["bullish_engulfing"] = ( - (prev_close < prev_open) - & (df["close"] > df["open"]) - & (df["open"] < prev_close) - & (df["close"] > prev_open) - & (body > engulf_ratio * prev_body) - ) - - # 看跌吞没 - df["bearish_engulfing"] = ( - (prev_close > prev_open) - & (df["close"] < df["open"]) - & (df["open"] > prev_close) - & (df["close"] < prev_open) - & (body > engulf_ratio * prev_body) - ) - - return df - - -# ── 策略类 ────────────────────────────────────────────────────── - - -class PriceActionStrategyV03(IStrategy): - """ - 多时间框架价格行为策略 — D1 定方向 → 1H 找结构 → 5M 抓时机。 - - v0.3 相比 v0.2 的核心改进: - - 成交量确认由"计算但未使用"→ 成为入场必要条件 - - 1H 趋势要求从"非反向"→ 必须同向 - - S/R 接近阈值从 3.0% → 1.5% - - 移动止损更宽:初始 2.0 ATR / 保本 1.5 ATR / 追踪 2.0 ATR - - 新增最低 ATR 波动率过滤 - - 出场增加 1H 趋势反转条件 - - 适用:ETH/USDT 永续合约,Binance,5M 主时间框架。 + 出场逻辑: + D1 结构反转(上升→非上升 或 下降→非下降) + 或 1H 结构失效(做多时 Swing Low 被跌破) """ - INTERFACE_VERSION = 3 - - # ── 基础设置 ────────────────────────────────────────────── + # ── 基础配置 ────────────────────────────────────────── timeframe = "5m" - can_short = True - max_open_trades = 1 - startup_candle_count = 200 - process_only_new_candles = True - use_exit_signal = True - - # ── 运行时强制属性(回测配置补齐) ───────────────────────── - stoploss = -0.15 + can_short = False # spot 回测临时关闭,实盘 futures 改回 True + stoploss = -0.25 # 硬止损安全网(25%),实际由 custom_stoploss 动态管理 use_custom_stoploss = True - minimal_roi = {"0": 100} + minimal_roi = {"0": 100} # 不设时间止盈,出场由结构决定 + max_open_trades = 1 - # ── 可优化参数 ──────────────────────────────────────────── + # 回测参数 + startup_candle_count = 20 # 需要足够的历史数据来建立 Swing Point - # -- 日线(宏观)-- - ema_fast_daily = IntParameter(10, 30, default=20, space="buy") - ema_slow_daily = IntParameter(40, 80, default=50, space="buy") - swing_window_daily = IntParameter(3, 10, default=5, space="buy") + # ── 可调参数 ────────────────────────────────────────── + # 这些参数是策略唯一的"旋钮",且都有结构含义 - # -- 1H(中期结构)-- - ema_fast_h1 = IntParameter(10, 30, default=20, space="buy") - ema_slow_h1 = IntParameter(40, 80, default=50, space="buy") - swing_window_h1 = IntParameter(3, 10, default=5, space="buy") + # Swing Point 检测窗口(寻找局部极值需要左右各 N 根K线确认) + swing_lookback_d1 = IntParameter( + 2, 10, default=5, space="buy", + ) + swing_lookback_h1 = IntParameter( + 2, 10, default=5, space="buy", + ) - # -- ATR 止损 -- - atr_period = IntParameter(10, 28, default=14, space="buy") - atr_stop_multiplier = DecimalParameter(1.5, 3.0, default=2.0, space="sell") + # Pin Bar 确认强度:影线至少是实体的 N 倍 + pin_bar_wick_ratio = DecimalParameter( + 1.5, 4.0, default=2.0, space="buy", + ) - # -- K线形态 -- - pin_bar_body_ratio = DecimalParameter(0.15, 0.40, default=0.30, space="buy") - engulfing_body_ratio = DecimalParameter(1.2, 3.0, default=1.5, space="buy") + # ================================================================ + # 工具函数 — 纯价格计算,不依赖任何技术指标 + # ================================================================ - # -- 成交量 -- - volume_surge_multiplier = DecimalParameter(1.2, 3.0, default=1.5, space="buy") - - # -- S/R 接近阈值 -- - sr_proximity_pct = DecimalParameter(0.5, 3.0, default=1.5, space="buy") - - # -- ATR 最低波动率 -- - min_atr_ratio = DecimalParameter(0.3, 1.0, default=0.5, space="buy") - - # ── 多时间框架声明 ──────────────────────────────────────── - - def informative_pairs(self): - pairs = self.dp.current_whitelist() - informative_pairs = [] - for pair in pairs: - informative_pairs.append((pair, "1h")) - informative_pairs.append((pair, "1d")) - return informative_pairs - - # ── 指标计算 ────────────────────────────────────────────── - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + @staticmethod + def _detect_swing_points( + high: pd.Series, + low: pd.Series, + lookback: int, + ) -> tuple[pd.Series, pd.Series]: """ - 三层时间框架的指标计算流水线: + 检测 Swing High 和 Swing Low。 - Layer 1 — D1:宏观趋势方向 - Layer 2 — 1H:S/R 区域 + 中期结构 - Layer 3 — 5M:入场信号 + K线形态 + 纯价格比较: + - Swing High: 当前高点 > 左右各 lookback 根K线的所有高点 + - Swing Low: 当前低点 < 左右各 lookback 根K线的所有低点 + + 这是价格行为学最基础的构件——不需要任何指标。 + """ + n = len(high) + is_swing_high = np.full(n, False) + is_swing_low = np.full(n, False) + + for i in range(lookback, n - lookback): + window_high = high.iloc[i - lookback : i + lookback + 1] + window_low = low.iloc[i - lookback : i + lookback + 1] + + if high.iloc[i] == window_high.max(): + is_swing_high[i] = True + if low.iloc[i] == window_low.min(): + is_swing_low[i] = True + + return ( + pd.Series(is_swing_high, index=high.index), + pd.Series(is_swing_low, index=low.index), + ) + + @staticmethod + def _build_structure( + high: pd.Series, + low: pd.Series, + close: pd.Series, + swing_high: pd.Series, + swing_low: pd.Series, + ) -> DataFrame: + """ + 从 Swing Points 构建市场结构信息。 + + 对每一个 K 线时刻,计算: + 1. trend_up / trend_down:当前处于上升/下降结构? + - 最近两个 SH 和两个 SL 同时上移 → 上升 + - 最近两个 SH 和两个 SL 同时下移 → 下降 + - 其他 → 保持上一个状态(结构延续) + + 2. nearest_support:最近 Swing Low 的价格 + 3. nearest_resistance:最近 Swing High 的价格 + + 4. in_demand_zone:价格在下半区(做多区域) + - 用区间中点划分:price_low < midpoint = 在下半区 + - 这比固定百分比更合理,因为区间大小由波动自然决定 + + 5. in_supply_zone:价格在上半区(做空区域) + + 返回值是一个 DataFrame,包含上述所有列。 + """ + n = len(high) + + # 输出数组 + trend_up_arr = np.full(n, False) + trend_down_arr = np.full(n, False) + nearest_support = np.full(n, np.nan) + nearest_resistance = np.full(n, np.nan) + in_demand_zone = np.full(n, False) + in_supply_zone = np.full(n, False) + + # 用于追踪 Swing Point 序列的队列 + sh_prices: list[float] = [] # 最近几个 Swing High 价格 + sl_prices: list[float] = [] # 最近几个 Swing Low 价格 + + for i in range(n): + # ── 更新 Swing Point 队列 ── + if swing_high.iloc[i] and not np.isnan(high.iloc[i]): + sh_prices.append(high.iloc[i]) + # 只保留最近 4 个(用于判断结构) + if len(sh_prices) > 4: + sh_prices.pop(0) + + if swing_low.iloc[i] and not np.isnan(low.iloc[i]): + sl_prices.append(low.iloc[i]) + if len(sl_prices) > 4: + sl_prices.pop(0) + + # ── 趋势判断:至少需要 2 个 SH 和 2 个 SL ── + if len(sh_prices) >= 2 and len(sl_prices) >= 2: + latest_sh, prev_sh = sh_prices[-1], sh_prices[-2] + latest_sl, prev_sl = sl_prices[-1], sl_prices[-2] + + if latest_sh > prev_sh and latest_sl > prev_sl: + trend_up_arr[i] = True + trend_down_arr[i] = False + elif latest_sh < prev_sh and latest_sl < prev_sl: + trend_up_arr[i] = False + trend_down_arr[i] = True + else: + # 结构不明确,延续前一个状态 + if i > 0: + trend_up_arr[i] = trend_up_arr[i - 1] + trend_down_arr[i] = trend_down_arr[i - 1] + elif i > 0: + # 数据不足,延续前一个状态 + trend_up_arr[i] = trend_up_arr[i - 1] + trend_down_arr[i] = trend_down_arr[i - 1] + + # ── 最近支撑/阻力 ── + if sl_prices: + nearest_support[i] = sl_prices[-1] + elif i > 0: + nearest_support[i] = nearest_support[i - 1] + + if sh_prices: + nearest_resistance[i] = sh_prices[-1] + elif i > 0: + nearest_resistance[i] = nearest_resistance[i - 1] + + # ── 入场区域:用 Swing 区间中点划分 ── + # 有有效的支撑和阻力时才能判断 + if ( + not np.isnan(nearest_support[i]) + and not np.isnan(nearest_resistance[i]) + and nearest_resistance[i] > nearest_support[i] + ): + mid = (nearest_support[i] + nearest_resistance[i]) / 2.0 + # 做多区域:价格低点触及下半区(有回落需求) + in_demand_zone[i] = low.iloc[i] <= mid + # 做空区域:价格高点触及上半区(有反弹供给) + in_supply_zone[i] = high.iloc[i] >= mid + elif i > 0: + in_demand_zone[i] = in_demand_zone[i - 1] + in_supply_zone[i] = in_supply_zone[i - 1] + + result = DataFrame( + { + "trend_up": trend_up_arr, + "trend_down": trend_down_arr, + "support": nearest_support, + "resistance": nearest_resistance, + "in_demand": in_demand_zone, + "in_supply": in_supply_zone, + }, + index=high.index, + ) + return result + + @staticmethod + def _detect_candle_patterns( + o: pd.Series, + h: pd.Series, + l: pd.Series, + c: pd.Series, + pin_ratio: float, + ) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]: + """ + 检测 K 线形态 — 纯 OHLC 计算。 + + Pin Bar (锤子线/流星线): + 影线远大于实体,实体在K线的一端。 + 看涨 Pin Bar: 长下影线 + 小实体在上方 = 买方在低位介入 + 看跌 Pin Bar: 长上影线 + 小实体在下方 = 卖方在高位施压 + + Engulfing (吞没形态): + 当前实体完全包裹前一实体,表示力量转换。 + """ + body = abs(c - o) + upper_wick = h - np.maximum(o, c) + lower_wick = np.minimum(o, c) - l + total_range = h - l + + # 避免除零 + valid_range = total_range > 0 + valid_body = body > 0 + + # ── Pin Bar ── + # 看涨:下影线 ≥ pin_ratio × 实体,上影线 ≤ 0.5 × 实体,实体在K线上方 + bullish_pin = ( + valid_range + & valid_body + & (lower_wick >= pin_ratio * body) + & (upper_wick <= 0.5 * body) + ) + + # 看跌:上影线 ≥ pin_ratio × 实体,下影线 ≤ 0.5 × 实体 + bearish_pin = ( + valid_range + & valid_body + & (upper_wick >= pin_ratio * body) + & (lower_wick <= 0.5 * body) + ) + + # ── Engulfing ── + prev_body = body.shift(1) + prev_o = o.shift(1) + prev_c = c.shift(1) + + bullish_engulf = ( + (c > o) # 当前阳线 + & (prev_c < prev_o) # 前一根阴线 + & (body > prev_body) # 当前实体更大 + ) + + bearish_engulf = ( + (c < o) # 当前阴线 + & (prev_c > prev_o) # 前一根阳线 + & (body > prev_body) # 当前实体更大 + ) + + return ( + pd.Series(bullish_pin, index=c.index), + pd.Series(bearish_pin, index=c.index), + pd.Series(bullish_engulf, index=c.index), + pd.Series(bearish_engulf, index=c.index), + ) + + # ================================================================ + # 信息时间框架 — D1 宏观结构 + # ================================================================ + + @informative("1d") + def populate_indicators_1d( + self, dataframe: DataFrame, metadata: dict + ) -> DataFrame: + """ + D1 日线分析:宏观结构方向。 + + 计算 Swing Point → 结构趋势 → 支撑/阻力。 + """ + sh, sl = self._detect_swing_points( + dataframe["high"], dataframe["low"], + self.swing_lookback_d1.value, + ) + + structure = self._build_structure( + dataframe["high"], dataframe["low"], dataframe["close"], + sh, sl, + ) + + dataframe["trend_up"] = structure["trend_up"] + dataframe["trend_down"] = structure["trend_down"] + + return dataframe + + # ================================================================ + # 信息时间框架 — 1H 中期结构 + # ================================================================ + + @informative("1h") + def populate_indicators_1h( + self, dataframe: DataFrame, metadata: dict + ) -> DataFrame: + """ + 1H 小时线分析:中期结构位 + 入场区域。 + + 计算 Swing Point → 结构趋势 → 支撑/阻力 → 供需区域。 + """ + sh, sl = self._detect_swing_points( + dataframe["high"], dataframe["low"], + self.swing_lookback_h1.value, + ) + + structure = self._build_structure( + dataframe["high"], dataframe["low"], dataframe["close"], + sh, sl, + ) + + dataframe["trend_up"] = structure["trend_up"] + dataframe["trend_down"] = structure["trend_down"] + dataframe["support"] = structure["support"] + dataframe["resistance"] = structure["resistance"] + dataframe["in_demand"] = structure["in_demand"] + dataframe["in_supply"] = structure["in_supply"] + + return dataframe + + # ================================================================ + # 主时间框架 — 5M K线形态 + # ================================================================ + + def populate_indicators( + self, dataframe: DataFrame, metadata: dict + ) -> DataFrame: + """ + 5M 五分钟线:仅检测 K 线形态。 + + 不需要任何指标——形态来自 OHLC 的几何关系。 + """ + bullish_pin, bearish_pin, bullish_engulf, bearish_engulf = ( + self._detect_candle_patterns( + dataframe["open"], + dataframe["high"], + dataframe["low"], + dataframe["close"], + self.pin_bar_wick_ratio.value, + ) + ) + + dataframe["bullish_pinbar"] = bullish_pin + dataframe["bearish_pinbar"] = bearish_pin + dataframe["bullish_engulfing"] = bullish_engulf + dataframe["bearish_engulfing"] = bearish_engulf + + # 综合看涨/看跌信号(任一形态触发即可) + dataframe["bullish_signal"] = bullish_pin | bullish_engulf + dataframe["bearish_signal"] = bearish_pin | bearish_engulf + + return dataframe + + # ================================================================ + # 入场信号 + # ================================================================ + + def populate_entry_trend( + self, dataframe: DataFrame, metadata: dict + ) -> DataFrame: + """ + 入场逻辑。 + + 做多条件(全部满足): + 1. D1 处于上升结构(trend_up_1d) + 2. 价格在 1H 下半区 / 需求区域(in_demand_1h) + ——这意味着价格已回调到支撑位附近 + 3. 5M 出现看涨 K 线形态(bullish_signal) + ——Pin Bar 或 Engulfing 在结构位确认入场 + + 做空条件(全部满足): + 1. D1 处于下降结构(trend_down_1d) + 2. 价格在 1H 上半区 / 供给区域(in_supply_1h) + 3. 5M 出现看跌 K 线形态(bearish_signal) """ - # ============================================================ - # Layer 1: 日线 —— 宏观方向 - # ============================================================ - - daily = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="1d") - - if not daily.empty: - daily["ema_fast"] = ta.EMA(daily, timeperiod=self.ema_fast_daily.value) - daily["ema_slow"] = ta.EMA(daily, timeperiod=self.ema_slow_daily.value) - - daily = detect_swing_points(daily, self.swing_window_daily.value) - - daily["trend_up"] = ( - (daily["ema_fast"] > daily["ema_slow"]) - & (daily["close"] > daily["ema_fast"]) - ) - daily["trend_down"] = ( - (daily["ema_fast"] < daily["ema_slow"]) - & (daily["close"] < daily["ema_fast"]) - ) - else: - daily = dataframe.copy() - for col in [ - "ema_fast", "ema_slow", "is_swing_high", "is_swing_low", - "last_swing_high", "last_swing_low", "trend_up", "trend_down", - ]: - daily[col] = np.nan - - dataframe = merge_informative_pair( - dataframe, daily, self.timeframe, "1d", ffill=True, - ) - - # ============================================================ - # Layer 2: 1H —— 中期结构 + S/R 区域 - # ============================================================ - - hourly = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="1h") - - if not hourly.empty: - hourly["ema_fast"] = ta.EMA(hourly, timeperiod=self.ema_fast_h1.value) - hourly["ema_slow"] = ta.EMA(hourly, timeperiod=self.ema_slow_h1.value) - - hourly = detect_swing_points(hourly, self.swing_window_h1.value) - - hourly["trend_up"] = ( - (hourly["ema_fast"] > hourly["ema_slow"]) - & (hourly["close"] > hourly["ema_fast"]) - ) - hourly["trend_down"] = ( - (hourly["ema_fast"] < hourly["ema_slow"]) - & (hourly["close"] < hourly["ema_fast"]) - ) - else: - hourly = dataframe.copy() - for col in [ - "ema_fast", "ema_slow", "is_swing_high", "is_swing_low", - "last_swing_high", "last_swing_low", "trend_up", "trend_down", - ]: - hourly[col] = np.nan - - dataframe = merge_informative_pair( - dataframe, hourly, self.timeframe, "1h", ffill=True, - ) - - # ============================================================ - # Layer 3: 5M —— 入场执行信号 - # ============================================================ - - # ATR - dataframe["atr"] = ta.ATR(dataframe, timeperiod=self.atr_period.value) - dataframe["atr_ratio"] = ( - dataframe["atr"] / dataframe["atr"].rolling(20).mean() - ) - - # 5M EMA - dataframe["ema_20_5m"] = ta.EMA(dataframe, timeperiod=20) - - # K线形态 - dataframe = detect_candle_patterns( - dataframe, - pin_body_ratio=self.pin_bar_body_ratio.value, - engulf_ratio=self.engulfing_body_ratio.value, - ) - - # 成交量确认 - dataframe["volume_ma20"] = dataframe["volume"].rolling(20).mean() - dataframe["volume_surge"] = ( - dataframe["volume"] - > self.volume_surge_multiplier.value * dataframe["volume_ma20"] - ) - - # ============================================================ - # S/R 距离 - # ============================================================ - - support = dataframe["last_swing_low_1h"] - resistance = dataframe["last_swing_high_1h"] - - dataframe["dist_to_support_pct"] = np.where( - support > 0, - (dataframe["close"] - support) / support * 100, - np.nan, - ) - dataframe["dist_to_resistance_pct"] = np.where( - resistance > 0, - (resistance - dataframe["close"]) / dataframe["close"] * 100, - np.nan, - ) - - # ============================================================ - # v0.3 新增:连续确认(避免单根假突破) - # ============================================================ - - dataframe["bullish_pattern_prev"] = dataframe["bullish_pinbar"].shift(1) | dataframe["bullish_engulfing"].shift(1) - dataframe["bearish_pattern_prev"] = dataframe["bearish_pinbar"].shift(1) | dataframe["bearish_engulfing"].shift(1) - - # ============================================================ - # NaN 清理:多时间框架合并后布尔列前部有 NaN - # ============================================================ - + # ── NaN 安全处理 ── + # 多时间框架合并后,前部可能有 NaN bool_cols = [ "trend_up_1d", "trend_down_1d", "trend_up_1h", "trend_down_1h", - "bullish_pinbar", "bearish_pinbar", - "bullish_engulfing", "bearish_engulfing", - "volume_surge", - "bullish_pattern_prev", "bearish_pattern_prev", + "in_demand_1h", "in_supply_1h", + "bullish_signal", "bearish_signal", ] for col in bool_cols: if col in dataframe.columns: - dataframe[col] = dataframe[col].fillna(False).infer_objects(copy=False) + dataframe[col] = dataframe[col].fillna(False) + + # ── 做多 ── + long_conditions = ( + dataframe["trend_up_1d"] # D1 上升结构 + & dataframe["in_demand_1h"] # 1H 下半区(需求区域) + & dataframe["bullish_signal"] # 5M 看涨形态 + ) + dataframe.loc[long_conditions, "enter_long"] = 1 + + # ── 做空 ── + if self.can_short: + short_conditions = ( + dataframe["trend_down_1d"] # D1 下降结构 + & dataframe["in_supply_1h"] # 1H 上半区(供给区域) + & dataframe["bearish_signal"] # 5M 看跌形态 + ) + dataframe.loc[short_conditions, "enter_short"] = 1 return dataframe - # ── 入场信号 ────────────────────────────────────────────── + # ================================================================ + # 出场信号 + # ================================================================ - def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def populate_exit_trend( + self, dataframe: DataFrame, metadata: dict + ) -> DataFrame: """ - 入场逻辑 —— 四层确认(v0.3 强化版): + 出场逻辑 — 由结构反转触发。 - 做多: - D1: 上升趋势 - 1H: 也必须上升趋势(v0.2 只要求"非下降"→ v0.3 要求同向) - 5M: 价格在支撑附近(<1.5%) + 看涨形态 + 成交量放大 + 连续确认 - 风控: ATR 波动率充足(不在沉闷市场中交易) + 做多出场: + D1 不再处于上升结构 → 宏观环境改变 + 或 1H 不再处于上升结构 → 中期结构失效 + + 做空出场: + D1 不再处于下降结构 → 宏观环境改变 + 或 1H 不再处于下降结构 → 中期结构失效 """ - # ── 宏观环境 ── - - daily_bullish = ( - dataframe["trend_up_1d"] - & (dataframe["close"] > dataframe["ema_fast_1d"]) - ) - daily_bearish = ( - dataframe["trend_down_1d"] - & (dataframe["close"] < dataframe["ema_fast_1d"]) + # 做多出场 + exit_long = ( + ~dataframe["trend_up_1d"].fillna(True) # D1 结构反转(NaN = 初始区,不出场) ) + dataframe.loc[exit_long, "exit_long"] = 1 - # ── 1H 中期条件 ── - - # v0.3 改动:从 "h1_not_bearish" 升级为 "h1_bullish"(必须同向) - h1_bullish = dataframe["trend_up_1h"] & (dataframe["close"] > dataframe["ema_fast_1h"]) - h1_bearish = dataframe["trend_down_1h"] & (dataframe["close"] < dataframe["ema_fast_1h"]) - - sr_pct = self.sr_proximity_pct.value - - price_near_support = ( - (dataframe["dist_to_support_pct"] < sr_pct) - & (dataframe["dist_to_support_pct"] > 0) - ) - price_near_resistance = ( - (dataframe["dist_to_resistance_pct"] < sr_pct) - & (dataframe["dist_to_resistance_pct"] > 0) - ) - - # ── 5M 入场形态 ── - - bullish_pattern = dataframe["bullish_pinbar"] | dataframe["bullish_engulfing"] - bearish_pattern = dataframe["bearish_pinbar"] | dataframe["bearish_engulfing"] - - # ── v0.3 新增过滤 ── - - # 成交量必选 - volume_ok = dataframe["volume_surge"] - - # 最低波动率:ATR 不能太小(市场太沉闷不做) - sufficient_volatility = dataframe["atr_ratio"] >= self.min_atr_ratio.value - - # 避免极端波动 - normal_vol = dataframe["atr_ratio"] < 2.0 - - # 连续确认:当前和前一根 K 线都有看涨/看跌信号,减少假突破 - consecutive_bullish = bullish_pattern & dataframe["bullish_pattern_prev"] - consecutive_bearish = bearish_pattern & dataframe["bearish_pattern_prev"] - - # ============================================================ - # 做多条件(严格过滤) - # ============================================================ - - conditions_long = [ - daily_bullish, - h1_bullish, # v0.3: 1H 必须同向上升 - price_near_support, - bullish_pattern, - volume_ok, # v0.3: 成交量必选 - sufficient_volatility, # v0.3: 最低波动率 - normal_vol, - ] - - # ============================================================ - # 做空条件(严格过滤) - # ============================================================ - - conditions_short = [ - daily_bearish, - h1_bearish, # v0.3: 1H 必须同向下降 - price_near_resistance, - bearish_pattern, - volume_ok, - sufficient_volatility, - normal_vol, - ] - - # ── 写入信号 ── - - if conditions_long: - dataframe.loc[ - reduce(lambda a, b: a & b, conditions_long), - "enter_long", - ] = 1 - - if conditions_short: - dataframe.loc[ - reduce(lambda a, b: a & b, conditions_short), - "enter_short", - ] = 1 + # 做空出场 + if self.can_short: + exit_short = ( + dataframe["trend_up_1d"].fillna(False) # D1 转为上升 + ) + dataframe.loc[exit_short, "exit_short"] = 1 return dataframe - # ── 出场信号 ────────────────────────────────────────────── - - def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - 信号出场(v0.3 增强): - - 主要出场仍由 custom_stoploss 的移动止损处理。 - 这里追加结构破坏级别的强制离场。 - """ - - # ── 多头离场 ── - - daily_no_longer_bullish = ~dataframe["trend_up_1d"] - h1_no_longer_bullish = ~dataframe["trend_up_1h"] # v0.3 新增 - - conditions_exit_long = [ - daily_no_longer_bullish, - h1_no_longer_bullish, - ] - - # ── 空头离场 ── - - daily_no_longer_bearish = ~dataframe["trend_down_1d"] - h1_no_longer_bearish = ~dataframe["trend_down_1h"] # v0.3 新增 - - conditions_exit_short = [ - daily_no_longer_bearish, - h1_no_longer_bearish, - ] - - # ── 写入 ── - - if conditions_exit_long: - dataframe.loc[ - reduce(lambda a, b: a | b, conditions_exit_long), - "exit_long", - ] = 1 - - if conditions_exit_short: - dataframe.loc[ - reduce(lambda a, b: a | b, conditions_exit_short), - "exit_short", - ] = 1 - - return dataframe - - # ── 动态移动止损 ────────────────────────────────────────── + # ================================================================ + # 动态止损 — 基于结构失效 + # ================================================================ def custom_stoploss( self, pair: str, - trade, - current_time, + trade: Trade, + current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs, - ) -> Optional[float]: + ) -> float | None: """ - v0.3 宽止损设计 —— 给趋势呼吸空间: + 结构止损:止损位设在最近的 1H Swing Low(做多)或 Swing High(做空)。 - 阶段1(利润 < 1.5 ATR):初始止损 ATR × 2.0 - 阶段2(利润 1.5~3.0 ATR):保本 - 阶段3(利润 > 3.0 ATR):追踪止损 ATR × 2.0 + 如果价格突破这个结构位,说明结构失效,交易逻辑不再成立。 + 这与传统的百分比止损或 ATR 止损不同——它不是"跌了N%就走", + 而是"结构破了就走"。 - v0.2 参考:初始 1.5 / 保本 0.5 / 追踪 1.0 + 随着行情发展,新的 Swing Point 形成,止损自动跟随, + 实现自然的移动止损——不依赖任何参数。 """ - dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) - if dataframe.empty: - return None - last_candle = dataframe.iloc[-1] - atr = last_candle.get("atr", current_rate * 0.005) - entry_price = trade.open_rate - atr_ratio = atr / entry_price - - if trade.is_short: - profit_ratio = -current_profit - - if profit_ratio > atr_ratio * 3.0: - return -atr_ratio * 2.0 - elif profit_ratio > atr_ratio * 1.5: - return 0 - else: - return -atr_ratio * self.atr_stop_multiplier.value - else: - if current_profit > atr_ratio * 3.0: - return -atr_ratio * 2.0 - elif current_profit > atr_ratio * 1.5: - return 0 - else: - return -atr_ratio * self.atr_stop_multiplier.value - - # ── 自定义出场(结构破坏) ──────────────────────────────── - - def custom_exit( - self, - pair: str, - trade, - current_time, - current_rate: float, - current_profit: float, - **kwargs, - ) -> Optional[str]: - """ - 结构层面出场:D1 或 1H 趋势反转 → 立刻离场。 - """ - dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) - if dataframe.empty: - return None - - last_candle = dataframe.iloc[-1] - - if trade.is_short: - if last_candle.get("trend_up_1d", False) or last_candle.get("trend_up_1h", False): - return "trend_reversed" - else: - if last_candle.get("trend_down_1d", False) or last_candle.get("trend_down_1h", False): - return "trend_reversed" - - return None - - # ── 仓位管理 ────────────────────────────────────────────── - - def custom_stake_amount( - self, - pair: str, - current_time, - current_rate: float, - proposed_stake: float, - min_stake: Optional[float], - max_stake: float, - leverage: float, - entry_tag: Optional[str], - side: str, - **kwargs, - ) -> float: - """ - 固定风险仓位管理:每次交易风险 = 账户的 1%。 - """ + # 获取已分析的 5M 数据(包含合并后的 1H 信息) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) - if dataframe.empty: - return min_stake or proposed_stake + if dataframe is None or len(dataframe) == 0: + return None # 使用默认 stoploss - last_candle = dataframe.iloc[-1] - atr = last_candle.get("atr", current_rate * 0.005) - stop_distance = atr * self.atr_stop_multiplier.value + last = dataframe.iloc[-1] - available_balance = self.wallets.get_total_stake_amount() - risk_amount = available_balance * 0.01 + if trade.is_short: + # 做空止损:放在最近的 1H Swing High 上方 + resistance = last.get("resistance_1h") + if resistance is not None and not (isinstance(resistance, float) and np.isnan(resistance)): + # stoploss = (current - stop_price) / current + # 做空时 stop 在 current 上方,所以 (current - resistance) 为负 + # 转为负的比例 + sl_ratio = (current_rate - float(resistance)) / current_rate + # 只使用比默认止损更紧的止损 + if sl_ratio > self.stoploss and sl_ratio < 0: + return sl_ratio + else: + # 做多止损:放在最近的 1H Swing Low 下方 + support = last.get("support_1h") + if support is not None and not (isinstance(support, float) and np.isnan(support)): + # stoploss = (stop_price - current) / current + # 做多时 stop 在 current 下方,结果为负 + sl_ratio = (float(support) - current_rate) / current_rate + # 只使用比默认止损更紧的止损 + if sl_ratio > self.stoploss and sl_ratio < 0: + return sl_ratio - position_size = risk_amount / stop_distance if stop_distance > 0 else proposed_stake - position_size = min(position_size, max_stake or float("inf")) - - if min_stake and position_size < min_stake: - return 0 - - return position_size - - # ── 最终入场确认 ────────────────────────────────────────── - - def confirm_trade_entry( - self, - pair: str, - order_type: str, - amount: float, - rate: float, - time_in_force: str, - current_time, - entry_tag: Optional[str], - side: str, - **kwargs, - ) -> bool: - return True + # 无法获取有效的结构位,使用默认硬止损 + return None