Files
beast-trader-strategies/strategy.py

543 lines
20 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.0
# 纯价格结构策略 — 零技术指标,价格行为学驱动
#
# 设计哲学:
# 趋势不由 EMA 定义,而由 HH/HLHigher High / Higher Low定义
# 支撑阻力不由百分比定义,而由历史 Swing Point 定义
# 止损不由 ATR 定义,而由结构失效点定义
# 出场不由固定盈亏比定义,而由结构反转定义
#
# 多时间框架:
# D1 → 宏观结构方向
# 1H → 中期结构位 + 入场区域判定
# 5M → K线形态确认入场时机
# ============================================================================
from datetime import datetime
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 StructureFlowStrategy(IStrategy):
"""
Structure Flow Strategy v1.0 — 纯价格结构策略
不使用任何技术指标(无 EMA、ATR、RSI、MACD、布林带等
一切信号来源于价格本身的 OHLC 数据和由此推导的结构信息。
趋势判断:
HH + HL → 上升趋势Bullish Structure
LH + LL → 下降趋势Bearish Structure
其他 → 震荡Chop / Range
入场逻辑:
做多: D1上升结构 + 价格在1H Swing区间的下半区 + 5M看涨K线形态
做空: D1下降结构 + 价格在1H Swing区间的上半区 + 5M看跌K线形态
结构位入场区间:
不使用固定百分比。入场区域由最近 Swing High 和 Swing Low
的中点定义——价格在下半区为做多区域,在上半区为做空区域。
止损逻辑:
初始止损: 1H 最近 Swing Low做多/ Swing High做空
跟踪止损: 随新 Swing Point 形成而上移(做多)或下移(做空)
这是"结构失效止损"——如果止损被触发,意味着结构被破坏,
交易逻辑不再成立。
出场逻辑:
D1 结构反转(上升→非上升 或 下降→非下降)
或 1H 结构失效(做多时 Swing Low 被跌破)
"""
# ── 基础配置 ──────────────────────────────────────────
timeframe = "5m"
can_short = False # spot 回测临时关闭,实盘 futures 改回 True
stoploss = -0.25 # 硬止损安全网25%),实际由 custom_stoploss 动态管理
use_custom_stoploss = True
minimal_roi = {"0": 100} # 不设时间止盈,出场由结构决定
max_open_trades = 1
# 回测参数
startup_candle_count = 20 # 需要足够的历史数据来建立 Swing Point
# ── 可调参数 ──────────────────────────────────────────
# 这些参数是策略唯一的"旋钮",且都有结构含义
# Swing Point 检测窗口(寻找局部极值需要左右各 N 根K线确认
swing_lookback_d1 = IntParameter(
2, 10, default=5, space="buy",
)
swing_lookback_h1 = IntParameter(
2, 10, default=5, space="buy",
)
# Pin Bar 确认强度:影线至少是实体的 N 倍
pin_bar_wick_ratio = DecimalParameter(
1.5, 4.0, default=2.0, space="buy",
)
# ================================================================
# 工具函数 — 纯价格计算,不依赖任何技术指标
# ================================================================
@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 构建市场结构信息。
对每一个 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
"""
# ── NaN 安全处理 ──
# 多时间框架合并后,前部可能有 NaN
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_1h", "trend_down_1h",
"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)
# ── 做多 ──
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_exit_trend(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""
出场逻辑 — 由结构反转触发。
做多出场:
D1 不再处于上升结构 → 宏观环境改变
或 1H 不再处于上升结构 → 中期结构失效
做空出场:
D1 不再处于下降结构 → 宏观环境改变
或 1H 不再处于下降结构 → 中期结构失效
"""
# 做多出场
exit_long = (
~dataframe["trend_up_1d"].fillna(True) # D1 结构反转NaN = 初始区,不出场)
)
dataframe.loc[exit_long, "exit_long"] = 1
# 做空出场
if self.can_short:
exit_short = (
dataframe["trend_up_1d"].fillna(False) # D1 转为上升
)
dataframe.loc[exit_short, "exit_short"] = 1
return dataframe
# ================================================================
# 动态止损 — 基于结构失效
# ================================================================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float | None:
"""
结构止损:止损位设在最近的 1H Swing Low做多或 Swing High做空
如果价格突破这个结构位,说明结构失效,交易逻辑不再成立。
这与传统的百分比止损或 ATR 止损不同——它不是"跌了N%就走"
而是"结构破了就走"
随着行情发展,新的 Swing Point 形成,止损自动跟随,
实现自然的移动止损——不依赖任何参数。
"""
# 获取已分析的 5M 数据(包含合并后的 1H 信息)
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return None # 使用默认 stoploss
last = dataframe.iloc[-1]
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
# 无法获取有效的结构位,使用默认硬止损
return None