Files
beast-trader-strategies/strategy.py

580 lines
21 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.2
# 纯价格结构策略 — 零技术指标,价格行为学驱动
#
# 版本变化 v1.1 → v1.2:
# - 硬止损改为 Entry Candle 失效点做多→入场K线低点做空→入场K线高点
# - 新增时间止损:入场后 N 根K线内无盈利则主动出场
# - 保留 trailing_stop结构跟踪止损盈利后切换
# - 策略类重命名为 StructureFlowStrategyV12
#
# 设计哲学:
# 趋势由 HH/HL 定义,支撑阻力由 Swing Point 定义,
# 止损由 Entry Candle 失效点定义,出场由结构反转定义。
#
# 多时间框架:
# D1 → 宏观结构方向
# 4H → 中期结构位 + 入场区域判定
# 1H → K线形态确认入场时机
# ============================================================================
from datetime import datetime, timedelta
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, informative
from freqtrade.persistence import Trade
class StructureFlowStrategyV12(IStrategy):
"""
Structure Flow Strategy v1.2 — 纯价格结构策略
不使用任何技术指标(无 EMA、ATR、RSI、MACD、布林带等
一切信号来源于价格本身的 OHLC 数据和由此推导的结构信息。
趋势判断:
HH + HL → 上升趋势Bullish Structure
LH + LL → 下降趋势Bearish Structure
入场逻辑:
做多: D1上升结构 + 价格在4H Swing区间下半区 + 1H看涨K线形态
做空: D1下降结构 + 价格在4H Swing区间上半区 + 1H看跌K线形态
止损逻辑v1.2 核心改进):
初始止损: Entry Candle 失效点做多→入场K线最低价做空→入场K线最高价
动态止损: 盈利后切换为结构跟踪止损custom_stoploss
时间止损: 入场后 N 根K线内无盈利则主动出场
"""
# ── 基础配置 ──────────────────────────────────────────
timeframe = "1h"
can_short = True
stoploss = -0.05 # 硬止损 5%,实际由 custom_stoploss 动态管理
use_custom_stoploss = True
minimal_roi = {"0": 100} # 不设时间止盈,出场由结构决定
max_open_trades = 1
# 回测参数
startup_candle_count = 40
# ── 可调参数 ──────────────────────────────────────────
swing_lookback_d1 = IntParameter(
2, 10, default=5, space="buy",
)
swing_lookback_h4 = IntParameter(
2, 10, default=5, space="buy",
)
# Pin Bar 确认强度:影线至少是实体的 N 倍
pin_bar_wick_ratio = DecimalParameter(
1.5, 4.0, default=2.0, space="buy",
)
# Entry Candle 止损缓冲(%):略低于/高于 Entry Candle 低点/高点
entry_sl_buffer = DecimalParameter(
0.001, 0.01, default=0.005, space="sell",
optimize=True,
)
# 时间止损:入场后 N 根K线内无盈利则出场
time_stop_bars = IntParameter(
6, 48, default=12, space="sell",
)
# 盈利后切换为结构止损的触发距离ATR 倍数暂无ATR用固定比例代替
profit_to_structure_sl_pct = DecimalParameter(
0.01, 0.05, default=0.02, space="sell",
optimize=True,
)
# ================================================================
# 工具函数 — 纯价格计算,不依赖任何技术指标
# ================================================================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
lookback: int,
) -> tuple[pd.Series, pd.Series]:
"""
检测 Swing High 和 Swing Low。
纯价格比较:
- 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 构建市场结构信息。
返回值包含:
trend_up / trend_down当前处于上升/下降结构
support最近 Swing Low 价格
resistance最近 Swing High 价格
in_demand价格在下半区做多区域
in_supply价格在上半区做空区域
"""
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: list[float] = []
sl_prices: list[float] = []
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])
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
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 计算。
"""
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
bullish_pin = (
valid_range
& valid_body
& (lower_wick >= pin_ratio * body)
& (upper_wick <= 0.5 * body)
)
bearish_pin = (
valid_range
& valid_body
& (upper_wick >= pin_ratio * body)
& (lower_wick <= 0.5 * body)
)
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:
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"]
return dataframe
# ================================================================
# 主时间框架 — 1H K线形态 + Entry Candle 记录
# ================================================================
# 类级别缓存:记录每笔交易的 Entry Candle 信息
# {trade_id: {"entry_low": float, "entry_high": float, "entry_idx": int}}
_entry_candle_cache = {}
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""
1H 一小时线:检测 K 线形态。
同时预标记可能的入场 K 线(供 custom_stoploss 使用)。
"""
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
# 预标记:如果这根 K 线是入场信号,记录其 OHLC供后续 custom_stoploss 使用)
# 注意:这里只是标记,实际入场由 populate_entry_trend 决定
dataframe["potential_entry_low"] = np.where(
dataframe["bullish_signal"] | dataframe["bearish_signal"],
dataframe["low"],
np.nan,
)
dataframe["potential_entry_high"] = np.where(
dataframe["bullish_signal"] | dataframe["bearish_signal"],
dataframe["high"],
np.nan,
)
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).infer_objects(copy=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:
"""
出场逻辑 — 由结构反转触发。
"""
# 做多出场D1 不再上升
exit_long = (
~dataframe["trend_up_1d"].fillna(True)
)
dataframe.loc[exit_long, "exit_long"] = 1
# 做空出场D1 不再下降
exit_short = (
dataframe["trend_up_1d"].fillna(False)
)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# ================================================================
# 动态止损 — v1.2 核心改进
# ================================================================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float | None:
"""
v1.2 止损逻辑(核心改进):
阶段一(刚入场,无盈利或微盈利):
止损 = Entry Candle 失效点 + 缓冲
- 做多入场K线最低价 × (1 - entry_sl_buffer)
- 做空入场K线最高价 × (1 + entry_sl_buffer)
阶段二(有一定盈利,超过 profit_to_structure_sl_pct
切换为结构跟踪止损(同 v1.1 逻辑)
- 做多:最近 4H Swing Low × (1 - buffer)
- 做空:最近 4H Swing High × (1 + buffer)
时间止损:
入场后超过 time_stop_bars 根K线且 current_profit < 0
返回 -0.01(立即市价出场)。
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return None
last = dataframe.iloc[-1]
buffer = self.entry_sl_buffer.value
# ── 时间止损检查 ──
# 计算入场至今的K线数1H = 1根/小时)
bars_held = (current_time - trade.open_date_utc).total_seconds() / 3600
if bars_held >= self.time_stop_bars.value and current_profit <= 0:
# 超时且无盈利,立即出场(返回当前价,即市价出场)
return -0.01 # 1% 内市价出场
# ── 尝试获取 Entry Candle 信息 ──
# 方法:在 dataframe 中找到 open_date_utc 附近的 K 线
entry_candle_low = None
entry_candle_high = None
# 通过 potential_entry_low/high 列找到入场信号 K 线
# 找到最先出现信号且在 open_date_utc 之前的 K 线
entry_mask = (
(dataframe["potential_entry_low"].notna())
| (dataframe["potential_entry_high"].notna())
)
entry_candidates = dataframe[
entry_mask
& (dataframe["date"] <= trade.open_date_utc + timedelta(hours=1))
& (dataframe["date"] >= trade.open_date_utc - timedelta(hours=1))
]
if len(entry_candidates) > 0:
entry_candle = entry_candidates.iloc[-1]
entry_candle_low = entry_candle.get("potential_entry_low")
entry_candle_high = entry_candle.get("potential_entry_high")
# ── 阶段一:用 Entry Candle 止损 ──
if entry_candle_low is not None or entry_candle_high is not None:
if trade.is_short:
if entry_candle_high is not None and not np.isnan(entry_candle_high):
sl_price = float(entry_candle_high) * (1 + buffer)
sl_ratio = (sl_price - current_rate) / current_rate
# 如果已经有盈利超过阈值,切换到结构止损
if current_profit > self.profit_to_structure_sl_pct.value:
pass # 继续到阶段二
else:
return max(sl_ratio, -0.25)
else:
if entry_candle_low is not None and not np.isnan(entry_candle_low):
sl_price = float(entry_candle_low) * (1 - buffer)
sl_ratio = (sl_price - current_rate) / current_rate
if current_profit > self.profit_to_structure_sl_pct.value:
pass # 继续到阶段二
else:
return max(sl_ratio, -0.25)
# ── 阶段二:结构跟踪止损(盈利足够后) ──
profit_trigger = self.profit_to_structure_sl_pct.value
if current_profit > profit_trigger:
if trade.is_short:
resistance = last.get("resistance_4h")
if resistance is not None and not (isinstance(resistance, float) and np.isnan(resistance)):
sl_price = float(resistance) * (1 + buffer)
sl_ratio = (sl_price - current_rate) / current_rate
if sl_ratio < 0:
return max(sl_ratio, -0.25)
else:
support = last.get("support_4h")
if support is not None and not (isinstance(support, float) and np.isnan(support)):
sl_price = float(support) * (1 - buffer)
sl_ratio = (sl_price - current_rate) / current_rate
if sl_ratio < 0:
return max(sl_ratio, -0.25)
return None
# ================================================================
# 时间止损的替代实现(通过 populate_exit_trend 扩展)
# ================================================================
def confirm_trade_exit(
self,
pair: str,
trade: Trade,
order_type: str,
amount: float,
rate: float,
time_in_force: str,
sell_reason: str,
**kwargs,
) -> bool:
"""
可在此处添加日志记录,便于回测分析。
"""
return True