Files
beast-trader-strategies/strategy.py

440 lines
15 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 Strategy v1.3
=======================
变更记录:
v1.0 (2026-06-07): 纯价格结构策略D1定方向→4H定位→1H入场支撑/阻力来自Swing Point
v1.1 (2026-06-07): 修复freqtrade 2026.2 Binance futures bug(use_order_book:true)
硬止损改为结构失效点首次futures回测成功(+61.52%)
v1.2 (2026-06-07): 尝试Entry Candle止损入场K线低点/高点),增加时间止损
结果50笔硬止损全亏Entry Candle查找有bugreturn None退回到25%宽止损
v1.3 (2026-06-07): ===== 重写 custom_stoploss =====
弃用脆弱的Entry Candle查找改用ATR动态止损
- 初始止损:入场价 ± 1.0 ATR快速认输
- 盈利>1%:移动止损至保本线
- 盈利>2%ATR追踪止损锁定利润
- 硬止损安全网:-5%stoploss属性
核心哲学:预估错误的交易,早早认输止损离场,不硬扛单
"""
from datetime import datetime
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV13(IStrategy):
"""
Structure Flow Strategy v1.3
核心逻辑:
D1 定宏观方向HH/HL 上升LH/LL 下降)
4H 定位结构位Swing Point → 支撑/阻力区域)
1H 找入场时机K线形态 + 在结构区域内)
止损逻辑v1.3重写):
- 初始止损:入场价 ± 1.0 ATR快速认输
- 盈利 > 1%移动止损至保本open_rate ± 0.1%
- 盈利 > 2%ATR追踪止损current_rate ∓ 1.0 ATR
- 硬止损安全网:-5%(防止极端行情)
"""
# =====================
# 基础属性
# =====================
can_short = True
stoploss = -0.05 # 硬止损安全网 5%,实际由 custom_stoploss 动态管理
use_custom_stoploss = True
minimal_roi = {"0": 100} # 不设时间止盈,靠移动止损出场
max_open_trades = 1
timeframe = "1h"
# =====================
# 可优化参数
# =====================
swing_lookback_d1 = IntParameter(8, 14, default=10, space="buy")
swing_lookback_h4 = IntParameter(5, 10, default=8, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
"""检测 Swing High / Swing Low。"""
n = len(high)
sh = pd.Series(np.nan, index=high.index, dtype=float)
sl = pd.Series(np.nan, index=low.index, dtype=float)
for i in range(window, n - window):
if high.iloc[i] > high.iloc[i - window : i].max() and high.iloc[i] > high.iloc[i + 1 : i + window + 1].max():
sh.iloc[i] = high.iloc[i]
if low.iloc[i] < low.iloc[i - window : i].min() and low.iloc[i] < low.iloc[i + 1 : i + window + 1].min():
sl.iloc[i] = low.iloc[i]
return sh, sl
# =====================
# 工具:结构分析
# =====================
def _build_structure(
self,
high: pd.Series,
low: pd.Series,
close: pd.Series,
swing_high: pd.Series,
swing_low: pd.Series,
) -> DataFrame:
"""从 Swing Points 构建市场结构信息。"""
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)
sh_prices = []
sl_prices = []
for i in range(n):
if swing_high.iloc[i] and not np.isnan(high.iloc[i]):
sh_prices.append(high.iloc[i])
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)
# 趋势判断
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
elif latest_sh < prev_sh and latest_sl < prev_sl:
trend_down_arr[i] = True
else:
# 沿用上一根K线的状态
trend_up_arr[i] = trend_up_arr[i - 1] if i > 0 else False
trend_down_arr[i] = trend_down_arr[i - 1] if i > 0 else False
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]
if sh_prices:
nearest_resistance[i] = sh_prices[-1]
# 需求/供给区域
c = close.iloc[i]
if not np.isnan(nearest_support[i]) and not np.isnan(nearest_resistance[i]):
zone_range = nearest_resistance[i] - nearest_support[i]
if zone_range > 0:
pos_pct = (c - nearest_support[i]) / zone_range
in_demand_zone[i] = pos_pct < 0.35
in_supply_zone[i] = pos_pct > 0.65
return 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)
# =====================
# 工具K线形态检测
# =====================
@staticmethod
def _detect_candle_patterns(
open_: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
pin_bar_wick_ratio: float = 0.6,
) -> tuple[pd.Series, pd.Series, pd.Series, pd.Series]:
"""检测 Pin Bar 和 Engulfing 形态。"""
body = (close - open_).abs()
total_range = (high - low).replace(0, 0.0001)
upper_wick = high - close.where(close > open_, open_)
lower_wick = open_.where(close > open_, close) - low
is_pin = (upper_wick + lower_wick) / total_range > pin_bar_wick_ratio
bullish_pin = is_pin & (close > open_) & (lower_wick > upper_wick)
bearish_pin = is_pin & (close < open_) & (upper_wick > lower_wick)
prev_open = open_.shift(1)
prev_close = close.shift(1)
bullish_engulf = (close > prev_open) & (open_ < prev_close) & (close > open_)
bearish_engulf = (close < prev_open) & (open_ > prev_close) & (close < open_)
return bullish_pin, bearish_pin, bullish_engulf, bearish_engulf
# =====================
# 工具ATR 计算
# =====================
@staticmethod
def _calc_atr(
high: pd.Series,
low: pd.Series,
close: pd.Series,
period: int = 14,
) -> pd.Series:
"""计算 ATR。"""
prev_close = close.shift(1)
tr = pd.concat(
[high - low, (high - prev_close).abs(), (low - prev_close).abs()],
axis=1,
).max(axis=1)
return tr.rolling(period).mean()
# ================================================================
# 信息时间框架 — D1 宏观结构
# ================================================================
@informative("1d")
def populate_indicators_1d(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
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
# ================================================================
# 信息时间框架 — 4H 中期结构
# ================================================================
@informative("4h")
def populate_indicators_4h(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_h4.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"]
# 4H ATR保留可能用于未来优化
dataframe["atr"] = self._calc_atr(
dataframe["high"], dataframe["low"], dataframe["close"], period=14
)
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别K线形态 + ATR。"""
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_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
# 1H ATR用于动态止损
dataframe["atr_1h"] = self._calc_atr(
dataframe["high"], dataframe["low"], dataframe["close"], period=14
)
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑1H 时间框架)。
做多条件:
1. D1 上升结构trend_up_1d
2. 4H 下半区 / 需求区域in_demand_4h
3. 1H 看涨 K 线形态bullish_signal
做空条件:
1. D1 下降结构trend_down_1d
2. 4H 上半区 / 供给区域in_supply_4h
3. 1H 看跌 K 线形态bearish_signal
"""
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# 做多
long_conditions = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
& dataframe["bullish_signal"]
)
dataframe.loc[long_conditions, "enter_long"] = 1
# 做空
short_conditions = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
& dataframe["bearish_signal"]
)
dataframe.loc[short_conditions, "enter_short"] = 1
return dataframe
# =====================
# 出场信号
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""出场逻辑 — 由结构反转触发。"""
exit_long = ~dataframe["trend_up_1d"].fillna(True)
dataframe.loc[exit_long, "exit_long"] = 1
exit_short = dataframe["trend_up_1d"].fillna(False)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# =====================
# 动态止损 — v1.3 重写
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
v1.3 止损逻辑(完全重写):
核心哲学:「预估错误的交易,早早认输止损离场,而不要硬扛单」
三阶段止损:
阶段一(无盈利或微盈利 < 1%
止损 = 入场价 ± 1.0 ATR
→ 距离近,价格稍有不利变动就止损,快速认输
阶段二(盈利 1% ~ 2%
止损移动至保本线open_rate ± 0.1%
→ 这笔交易已经不亏了,卸下心理负担
阶段三(盈利 > 2%
追踪止损 = current_rate ∓ 1.0 ATR
→ 价格回调超过1ATR才出场给趋势足够的呼吸空间
参数说明:
- ATR 来自当前1H K线的 atr_1h 值
- 如果 ATR 为 NaNfallback 到 2% 固定止损
- 最终返回的止损比率不会超过 -5%(硬止损安全网)
"""
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
last_candle = dataframe.iloc[-1]
atr = last_candle.get("atr_1h", np.nan)
if pd.isna(atr) or atr <= 0:
atr = current_rate * 0.02
else:
atr = float(atr)
open_rate = trade.open_rate
if not trade.is_short:
# ── 做多 ──
if current_profit <= 0.01:
sl_price = open_rate - atr * 1.0
elif current_profit <= 0.02:
sl_price = open_rate * 0.999
else:
sl_price = current_rate - atr * 1.0
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.05)
else:
# ── 做空 ──
if current_profit <= 0.01:
sl_price = open_rate + atr * 1.0
elif current_profit <= 0.02:
sl_price = open_rate * 1.001
else:
sl_price = current_rate + atr * 1.0
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.05)