Files
beast-trader-strategies/strategy.py

516 lines
19 KiB
Python
Raw 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.

# structure_flow_momentum_scalp.py
# 顺趋势剥头皮策略 v2.0
#
# 核心思路不再在S/R处做反向交易接飞刀而是顺趋势方向等回调后入场。
#
# ┌─────────────────────────────────────────────────────────────┐
# │ 15m趋势方向判断EMA20 vs EMA50
# │ ↓ │
# │ 上升趋势 → 只等5m回调到EMA20/支撑附近 → 止跌信号 → 做多 │
# │ 下降趋势 → 只等5m反弹到EMA20/阻力附近 → 止涨信号 → 做空 │
# │ ↓ │
# │ 止损ATR×1.0 | 止盈ATR×1.5 | 时间止损60分钟 │
# └─────────────────────────────────────────────────────────────┘
#
# v2.0 (2026-06-10): 初始版本,完全重写
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, informative
from pandas import DataFrame
import pandas as pd
import numpy as np
from datetime import datetime
from freqtrade.persistence import Trade
class StructureFlowMomentumScalp(IStrategy):
"""
顺趋势剥头皮策略 v2.0
核心逻辑:
- 15m EMA趋势方向过滤只做顺趋势方向的单
- 5m 回调到EMA20或S/R支撑/阻力区域时等待K线信号确认后入场
- 止损 ATR×1.0,止盈 ATR×1.5,时间止损 60 分钟
- 不做方向猜测,不吃鱼头鱼尾,只吃回调结束那一小段
"""
# ── 时间框架 ──
timeframe = "5m"
# ── 交易参数 ──
can_short = True
max_open_trades = 1
stake_amount = "unlimited"
use_custom_stoploss = True
use_exit_signal = False # 出场完全由 custom_stoploss + custom_exit 管理
# ── 合约参数 ──
margin_mode = "cross"
trading_mode = "futures"
# ── 可优化参数 ──
# 趋势检测
trend_ema_period = IntParameter(10, 30, default=20, space="buy")
# 回调确认幅度
pullback_deviation = DecimalParameter(0.2, 1.0, default=0.5, decimals=1, space="buy")
# 入场冷却期
cooldown_bars = IntParameter(2, 8, default=3, space="buy")
# K线形态灵敏度
pin_bar_wick_ratio = IntParameter(50, 80, default=60, space="buy")
# 止损ATR倍数
atr_mult_stop = DecimalParameter(0.8, 2.0, default=1.0, decimals=1, space="sell")
# 止盈ATR倍数
atr_mult_tp = DecimalParameter(1.0, 3.0, default=1.5, decimals=1, space="sell")
# ── 常数 ──
time_stop_minutes = 60 # 最大持仓时间
# ── 保护性止损 ──
stoploss = -0.10 # 硬止损 10%
# ================================================================
# 杠杆
# ================================================================
def leverage(
self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs,
) -> float:
"""20x 杠杆起步,验证胜率后再上量"""
return min(20.0, max_leverage)
# ================================================================
# 信息时间框架 — 15m 趋势判断 + S/R
# ================================================================
@informative("15m")
def populate_indicators_15m(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""15m级别EMA趋势方向 + swing point S/R。"""
# ── EMA 趋势方向 ──
ema_period = self.trend_ema_period.value
dataframe["ema_fast"] = dataframe["close"].ewm(span=ema_period, adjust=False).mean()
dataframe["ema_slow"] = dataframe["close"].ewm(span=ema_period * 2.5, adjust=False).mean()
dataframe["trend_up"] = dataframe["ema_fast"] > dataframe["ema_slow"]
dataframe["trend_down"] = dataframe["ema_fast"] < dataframe["ema_slow"]
# ── Swing Point 支撑/阻力 ──
high = dataframe["high"].tolist()
low = dataframe["low"].tolist()
close = dataframe["close"].tolist()
sh, sl = self._detect_swing_points(high, low, window=5)
trend_up_arr, trend_down_arr, support_arr, resistance_arr = self._build_structure(
high, low, close, sh, sl,
)
dataframe["trend_up_sp"] = trend_up_arr
dataframe["trend_down_sp"] = trend_down_arr
# EMA平滑S/R避免跳变
dataframe["support"] = self._ema_smooth(support_arr, alpha=0.3)
dataframe["resistance"] = self._ema_smooth(resistance_arr, alpha=0.3)
return dataframe
# ================================================================
# 主框架 — 5m 级别指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""5m级别ATR + K线形态 + EMA趋势整合。"""
# ── ATR(14) ──
high = dataframe["high"]
low = dataframe["low"]
close = dataframe["close"]
prev_close = close.shift(1)
tr = pd.concat([
high - low,
(high - prev_close).abs(),
(low - prev_close).abs(),
], axis=1).max(axis=1)
dataframe["atr"] = tr.rolling(14).mean()
atr_mean = dataframe["atr"].mean()
dataframe["atr"] = dataframe["atr"].fillna(atr_mean)
# ── K线形态 ──
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 / 100.0,
)
)
dataframe["bullish_signal"] = bullish_pin | bullish_engulf
dataframe["bearish_signal"] = bearish_pin | bearish_engulf
# ── 5m EMA用于短期拉回确认 ──
dataframe["ema5"] = close.ewm(span=5, adjust=False).mean()
dataframe["ema8"] = close.ewm(span=8, adjust=False).mean()
# ── 布尔列NaN填充 ──
for col in ["bullish_signal", "bearish_signal"]:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# ================================================================
# 入场逻辑
# ================================================================
def populate_entry_trend(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""
入场逻辑。
只做顺趋势回调入场不做S/R反向交易
做多条件:
1. 15m 上升趋势EMA_fast > EMA_slow
2. 5m 价格回调到15m EMA_fast 或 支撑位附近
3. 5m K线止跌信号pinbar/engulfing
做空条件(对称):
1. 15m 下降趋势
2. 5m 价格反弹到15m EMA_fast 或 阻力位附近
3. 5m K线止涨信号
"""
cooldown = self.cooldown_bars.value
dev = self.pullback_deviation.value / 100.0 # 0.5% → 0.005
# ── 必要列检查 ──
required = [
"ema_fast_15m", "trend_up_15m", "trend_down_15m",
"support_15m", "resistance_15m",
]
for col in required:
if col not in dataframe.columns:
return dataframe
# ── 布尔列填充 ──
for col in [
"bullish_signal", "bearish_signal",
"trend_up_15m", "trend_down_15m",
]:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ═══════════════════════════════════════════════════════════
# 做多:上升趋势 + 回调到EMA/支撑 + 止跌信号
# ═══════════════════════════════════════════════════════════
# 条件115m 上升趋势
trend_up = dataframe["trend_up_15m"]
# 条件2价格在EMA20或支撑位附近回调到顺趋势的支撑区
near_ema = (
(dataframe["low"] <= dataframe["ema_fast_15m"] * (1.0 + dev * 0.5)) &
(dataframe["low"] >= dataframe["ema_fast_15m"] * (1.0 - dev * 2.0))
)
near_support = (
(dataframe["low"] <= dataframe["support_15m"] * (1.0 + dev)) &
(dataframe["low"] >= dataframe["support_15m"] * (1.0 - dev))
)
pullback_long = near_ema | near_support
# 条件3K线止跌信号
signal_long = dataframe["bullish_signal"]
# 综合入场
enter_long = trend_up & pullback_long & signal_long
long_recent = enter_long.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[enter_long & long_recent, "enter_long"] = 1
# ═══════════════════════════════════════════════════════════
# 做空:下降趋势 + 反弹到EMA/阻力 + 止涨信号
# ═══════════════════════════════════════════════════════════
# 条件115m 下降趋势
trend_down = dataframe["trend_down_15m"]
# 条件2价格在EMA20或阻力位附近反弹到顺趋势的阻力区
near_ema_short = (
(dataframe["high"] >= dataframe["ema_fast_15m"] * (1.0 - dev * 0.5)) &
(dataframe["high"] <= dataframe["ema_fast_15m"] * (1.0 + dev * 2.0))
)
near_resistance = (
(dataframe["high"] >= dataframe["resistance_15m"] * (1.0 - dev)) &
(dataframe["high"] <= dataframe["resistance_15m"] * (1.0 + dev))
)
pullback_short = near_ema_short | near_resistance
# 条件3K线止涨信号
signal_short = dataframe["bearish_signal"]
# 综合入场
enter_short = trend_down & pullback_short & signal_short
short_recent = enter_short.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[enter_short & short_recent, "enter_short"] = 1
return dataframe
# ================================================================
# exit_trendfreqtrade 2025.11 强制要求,即使 use_exit_signal=False
# ================================================================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场完全由 custom_stoploss + custom_exit 管理。"""
return dataframe
# ================================================================
# 出场 — 止损ATR动态
# ================================================================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损 = 入场价 ± ATR × atr_mult_stop
- ATR值从入场K线锁定持仓期间不变
- 做多entry_price - (locked_atr × mult)
- 做空entry_price + (locked_atr × mult)
- 配20x杠杆ATR×1.0 ≈ 对应约 $3.7 止损当前5m ATR~$3.74
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return -0.02 if not trade.is_short else 0.02
entry_row = self._get_entry_row(dataframe, trade)
if entry_row is None:
return -0.02 if not trade.is_short else 0.02
atr = entry_row.get("atr", np.nan)
if pd.isna(atr) or atr <= 0:
return -0.02 if not trade.is_short else 0.02
mult = self.atr_mult_stop.value
if not trade.is_short:
sl_price = trade.open_rate - (atr * mult)
sl_ratio = (sl_price / trade.open_rate) - 1.0
return max(sl_ratio, -self.stoploss)
else:
sl_price = trade.open_rate + (atr * mult)
sl_ratio = 1.0 - (sl_price / trade.open_rate)
return min(sl_ratio, self.stoploss)
# ================================================================
# 出场 — 止盈ATR动态+ 时间止损
# ================================================================
def custom_exit(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
**kwargs,
) -> str | None:
"""
出场逻辑:
1. ATR止盈利润达到入场时锁定的 ATR × atr_mult_tp → 止盈
2. 时间止损:持仓超过 time_stop_minutes → 强制出场
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return None
entry_row = self._get_entry_row(dataframe, trade)
if entry_row is None:
return None
atr = entry_row.get("atr", np.nan)
if pd.isna(atr) or atr <= 0:
return None
# 1. ATR 止盈
tp_mult = self.atr_mult_tp.value
tp_ratio = (atr * tp_mult) / trade.open_rate
if current_profit >= tp_ratio:
return "atr_tp"
# 2. 时间止损
elapsed = (current_time - trade.open_date).total_seconds() / 60.0
if elapsed >= self.time_stop_minutes:
return "time_stop"
return None
# ================================================================
# 工具函数
# ================================================================
def _detect_swing_points(
self, highs: list, lows: list, window: int = 5
):
"""
Swing High / Swing Low 检测。
当一根K线的最高价高于其两侧window根K线的最高价时标记为Swing High。
Swing Low同理。
"""
n = len(highs)
swing_high = [np.nan] * n
swing_low = [np.nan] * n
for i in range(window, n - window):
# Swing High
is_high = True
for j in range(i - window, i + window + 1):
if j == i:
continue
if highs[j] >= highs[i]:
is_high = False
break
if is_high:
swing_high[i] = highs[i]
# Swing Low
is_low = True
for j in range(i - window, i + window + 1):
if j == i:
continue
if lows[j] <= lows[i]:
is_low = False
break
if is_low:
swing_low[i] = lows[i]
return swing_high, swing_low
def _build_structure(
self, highs: list, lows: list, closes: list,
swing_high: list, swing_low: list,
):
"""构建趋势结构和支撑/阻力位。"""
n = len(highs)
trend_up = [False] * n
trend_down = [False] * n
support = [np.nan] * n
resistance = [np.nan] * n
# 用最近4个swing point的位置判断
last_sh_idx = -1
last_sl_idx = -1
prev_sh = []
prev_sl = []
for i in range(n):
if not np.isnan(swing_high[i]):
prev_sh.append(swing_high[i])
last_sh_idx = i
if len(prev_sh) > 4:
prev_sh.pop(0)
if not np.isnan(swing_low[i]):
prev_sl.append(swing_low[i])
last_sl_idx = i
if len(prev_sl) > 4:
prev_sl.pop(0)
# 趋势判断最新的HH > 次新的HH = 上升趋势中的higher high
if len(prev_sh) >= 2 and prev_sh[-1] > prev_sh[-2]:
trend_up[i] = True
# 趋势判断最新的LL < 次新的LL = 下降趋势中的lower low
if len(prev_sl) >= 2 and prev_sl[-1] < prev_sl[-2]:
trend_down[i] = True
# 支撑 = 最近的有效Swing LowEMA平滑后在调用侧处理
if prev_sl:
support[i] = prev_sl[-1]
if prev_sh:
resistance[i] = prev_sh[-1]
return trend_up, trend_down, support, resistance
def _ema_smooth(self, values: list, alpha: float = 0.3):
"""对数组做EMA平滑避免跳变。"""
result = [np.nan] * len(values)
ema = None
for i, v in enumerate(values):
if pd.isna(v) or v is None:
if ema is not None:
result[i] = ema
continue
if ema is None:
ema = v
else:
ema = alpha * v + (1 - alpha) * ema
result[i] = ema
return np.array(result)
def _detect_candle_patterns(
self, opens, highs, lows, closes, wick_ratio=0.6,
):
"""检测K线形态pinbar锤子线/射击星)和吞没形态。"""
n = len(opens)
bullish_pin = [False] * n
bearish_pin = [False] * n
bullish_engulf = [False] * n
bearish_engulf = [False] * n
for i in range(n):
o, h, l, c = opens[i], highs[i], lows[i], closes[i]
total_range = h - l if h > l else 0.001
is_bullish = c > o
is_bearish = c < o
body = abs(c - o)
upper_wick = h - max(c, o)
lower_wick = min(c, o) - l
# Pinbar影线 > total_range × wick_ratio
if is_bullish and lower_wick / total_range > wick_ratio:
bullish_pin[i] = True
if is_bearish and upper_wick / total_range > wick_ratio:
bearish_pin[i] = True
# 吞没形态
if i > 0:
prev_o = opens[i - 1]
prev_c = closes[i - 1]
if is_bullish and c > prev_o and o < prev_c:
bullish_engulf[i] = True
if is_bearish and c < prev_o and o > prev_c:
bearish_engulf[i] = True
return (
pd.Series(bullish_pin),
pd.Series(bearish_pin),
pd.Series(bullish_engulf),
pd.Series(bearish_engulf),
)
def _get_entry_row(self, dataframe: DataFrame, trade: Trade):
"""查找入场K线行兼容live/backtesting两种模式。"""
if "date" in dataframe.columns:
entry_mask = pd.to_datetime(dataframe["date"]) <= trade.open_date
if not entry_mask.any():
return None
return dataframe[entry_mask].iloc[-1]
else:
try:
idx = dataframe.index.get_indexer([trade.open_date], method="pad")
if idx[0] < 0 or idx[0] >= len(dataframe):
return None
return dataframe.iloc[idx[0]]
except (TypeError, ValueError):
return None