Initial commit: 首次建仓,建立目录结构

This commit is contained in:
FXY
2026-06-11 23:49:54 +08:00
commit 4038a476b5
9396 changed files with 2372905 additions and 0 deletions

View File

@ -0,0 +1,428 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
import numpy as np
import pandas as pd
from datetime import datetime, timedelta, timezone
from pandas import DataFrame
from typing import Optional, Union
from freqtrade.strategy import (
IStrategy,
Trade,
Order,
PairLocks,
informative, # @informative decorator
# Hyperopt Parameters
BooleanParameter,
CategoricalParameter,
DecimalParameter,
IntParameter,
RealParameter,
# timeframe helpers
timeframe_to_minutes,
timeframe_to_next_date,
timeframe_to_prev_date,
# Strategy helper functions
merge_informative_pair,
stoploss_from_absolute,
stoploss_from_open,
)
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
from technical import qtpylib
# This class is a sample. Feel free to customize it.
class SampleStrategy(IStrategy):
"""
This is a sample strategy to inspire you.
More information in https://www.freqtrade.io/en/stable/strategy-customization/
You can:
:return: a Dataframe with all mandatory indicators for the strategies
- Rename the class name (Do not forget to update class_name)
- Add any methods you want to build your strategy
- Add any lib you need to build your strategy
You must keep:
- the lib in the section "Do not remove these libs"
- the methods: populate_indicators, populate_entry_trend, populate_exit_trend
You should keep:
- timeframe, minimal_roi, stoploss, trailing_*
"""
# Strategy interface version - allow new iterations of the strategy interface.
# Check the documentation or the Sample strategy to get the latest version.
INTERFACE_VERSION = 3
# Can this strategy go short?
can_short: bool = False
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi".
minimal_roi = {
# "120": 0.0, # exit after 120 minutes at break even
"60": 0.01,
"30": 0.02,
"0": 0.04,
}
# Optimal stoploss designed for the strategy.
# This attribute will be overridden if the config file contains "stoploss".
stoploss = -0.10
# Trailing stoploss
trailing_stop = False
# trailing_only_offset_is_reached = False
# trailing_stop_positive = 0.01
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
# Optimal timeframe for the strategy.
timeframe = "5m"
# Run "populate_indicators()" only for new candle.
process_only_new_candles = True
# These values can be overridden in the config.
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# Hyperoptable parameters
buy_rsi = IntParameter(low=1, high=50, default=30, space="buy", optimize=True, load=True)
sell_rsi = IntParameter(low=50, high=100, default=70, space="sell", optimize=True, load=True)
short_rsi = IntParameter(low=51, high=100, default=70, space="sell", optimize=True, load=True)
exit_short_rsi = IntParameter(
low=1, high=50, default=30, space="exit", optimize=True, load=True
)
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 200
# Optional order type mapping.
order_types = {
"entry": "limit",
"exit": "limit",
"stoploss": "market",
"stoploss_on_exchange": False,
}
# Optional order time in force.
order_time_in_force = {"entry": "GTC", "exit": "GTC"}
plot_config = {
"main_plot": {
"tema": {},
"sar": {"color": "white"},
},
"subplots": {
"MACD": {
"macd": {"color": "blue"},
"macdsignal": {"color": "orange"},
},
"RSI": {
"rsi": {"color": "red"},
},
},
}
def informative_pairs(self):
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
These pair/interval combinations are non-tradeable, unless they are part
of the whitelist as well.
For more information, please consult the documentation
:return: List of tuples in the format (pair, interval)
Sample: return [("ETH/USDT", "5m"),
("BTC/USDT", "15m"),
]
"""
return []
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
:param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
# Momentum Indicators
# ------------------------------------
# ADX
dataframe["adx"] = ta.ADX(dataframe)
# # Plus Directional Indicator / Movement
# dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
# dataframe['plus_di'] = ta.PLUS_DI(dataframe)
# # Minus Directional Indicator / Movement
# dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
# dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# # Aroon, Aroon Oscillator
# aroon = ta.AROON(dataframe)
# dataframe['aroonup'] = aroon['aroonup']
# dataframe['aroondown'] = aroon['aroondown']
# dataframe['aroonosc'] = ta.AROONOSC(dataframe)
# # Awesome Oscillator
# dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
# # Keltner Channel
# keltner = qtpylib.keltner_channel(dataframe)
# dataframe["kc_upperband"] = keltner["upper"]
# dataframe["kc_lowerband"] = keltner["lower"]
# dataframe["kc_middleband"] = keltner["mid"]
# dataframe["kc_percent"] = (
# (dataframe["close"] - dataframe["kc_lowerband"]) /
# (dataframe["kc_upperband"] - dataframe["kc_lowerband"])
# )
# dataframe["kc_width"] = (
# (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"]
# )
# # Ultimate Oscillator
# dataframe['uo'] = ta.ULTOSC(dataframe)
# # Commodity Channel Index: values [Oversold:-100, Overbought:100]
# dataframe['cci'] = ta.CCI(dataframe)
# RSI
dataframe["rsi"] = ta.RSI(dataframe)
# # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy)
# rsi = 0.1 * (dataframe['rsi'] - 50)
# dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1)
# # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy)
# dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# # Stochastic Slow
# stoch = ta.STOCH(dataframe)
# dataframe['slowd'] = stoch['slowd']
# dataframe['slowk'] = stoch['slowk']
# Stochastic Fast
stoch_fast = ta.STOCHF(dataframe)
dataframe["fastd"] = stoch_fast["fastd"]
dataframe["fastk"] = stoch_fast["fastk"]
# # Stochastic RSI
# Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this.
# STOCHRSI is NOT aligned with tradingview, which may result in non-expected results.
# stoch_rsi = ta.STOCHRSI(dataframe)
# dataframe['fastd_rsi'] = stoch_rsi['fastd']
# dataframe['fastk_rsi'] = stoch_rsi['fastk']
# MACD
macd = ta.MACD(dataframe)
dataframe["macd"] = macd["macd"]
dataframe["macdsignal"] = macd["macdsignal"]
dataframe["macdhist"] = macd["macdhist"]
# MFI
dataframe["mfi"] = ta.MFI(dataframe)
# # ROC
# dataframe['roc'] = ta.ROC(dataframe)
# Overlap Studies
# ------------------------------------
# Bollinger Bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe["bb_lowerband"] = bollinger["lower"]
dataframe["bb_middleband"] = bollinger["mid"]
dataframe["bb_upperband"] = bollinger["upper"]
dataframe["bb_percent"] = (dataframe["close"] - dataframe["bb_lowerband"]) / (
dataframe["bb_upperband"] - dataframe["bb_lowerband"]
)
dataframe["bb_width"] = (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe[
"bb_middleband"
]
# Bollinger Bands - Weighted (EMA based instead of SMA)
# weighted_bollinger = qtpylib.weighted_bollinger_bands(
# qtpylib.typical_price(dataframe), window=20, stds=2
# )
# dataframe["wbb_upperband"] = weighted_bollinger["upper"]
# dataframe["wbb_lowerband"] = weighted_bollinger["lower"]
# dataframe["wbb_middleband"] = weighted_bollinger["mid"]
# dataframe["wbb_percent"] = (
# (dataframe["close"] - dataframe["wbb_lowerband"]) /
# (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"])
# )
# dataframe["wbb_width"] = (
# (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) /
# dataframe["wbb_middleband"]
# )
# # EMA - Exponential Moving Average
# dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
# dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
# dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
# dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21)
# dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
# dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# # SMA - Simple Moving Average
# dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3)
# dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5)
# dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10)
# dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21)
# dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50)
# dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100)
# Parabolic SAR
dataframe["sar"] = ta.SAR(dataframe)
# TEMA - Triple Exponential Moving Average
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe["htsine"] = hilbert["sine"]
dataframe["htleadsine"] = hilbert["leadsine"]
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
# # Hammer: values [0, 100]
# dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# # Inverted Hammer: values [0, 100]
# dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# # Dragonfly Doji: values [0, 100]
# dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# # Piercing Line: values [0, 100]
# dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# # Morningstar: values [0, 100]
# dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# # Three White Soldiers: values [0, 100]
# dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
# # Hanging Man: values [0, 100]
# dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# # Shooting Star: values [0, 100]
# dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# # Gravestone Doji: values [0, 100]
# dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# # Dark Cloud Cover: values [0, 100]
# dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# # Evening Doji Star: values [0, 100]
# dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# # Evening Star: values [0, 100]
# dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
# # Three Line Strike: values [0, -100, 100]
# dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# # Spinning Top: values [0, -100, 100]
# dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# # Engulfing: values [0, -100, 100]
# dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# # Harami: values [0, -100, 100]
# dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# # Three Outside Up/Down: values [0, -100, 100]
# dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# # Three Inside Up/Down: values [0, -100, 100]
# dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
# # Chart type
# # ------------------------------------
# # Heikin Ashi Strategy
# heikinashi = qtpylib.heikinashi(dataframe)
# dataframe['ha_open'] = heikinashi['open']
# dataframe['ha_close'] = heikinashi['close']
# dataframe['ha_high'] = heikinashi['high']
# dataframe['ha_low'] = heikinashi['low']
# Retrieve best bid and best ask from the orderbook
# ------------------------------------
"""
# first check if dataprovider is available
if self.dp:
if self.dp.runmode.value in ('live', 'dry_run'):
ob = self.dp.orderbook(metadata['pair'], 1)
dataframe['best_bid'] = ob['bids'][0][0]
dataframe['best_ask'] = ob['asks'][0][0]
"""
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the entry signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with entry columns populated
"""
dataframe.loc[
(
# Signal: RSI crosses above 30
(qtpylib.crossed_above(dataframe["rsi"], self.buy_rsi.value))
& (dataframe["tema"] <= dataframe["bb_middleband"]) # Guard: tema below BB middle
& (dataframe["tema"] > dataframe["tema"].shift(1)) # Guard: tema is raising
& (dataframe["volume"] > 0) # Make sure Volume is not 0
),
"enter_long",
] = 1
dataframe.loc[
(
# Signal: RSI crosses above 70
(qtpylib.crossed_above(dataframe["rsi"], self.short_rsi.value))
& (dataframe["tema"] > dataframe["bb_middleband"]) # Guard: tema above BB middle
& (dataframe["tema"] < dataframe["tema"].shift(1)) # Guard: tema is falling
& (dataframe["volume"] > 0) # Make sure Volume is not 0
),
"enter_short",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with exit columns populated
"""
dataframe.loc[
(
# Signal: RSI crosses above 70
(qtpylib.crossed_above(dataframe["rsi"], self.sell_rsi.value))
& (dataframe["tema"] > dataframe["bb_middleband"]) # Guard: tema above BB middle
& (dataframe["tema"] < dataframe["tema"].shift(1)) # Guard: tema is falling
& (dataframe["volume"] > 0) # Make sure Volume is not 0
),
"exit_long",
] = 1
dataframe.loc[
(
# Signal: RSI crosses above 30
(qtpylib.crossed_above(dataframe["rsi"], self.exit_short_rsi.value))
&
# Guard: tema below BB middle
(dataframe["tema"] <= dataframe["bb_middleband"])
& (dataframe["tema"] > dataframe["tema"].shift(1)) # Guard: tema is raising
& (dataframe["volume"] > 0) # Make sure Volume is not 0
),
"exit_short",
] = 1
return dataframe

View File

@ -0,0 +1,515 @@
# 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

View File

@ -0,0 +1,589 @@
"""
Structure Flow Scalp — 震荡市剥头皮策略
==========================================
基于Al Brooks价格行为学
- 在已识别的震荡区间内,支撑位做多、阻力位做空
- 15m级别支撑/阻力决定交易区间5m级别入场
- 100x全仓杠杆每次10%仓位
- 区间高度40%止盈15m支撑/阻力外侧0.3%止损
变更记录:
v1 (2026-06-10): 初版基于v2.2b核心逻辑重构
v1.1 (2026-06-10): 支撑阻力从4H改为15m
v1.2 (2026-06-10): 去掉4H趋势强度判断冗余启用100x全仓杠杆10%仓位
v1.3 (2026-06-10): 代码审查修复——移除populate_exit_trend死循环NaN安全杠杆上限
v1.4 (2026-06-10): EMA动态S/R + 入场锁定S/R——止损止盈使用入场时的锁定值不追最新
v1.5 (2026-06-10): 扩展入场信号 + 追踪止损保护 + 延长活S/R窗口
v1.6 (2026-06-10): 止损改为ATR动态计算——绑入场价不绑支撑位追踪改为ATR×0.5自适应
"""
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 StructureFlowScalp(IStrategy):
"""
震荡市剥头皮策略 — 5m框架100x全仓杠杆。
去掉4H趋势强度判断——15m支撑阻力本身就是最好的过滤器。
"""
can_short = True
stoploss = -0.15
use_custom_stoploss = True
use_custom_exit = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "5m"
# =====================
# 杠杆设置 - 全仓 100x
# =====================
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""返回固定 100x 杠杆,不超过交易所允许的最大值"""
return min(100.0, max_leverage)
# =====================
# 工具查找入场K线锁定S/R用
# =====================
def _get_entry_row(self, dataframe: DataFrame, trade: Trade) -> pd.Series | None:
"""
从 dataframe 中找到入场 trade 对应的 K 线行。
兼容 live/dry_runDatetimeIndex和 backtestingRangeIndex + date 列)两种模式。
"""
if 'date' in dataframe.columns:
# Backtesting 模式dataframe 有 date 列index 是 int
entry_mask = pd.to_datetime(dataframe['date']) <= trade.open_date
if not entry_mask.any():
return None
return dataframe[entry_mask].iloc[-1]
else:
# Live/Dry-run 模式index 是 DatetimeIndex
try:
entry_idx = dataframe.index.get_indexer([trade.open_date], method="pad")
if entry_idx[0] < 0 or entry_idx[0] >= len(dataframe):
return None
return dataframe.iloc[entry_idx[0]]
except (TypeError, ValueError):
return None
# =====================
# 可优化参数
# =====================
# 15m支撑阻力计算窗口
swing_lookback_15m = IntParameter(5, 15, default=10, space="buy")
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
cooldown_bars = IntParameter(2, 8, default=3, space="buy")
# 区间高度止盈比例(%
profit_zone_pct = IntParameter(20, 60, default=40, space="buy")
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
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:
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)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif 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:
# EMA平滑不取最后一个而是对最近swing lows做指数加权
# alpha=0.3每个新swing point向它移动30%,有"惯性"不跳变
ema_s = sl_prices[0]
for p in sl_prices[1:]:
ema_s = 0.3 * p + 0.7 * ema_s
nearest_support[i] = ema_s
if sh_prices:
ema_r = sh_prices[0]
for p in sh_prices[1:]:
ema_r = 0.3 * p + 0.7 * ema_r
nearest_resistance[i] = ema_r
return DataFrame({
"trend_up": trend_up_arr,
"trend_down": trend_down_arr,
"support": nearest_support,
"resistance": nearest_resistance,
}, 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]:
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
# ================================================================
# 信息时间框架 — 15m 短期支撑阻力(核心过滤器)
# ================================================================
@informative("15m")
def populate_indicators_15m(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_15m.value,
)
structure = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh, sl,
)
dataframe["support"] = structure["support"]
dataframe["resistance"] = structure["resistance"]
# ── 活支撑检查15根15m ≈ 3.75小时,震荡市中支撑可长期有效)──
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(15, min_periods=1).max() > 0
# ── 活阻力检查15根窗口──
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(15, min_periods=1).max() > 0
# 区间高度(用于止盈计算)
dataframe["zone_height"] = (dataframe["resistance"] - dataframe["support"]).fillna(0)
return dataframe
# ================================================================
# 主时间框架 — 5m 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""5m级别ATR + K线形态 + 信号整合。"""
# ── 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()
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
# ── 扩展信号长下影线比pinbar更宽松只要下影线>总范围50% ──
total_range = (dataframe["high"] - dataframe["low"]).replace(0, 0.0001)
body = (dataframe["close"] - dataframe["open"]).abs()
# 下影线 = min(open, close) - low
lower_wick = (
dataframe[["open", "close"]].min(axis=1) - dataframe["low"]
)
# 上影线 = high - max(open, close)
upper_wick = (
dataframe["high"] - dataframe[["open", "close"]].max(axis=1)
)
# 长下影线:下影线>总范围50% 且 下影线>上影线
long_lower_wick = (
(lower_wick / total_range > 0.5) &
(lower_wick > upper_wick)
)
dataframe["long_lower_wick"] = long_lower_wick
# ── 扩展信号:支撑位附近的强力反弹阳线 ──
# 条件价格在支撑0.5%范围内 + 阳线 + 实体>0.2%
if "support_15m" in dataframe.columns:
near_support = (
(dataframe["low"] <= dataframe["support_15m"] * 1.005) &
(dataframe["low"] >= dataframe["support_15m"] * 0.995)
)
is_bullish = dataframe["close"] > dataframe["open"]
body_pct = body / dataframe["open"]
strong_recovery = near_support & is_bullish & (body_pct > 0.002)
else:
strong_recovery = pd.Series(False, index=dataframe.index)
dataframe["strong_recovery"] = strong_recovery
# ── 综合止跌/止涨信号(扩展后) ──
dataframe["bullish_signal"] = (
bullish_pin | bullish_engulf | long_lower_wick | strong_recovery
)
dataframe["bearish_signal"] = (
bearish_pin | bearish_engulf
)
# 做空对称:阻力位附近的强力下跌阴线
if "resistance_15m" in dataframe.columns:
near_resistance = (
(dataframe["high"] >= dataframe["resistance_15m"] * 0.995) &
(dataframe["high"] <= dataframe["resistance_15m"] * 1.005)
)
is_bearish = dataframe["close"] < dataframe["open"]
body_pct = body / dataframe["open"]
strong_rejection = near_resistance & is_bearish & (body_pct > 0.002)
else:
strong_rejection = pd.Series(False, index=dataframe.index)
dataframe["strong_rejection"] = strong_rejection
dataframe["bearish_signal"] = (
bearish_pin | bearish_engulf | strong_rejection
)
# NaN 安全处理
bool_cols = [
"support_alive_15m", "resistance_alive_15m",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ATR fillna前14根无ATR值用均值填补
if "atr" in dataframe.columns:
atr_mean = dataframe["atr"].mean()
dataframe["atr"] = dataframe["atr"].fillna(atr_mean)
return dataframe
# =====================
# 入场信号
# =====================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场逻辑5m 时间框架)。
不做4H趋势判断——15m支撑阻力本身就是过滤器
- 趋势强时价格直接突破15m S/R不会在支撑/阻力附近停留
- 在支撑/阻力附近停留 = 震荡市
入场条件3个去掉了冗余的4H趋势判断
- 做多价格贴近15m支撑 + 支撑有效 + K线止跌信号
- 做空价格贴近15m阻力 + 阻力有效 + K线止涨信号
出场只依赖 custom_stoploss 和 custom_exit不需要 D1 结构反转退出。
(去掉 populate_exit_trend震荡市入场 → D1 非上升趋势 → 立即出场 的死循环)
"""
cooldown = self.cooldown_bars.value
# NaN 安全处理 — 如果 15m informative 列还没对齐,直接跳过本根 K 线
required_cols = ["support_15m", "resistance_15m",
"support_alive_15m", "resistance_alive_15m"]
for col in required_cols:
if col not in dataframe.columns:
return dataframe # 数据尚未就绪,跳过
for col in ["bullish_signal", "bearish_signal",
"support_alive_15m", "resistance_alive_15m"]:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
# 条件价格贴近15m支撑0.5%范围内)- 使用 low 而非 open
# 因为支撑测试看的是价格是否到达支撑位,不是开盘在哪
near_support = (
(dataframe["low"] <= dataframe["support_15m"] * 1.005) &
(dataframe["low"] >= dataframe["support_15m"] * 0.995)
)
long_conditions = (
near_support
& dataframe["support_alive_15m"]
& dataframe["bullish_signal"]
)
long_recent = long_conditions.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_conditions & long_recent, "enter_long"] = 1
# ── 做空 ──
# 条件价格贴近15m阻力0.5%范围内)- 使用 high 而非 open
near_resistance = (
(dataframe["high"] >= dataframe["resistance_15m"] * 0.995) &
(dataframe["high"] <= dataframe["resistance_15m"] * 1.005)
)
short_conditions = (
near_resistance
& dataframe["resistance_alive_15m"]
& dataframe["bearish_signal"]
)
short_recent = short_conditions.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_conditions & short_recent, "enter_short"] = 1
return dataframe
# =====================
# exit_trendfreqtrade 2025.11 要求必须实现,即使 use_custom_exit=True
# =====================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""退出逻辑完全由 custom_stoploss + custom_exit 管理。"""
return dataframe
# =====================
# 动态止损 — 入场价 - ATR×2.0(基于市场波动,非固定比例)
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损锚定入场价宽度根据市场波动ATR动态计算而非固定比例。
核心逻辑:
- 做多止损 = entry_price - ATR_5m × 2.0
- 做空止损 = entry_price + ATR_5m × 2.0
- ATR值从入场时的K线锁定持仓期间不漂移
为什么用ATR不用固定比例
- ATR自动适应市场波动大时止损放宽免误扫波动小时收紧控风险
- 固定比例是拍脑袋ATR是算出来的
追踪保护v1.6 ATR自适应版
- 利润达止盈目标50%:上移到保本(入场价)
- 利润达止盈目标80%启动ATR×0.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
# 查找入场时的 K 线,锁定当时的 ATR 值
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 值,用于全程止损/追踪计算(不追最新,防止漂移)
atr_value = entry_row.get("atr", np.nan)
if pd.isna(atr_value) or atr_value <= 0:
return -0.02 if not trade.is_short else 0.02
if not trade.is_short:
# 做多:止损 = 入场价 - ATR × 2.0
base_sl_price = trade.open_rate - (atr_value * 2.0)
base_sl = (base_sl_price / trade.open_rate) - 1.0
base_sl = max(base_sl, -0.15)
# 追踪保护:需要入场行计算止盈目标
support = entry_row.get("support_15m", np.nan)
resistance = entry_row.get("resistance_15m", np.nan)
if (not pd.isna(support) and not pd.isna(resistance)
and resistance > support and current_profit > 0):
zone_height = resistance - support
tp_target = (zone_height * self.profit_zone_pct.value / 100.0) / trade.open_rate
if current_profit >= tp_target * 0.8:
# 利润达止盈80%ATR自适应窄追踪
trail_price = current_rate - (atr_value * 0.5)
trail_ratio = (trail_price / trade.open_rate) - 1.0
return max(trail_ratio, base_sl)
elif current_profit >= tp_target * 0.5:
# 利润达止盈50%:保本
return max(0.0, base_sl)
return base_sl
else:
# 做空:止损 = 入场价 + ATR × 2.0
base_sl_price = trade.open_rate + (atr_value * 2.0)
base_sl = 1.0 - (base_sl_price / trade.open_rate)
base_sl = min(base_sl, 0.15)
# 追踪保护(做空对称)
support = entry_row.get("support_15m", np.nan)
resistance = entry_row.get("resistance_15m", np.nan)
if (not pd.isna(support) and not pd.isna(resistance)
and resistance > support and current_profit > 0):
zone_height = resistance - support
tp_target = (zone_height * self.profit_zone_pct.value / 100.0) / trade.open_rate
if current_profit >= tp_target * 0.8:
# ATR自适应窄追踪做空对称
trail_price = current_rate + (atr_value * 0.5)
trail_ratio = (trail_price / trade.open_rate) - 1.0
return min(trail_ratio, base_sl)
elif current_profit >= tp_target * 0.5:
# 保本
return min(0.0, base_sl)
return base_sl
# =====================
# 区间高度止盈
# =====================
def custom_exit(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
**kwargs,
) -> str | None:
"""
当利润达到入场时锁定的15m区间高度的设定比例时止盈。
使用入场时锁定的S/R值计算区间高度zone_height而非最新的值
- 入场后如果区间收缩,止盈目标不会跟着变小
- 让入场时确定的止盈逻辑"钉死"
- profit_zone_pct 默认40%即锁定区间高度的40%
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return None
# 查找入场时的 K 线,锁定当时的 S/R 值
entry_row = self._get_entry_row(dataframe, trade)
if entry_row is None:
return None
support = entry_row.get("support_15m", np.nan)
resistance = entry_row.get("resistance_15m", np.nan)
if pd.isna(support) or pd.isna(resistance) or resistance <= support:
return None
# 用锁定的区间高度计算止盈目标(不随市场漂移)
locked_zone_height = resistance - support
target_pct = (locked_zone_height * self.profit_zone_pct.value / 100.0) / trade.open_rate
if current_profit >= target_pct:
return "zone_tp"
return None
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_15m": {"color": "green", "type": "line"},
"resistance_15m": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
"bullish_signal": {"color": "lime", "type": "scatter"},
"bearish_signal": {"color": "orange", "type": "scatter"},
},
"filters": {
"support_alive_15m": {"color": "green", "type": "line"},
"resistance_alive_15m": {"color": "red", "type": "line"},
},
},
}

View File

@ -0,0 +1,456 @@
"""
Structure Flow Strategy v2.1
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.1 (2026-06-08): ===== D1: 趋势强度过滤 =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
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 StructureFlowStrategyV21(IStrategy):
"""
Structure Flow Strategy v2.1 — D1: 趋势强度过滤
v2.1改动相对于v1.6
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
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")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # -20=允许SP轻微收缩, 最佳值
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
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:
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 pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif 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]
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]:
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
# ================================================================
# 信息时间框架 — 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"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别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_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
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_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 时间框架)。
v2.1 核心改动D1 — 趋势强度过滤
做多额外条件4H上升趋势在扩张strong_uptrend_4h
做空额外条件4H下降趋势在扩张strong_downtrend_4h
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
& dataframe["bullish_signal"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive_4h"]
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
& dataframe["strong_uptrend_4h"]
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
& dataframe["bearish_signal"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive_4h"]
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
& dataframe["strong_downtrend_4h"]
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "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
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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 = dataframe.iloc[-1]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

View File

@ -0,0 +1,455 @@
"""
Structure Flow Strategy v2.2b
=======================
变更记录:
v1.6 (2026-06-07): 最优基线 — +3659.63%, 190笔, 69.3% trailing胜率
v2.0 (2026-06-08): B1 入场延迟确认 — 方向正确但降频严重
v2.2b (2026-06-09): ===== 只移除 bullish_signal/bearish_signal =====
在4H级别评估趋势强度最近2个Swing Point的间距变化。
如果趋势在扩张HH/HL间距增大允许入场
如果趋势在收缩HH/HL间距缩小或震荡过滤信号。
目的:只在趋势明确时交易,避免震荡市反复止损。
"""
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 StructureFlowStrategyV22b(IStrategy):
"""
Structure Flow Strategy v2.2b — D1: 趋势强度过滤
v2.2b改动相对于v2.1
在4H级别计算趋势强度最近2个Swing High间距 + Swing Low间距的变化。
只有趋势在扩张(或至少不收缩)时才允许入场。
"""
can_short = True
stoploss = -0.15
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")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
# v2.1 新增趋势强度最小扩张比例x/100 = 0%~50%
# 0 = 只要不收缩就行;越大要求趋势扩张越强
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy") # 扫描更宽范围
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
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:
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 pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif 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]
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]:
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
# ================================================================
# 信息时间框架 — 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"]
# ================================
# v1.6 活支撑/阻力检查(保留)
# ================================
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ================================
# v2.1 新增:趋势强度评估
# ================================
# 计算最近2个Swing Point之间的间距变化
# 上升趋势HH间距 + HL间距都在扩大 → 趋势强
# 下降趋势LH间距 + LL间距都在扩大 → 趋势强
# 间距缩小 → 趋势减弱/震荡
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
# 上升趋势强度HH[-1] vs HH[-2], HL[-1] vs HL[-2]
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
# HH间距最近两个Swing High的差值百分比
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
# HL间距最近两个Swing Low的差值百分比
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
# 上升趋势强度 = HH间距 + HL间距都正=扩张,一正一负=不确定,都负=收缩)
trend_strength_up[i] = hh_dist + hl_dist
# 下降趋势强度(取反:间距缩小是负值)
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
# 趋势强度是否足够(扩张中)
min_strength = self.trend_strength_min.value / 100.0 # 0~0.30
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""1H 级别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_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
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_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 时间框架)。
v2.2b 改动:只移除 bullish_signal/bearish_signal1H K线过滤
消融实验变体3移除后收益 +19.4%,是三个可移除条件中收益提升最大的
"""
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
# NaN 安全处理
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand_4h", "in_supply_4h",
"support_alive_4h", "resistance_alive_4h",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
long_stop_dist = (dataframe["open"] - dataframe["support_4h"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand_4h"]
# v2.2b: 已移除 bullish_signal消融变体3
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive_4h"]
# v2.1: 趋势强度 — 4H上升趋势必须在扩张
& dataframe["strong_uptrend_4h"]
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空 ──
short_stop_dist = (dataframe["resistance_4h"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply_4h"]
# v2.2b: 已移除 bearish_signal消融变体3
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive_4h"]
# v2.1: 趋势强度 — 4H下降趋势必须在扩张
& dataframe["strong_downtrend_4h"]
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "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
# =====================
# 动态止损 — 纯价格结构基于Swing Point
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
"""
止损逻辑完全基于价格结构零指标与v1.6相同)。
"""
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 = dataframe.iloc[-1]
if not trade.is_short:
support = last.get("support_4h", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance_4h", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support_4h": {"color": "green", "type": "line"},
"resistance_4h": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive_4h": {"color": "green", "type": "line"},
"resistance_alive_4h": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

View File

@ -0,0 +1,427 @@
"""
Structure Flow Strategy v2.2b — 1H S/R 实验版
==============================================
变更记录:
v2.2b (2026-06-09): 原版 — 4H级别S/R + 趋势强度
v2.2b-1h-sr (2026-06-10): 实验版 — 将S/R从4H改为1H级别趋势强度仍用4H
改动:
support_alive/resistance_alive 从4H级别 → 1H级别
support/resistance 引用 从4H → 1H
in_demand/in_supply 从4H → 1H
趋势强度strong_uptrend/downtrend保持在4H
"""
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 StructureFlowStrategyV22c(IStrategy):
can_short = True
stoploss = -0.15
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")
swing_lookback_1h = IntParameter(3, 7, default=5, space="buy") # 新增1H swing参数
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy")
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
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:
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 pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif 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]
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]:
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
# ================================================================
# 信息时间框架 — 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,
)
# 趋势强度计算(原版逻辑)
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
trend_strength_up[i] = hh_dist + hl_dist
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
min_strength = self.trend_strength_min.value / 100.0
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标(含 1H S/R + 活支撑/阻力)
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
# ── 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_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级别 Swing Point + 结构替代原4H S/R ──
sh_1h, sl_1h = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_1h.value,
)
structure_1h = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh_1h, sl_1h,
)
dataframe["trend_up_1h"] = structure_1h["trend_up"]
dataframe["trend_down_1h"] = structure_1h["trend_down"]
dataframe["support"] = structure_1h["support"]
dataframe["resistance"] = structure_1h["resistance"]
dataframe["in_demand"] = structure_1h["in_demand"]
dataframe["in_supply"] = structure_1h["in_supply"]
# ── 1H 活支撑/阻力检查 ──
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ── NaN 安全处理 ──
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand", "in_supply",
"support_alive", "resistance_alive",
"strong_uptrend_4h", "strong_downtrend_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:
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand", "in_supply",
"support_alive", "resistance_alive",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多使用1H S/R ──
long_stop_dist = (dataframe["open"] - dataframe["support"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive"]
& dataframe["strong_uptrend_4h"]
)
long_recent = long_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_base & long_recent, "enter_long"] = 1
# ── 做空使用1H S/R ──
short_stop_dist = (dataframe["resistance"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive"]
& dataframe["strong_downtrend_4h"]
)
short_recent = short_base.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_base & short_recent, "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
# =====================
# 动态止损基于1H S/R
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
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 = dataframe.iloc[-1]
if not trade.is_short:
support = last.get("support", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support": {"color": "green", "type": "line"},
"resistance": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive": {"color": "green", "type": "line"},
"resistance_alive": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

View File

@ -0,0 +1,451 @@
"""
Structure Flow Strategy v2.2c — 冷却期修复版
==============================================
变更记录:
v2.2c (2026-06-11): 1H S/R 替代 4H S/R
v2.2c-coolfix (2026-06-11): 修复冷却期无限阻止下单 bug
"""
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 StructureFlowStrategyV22d(IStrategy):
can_short = True
stoploss = -0.15
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")
swing_lookback_1h = IntParameter(3, 7, default=5, space="buy") # 新增1H swing参数
pin_bar_wick_ratio = IntParameter(50, 70, default=60, space="buy")
max_stop_dist = IntParameter(20, 50, default=50, space="buy")
cooldown_bars = IntParameter(3, 12, default=6, space="buy")
trend_strength_min = IntParameter(-50, 20, default=-20, space="buy")
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
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:
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 pd.notna(swing_high.iloc[i]):
sh_prices.append(swing_high.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(swing_low.iloc[i]):
sl_prices.append(swing_low.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
if sh_prices[-1] > sh_prices[-2] and sl_prices[-1] > sl_prices[-2]:
trend_up_arr[i] = True
elif sh_prices[-1] < sh_prices[-2] and sl_prices[-1] < sl_prices[-2]:
trend_down_arr[i] = True
elif 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]
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]:
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
# =====================
# 工具:冷却期正确实现(修复 bug
# =====================
def _apply_cooldown(self, signal: pd.Series, cooldown_bars: int) -> pd.Series:
"""
正确应用冷却期:入场后才冷却,而非条件满足就冷却。
原逻辑 buglong_base.rolling(cooldown).max().shift(1) == 0
- 当市场持续满足入场条件时rolling window 里永远有 True
- 导致冷却期无限阻止下单
修复逻辑:遍历 K 线,模拟"入场 -> 冷却"过程。
- 满足条件 + 距离上次入场 > cooldown -> 允许入场
- 入场后 cooldown 根 K 线内不再入场
"""
n = len(signal)
result = [False] * n
last_entry = -99999 # 上次入场的 bar 索引
# 遍历(对 numpy array 操作O(n) 约几毫秒)
values = signal.values # numpy array快速访问
for i in range(n):
if values[i] and (i - last_entry) > cooldown_bars:
result[i] = True
last_entry = i
return pd.Series(result, index=signal.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,
)
# 趋势强度计算(原版逻辑)
sh_prices = []
sl_prices = []
trend_strength_up = np.full(len(dataframe), np.nan)
trend_strength_down = np.full(len(dataframe), np.nan)
for i in range(len(dataframe)):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 4:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 4:
sl_prices.pop(0)
if len(sh_prices) >= 2 and len(sl_prices) >= 2:
hh_dist = (sh_prices[-1] - sh_prices[-2]) / sh_prices[-2] if sh_prices[-2] > 0 else 0
hl_dist = (sl_prices[-1] - sl_prices[-2]) / sl_prices[-2] if sl_prices[-2] > 0 else 0
trend_strength_up[i] = hh_dist + hl_dist
trend_strength_down[i] = -(hh_dist + hl_dist)
dataframe["trend_strength_up"] = pd.Series(trend_strength_up, index=dataframe.index)
dataframe["trend_strength_down"] = pd.Series(trend_strength_down, index=dataframe.index)
min_strength = self.trend_strength_min.value / 100.0
dataframe["strong_uptrend"] = dataframe["trend_strength_up"] > min_strength
dataframe["strong_downtrend"] = dataframe["trend_strength_down"] > min_strength
return dataframe
# ================================================================
# 主时间框架 — 1H 指标(含 1H S/R + 活支撑/阻力)
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
# ── 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_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级别 Swing Point + 结构替代原4H S/R ──
sh_1h, sl_1h = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback_1h.value,
)
structure_1h = self._build_structure(
dataframe["high"], dataframe["low"], dataframe["close"],
sh_1h, sl_1h,
)
dataframe["trend_up_1h"] = structure_1h["trend_up"]
dataframe["trend_down_1h"] = structure_1h["trend_down"]
dataframe["support"] = structure_1h["support"]
dataframe["resistance"] = structure_1h["resistance"]
dataframe["in_demand"] = structure_1h["in_demand"]
dataframe["in_supply"] = structure_1h["in_supply"]
# ── 1H 活支撑/阻力检查 ──
touched_support = (
(dataframe["low"] <= dataframe["support"] * 1.005) &
(dataframe["low"] >= dataframe["support"] * 0.995)
)
held_support = dataframe["close"] > dataframe["support"]
support_tested_and_held = touched_support & held_support
dataframe["support_alive"] = support_tested_and_held.rolling(3, min_periods=1).max() > 0
touched_resistance = (
(dataframe["high"] >= dataframe["resistance"] * 0.995) &
(dataframe["high"] <= dataframe["resistance"] * 1.005)
)
held_resistance = dataframe["close"] < dataframe["resistance"]
resistance_tested_and_held = touched_resistance & held_resistance
dataframe["resistance_alive"] = resistance_tested_and_held.rolling(3, min_periods=1).max() > 0
# ── NaN 安全处理 ──
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand", "in_supply",
"support_alive", "resistance_alive",
"strong_uptrend_4h", "strong_downtrend_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:
max_dist = self.max_stop_dist.value / 100.0
cooldown = self.cooldown_bars.value
bool_cols = [
"trend_up_1d", "trend_down_1d",
"trend_up_4h", "trend_down_4h",
"in_demand", "in_supply",
"support_alive", "resistance_alive",
"strong_uptrend_4h", "strong_downtrend_4h",
"bullish_signal", "bearish_signal",
]
for col in bool_cols:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多使用1H S/R ──
long_stop_dist = (dataframe["open"] - dataframe["support"]) / dataframe["open"]
long_base = (
dataframe["trend_up_1d"]
& dataframe["in_demand"]
& (long_stop_dist <= max_dist)
& (long_stop_dist > 0.003)
& dataframe["support_alive"]
& dataframe["strong_uptrend_4h"]
)
# ✅ 修复:正确应用冷却期(基于实际入场,而非条件满足)
long_entries = self._apply_cooldown(long_base, cooldown)
dataframe.loc[long_entries, "enter_long"] = 1
# ── 做空使用1H S/R ──
short_stop_dist = (dataframe["resistance"] - dataframe["open"]) / dataframe["open"]
short_base = (
dataframe["trend_down_1d"]
& dataframe["in_supply"]
& (short_stop_dist <= max_dist)
& (short_stop_dist > 0.003)
& dataframe["resistance_alive"]
& dataframe["strong_downtrend_4h"]
)
# ✅ 修复:正确应用冷却期(基于实际入场,而非条件满足)
short_entries = self._apply_cooldown(short_base, cooldown)
dataframe.loc[short_entries, "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
# =====================
# 动态止损基于1H S/R
# =====================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
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 = dataframe.iloc[-1]
if not trade.is_short:
support = last.get("support", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
sl_price = support * 0.999
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.15)
else:
resistance = last.get("resistance", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
sl_price = resistance * 1.001
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.15)
# =====================
# Plot config
# =====================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"support": {"color": "green", "type": "line"},
"resistance": {"color": "red", "type": "line"},
},
"subplots": {
"signals": {
"bullish_pinbar": {"color": "green", "type": "scatter"},
"bearish_pinbar": {"color": "red", "type": "scatter"},
},
"filters": {
"support_alive": {"color": "green", "type": "line"},
"resistance_alive": {"color": "red", "type": "line"},
"strong_uptrend_4h": {"color": "blue", "type": "line"},
"strong_downtrend_4h": {"color": "orange", "type": "line"},
},
},
}

View File

@ -0,0 +1,425 @@
"""
Structure Flow Swing Strategy v3.0
==================================
波段交易策略 — 基于4H震荡区间保守参数
核心思路(冯总指示):
1. 在4H级别识别震荡区间
2. 只在确认震荡时交易(区间宽度稳定、价格测试过边界、无突破)
3. 止损设在支撑/阻力外侧,确保几乎不被噪音触发
4. 止损被触发 = 结构已坏,离场正确
5. 止盈区间高度的70%
保守参数:
- 杠杆1x无杠杆
- 止损安全边际ATR(4H, 14) * 1.5
- 区间宽度稳定阈值15%
- 止盈区间70%
- 入场范围:支撑/阻力2%以内
版本历史:
v3.0 (2026-06-10): 初版,基于冯总波段交易新思路
"""
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 StructureFlowSwingV30(IStrategy):
"""
Structure Flow Swing Strategy v3.0
4H震荡区间波段交易
"""
can_short = True
stoploss = -0.20
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "4h"
# =====================
# 可优化参数(保守默认值)
# =====================
swing_lookback = IntParameter(4, 8, default=5, space="buy")
zone_stability_threshold = IntParameter(10, 25, default=15, space="buy")
entry_zone_pct = IntParameter(1, 3, default=2, space="buy")
atr_stop_mult = IntParameter(10, 25, default=15, space="buy") # /10, e.g. 15 = 1.5x
take_profit_pct = IntParameter(50, 80, default=70, space="sell")
# 固定参数
zone_touch_lookback = 10
breakout_bars = 2
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
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 _detect_range(
self,
sh: pd.Series,
sl: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
) -> DataFrame:
n = len(high)
is_ranging = np.full(n, False)
support_arr = np.full(n, np.nan)
resistance_arr = np.full(n, np.nan)
zone_width_arr = np.full(n, np.nan)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 5:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 5:
sl_prices.pop(0)
if len(sh_prices) < 3 or len(sl_prices) < 3:
continue
current_sh = sh_prices[-1]
current_sl = sl_prices[-1]
if current_sh <= current_sl:
continue
zone_width = (current_sh - current_sl) / current_sl
support_arr[i] = current_sl
resistance_arr[i] = current_sh
zone_width_arr[i] = zone_width
# 条件1区间宽度稳定性
widths = []
for j in range(min(len(sh_prices), len(sl_prices)) - 1, -1, -1):
w = (sh_prices[j] - sl_prices[j]) / sl_prices[j]
widths.append(w)
if len(widths) >= 3:
break
if len(widths) >= 3:
mean_width = np.mean(widths)
if mean_width > 0:
max_dev = max(abs(w - mean_width) / mean_width for w in widths)
stability_threshold = self.zone_stability_threshold.value / 100.0
is_stable = max_dev <= stability_threshold
else:
is_stable = False
else:
is_stable = False
if not is_stable:
continue
# 条件2价格测试过边界
start_idx = max(0, i - self.zone_touch_lookback)
support_zone_upper = current_sl * 1.01
touched_support = any(
low.iloc[j] <= support_zone_upper
for j in range(start_idx, i + 1)
)
resistance_zone_lower = current_sh * 0.99
touched_resistance = any(
high.iloc[j] >= resistance_zone_lower
for j in range(start_idx, i + 1)
)
if not (touched_support and touched_resistance):
continue
# 条件3无突破
consecutive_outside = 0
for j in range(i, max(0, i - self.breakout_bars) - 1, -1):
if close.iloc[j] > current_sh or close.iloc[j] < current_sl:
consecutive_outside += 1
else:
break
if consecutive_outside >= self.breakout_bars:
continue
is_ranging[i] = True
return DataFrame({
"is_ranging": is_ranging,
"support": support_arr,
"resistance": resistance_arr,
"zone_width": zone_width_arr,
}, index=high.index)
# =====================
# 工具ATR计算
# =====================
@staticmethod
def _calc_atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series:
tr = pd.DataFrame({
"hl": high - low,
"hc": (high - close.shift(1)).abs(),
"lc": (low - close.shift(1)).abs(),
}).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"], window=5
)
sh_vals = sh.dropna()
sl_vals = sl.dropna()
is_uptrend = pd.Series(False, index=dataframe.index)
is_downtrend = pd.Series(False, index=dataframe.index)
if len(sh_vals) >= 2 and len(sl_vals) >= 2:
if sh_vals.iloc[-1] > sh_vals.iloc[-2] and sl_vals.iloc[-1] > sl_vals.iloc[-2]:
is_uptrend[:] = True
elif sh_vals.iloc[-1] < sh_vals.iloc[-2] and sl_vals.iloc[-1] < sl_vals.iloc[-2]:
is_downtrend[:] = True
dataframe["d1_uptrend"] = is_uptrend
dataframe["d1_downtrend"] = is_downtrend
return dataframe
# ================================================================
# 主时间框架 — 4H 指标
# ================================================================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback.value,
)
range_info = self._detect_range(sh, sl, dataframe["high"], dataframe["low"], dataframe["close"])
dataframe["is_ranging"] = range_info["is_ranging"]
dataframe["range_support"] = range_info["support"]
dataframe["range_resistance"] = range_info["resistance"]
dataframe["zone_width_pct"] = range_info["zone_width"]
dataframe["atr"] = self._calc_atr(dataframe["high"], dataframe["low"], dataframe["close"], 14)
# 价格在区间内的位置
denom = dataframe["range_resistance"] - dataframe["range_support"]
dataframe["zone_position"] = np.where(
denom > 0,
(dataframe["close"] - dataframe["range_support"]) / denom,
np.nan,
)
# 距离边界百分比
dataframe["dist_to_support"] = np.where(
dataframe["range_support"] > 0,
(dataframe["close"] - dataframe["range_support"]) / dataframe["close"],
np.nan,
)
dataframe["dist_to_resistance"] = np.where(
dataframe["range_resistance"] > 0,
(dataframe["range_resistance"] - dataframe["close"]) / dataframe["close"],
np.nan,
)
for col in ["is_ranging", "zone_position", "dist_to_support", "dist_to_resistance"]:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False if col == "is_ranging" else 999)
return dataframe
# ================================================================
# 入场信号
# ================================================================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
entry_zone = self.entry_zone_pct.value / 100.0
# freqtrade adds _1d suffix to informative columns
d1_downtrend_col = "d1_downtrend_1d"
d1_uptrend_col = "d1_uptrend_1d"
for col in ["is_ranging", d1_uptrend_col, d1_downtrend_col]:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
else:
dataframe[col] = False
# ── 做多:震荡市中,价格靠近支撑位 ──
long_conds = (
dataframe["is_ranging"]
& (dataframe["dist_to_support"] <= entry_zone)
& (dataframe["dist_to_support"] > 0)
& (~dataframe[d1_downtrend_col])
)
cooldown = 3
long_recent = long_conds.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_conds & long_recent, "enter_long"] = 1
# ── 做空:震荡市中,价格靠近阻力位 ──
short_conds = (
dataframe["is_ranging"]
& (dataframe["dist_to_resistance"] <= entry_zone)
& (dataframe["dist_to_resistance"] > 0)
& (~dataframe[d1_uptrend_col])
)
short_recent = short_conds.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_conds & short_recent, "enter_short"] = 1
return dataframe
# ================================================================
# 出场信号
# ================================================================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe
# ================================================================
# 自定义止损:支撑/阻力外侧ATR*1.5 缓冲
# ================================================================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
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 = dataframe.iloc[-1]
atr_mult = self.atr_stop_mult.value / 10.0
if not trade.is_short:
support = last.get("range_support", np.nan)
atr = last.get("atr", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
if pd.notna(atr) and atr > 0:
sl_price = support - atr * atr_mult
else:
sl_price = support * 0.985
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.20)
else:
resistance = last.get("range_resistance", np.nan)
atr = last.get("atr", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
if pd.notna(atr) and atr > 0:
sl_price = resistance + atr * atr_mult
else:
sl_price = resistance * 1.015
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.20)
# ================================================================
# 自定义止盈区间70%
# ================================================================
def custom_exit(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
**kwargs,
) -> str | None:
tp_pct = self.take_profit_pct.value / 100.0
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return None
last = dataframe.iloc[-1]
if not trade.is_short:
support = last.get("range_support", np.nan)
resistance = last.get("range_resistance", np.nan)
if pd.notna(support) and pd.notna(resistance) and resistance > support:
zone_height = (resistance - support) / support
tp_target = zone_height * tp_pct
if current_profit >= tp_target:
return "take_profit"
else:
support = last.get("range_support", np.nan)
resistance = last.get("range_resistance", np.nan)
if pd.notna(support) and pd.notna(resistance) and resistance > support:
zone_height = (resistance - support) / resistance
tp_target = zone_height * tp_pct
if current_profit >= tp_target:
return "take_profit"
return None
# ================================================================
# Plot config
# ================================================================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"range_support": {"color": "green", "type": "line"},
"range_resistance": {"color": "red", "type": "line"},
},
"subplots": {
"range": {
"is_ranging": {"color": "blue", "type": "line"},
"zone_width_pct": {"color": "purple", "type": "line"},
},
"position": {
"dist_to_support": {"color": "green", "type": "line"},
"dist_to_resistance": {"color": "red", "type": "line"},
},
},
}

View File

@ -0,0 +1,423 @@
"""
Structure Flow Swing Strategy v3.1
==================================
波段交易策略 — 基于4H震荡区间保守参数 v2
v3.1 改动基于v3.0诊断结果):
1. 双边测试 AND→OR在10根K线内测试过支撑 OR 阻力即可(不需两者都测过)
2. 区间稳定性 15%→25%:放宽波动容忍度
3. 入场范围 2%→3%:增加候选信号密度
4. 冷却期 3根→1根减少过渡过滤
保留纯震荡定位、ATR×1.5止损、区间70%止盈、D1趋势过滤
预期年交易量从9笔 → 50-80笔约1-2单/周)
版本历史:
v3.0 (2026-06-10): 初版,基于冯总波段交易新思路
v3.1 (2026-06-10): 降低条件门槛,提升交易频率
"""
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 StructureFlowSwingV31(IStrategy):
"""
Structure Flow Swing Strategy v3.1
4H震荡区间波段交易 — 放宽震荡判定
"""
can_short = True
stoploss = -0.20
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "4h"
# =====================
# 可优化参数(放宽后默认值)
# =====================
swing_lookback = IntParameter(4, 8, default=5, space="buy")
zone_stability_threshold = IntParameter(15, 40, default=25, space="buy") # v3.1: 15→25↑
entry_zone_pct = IntParameter(1, 5, default=3, space="buy") # v3.1: 2→3↑
atr_stop_mult = IntParameter(10, 25, default=15, space="buy")
take_profit_pct = IntParameter(50, 80, default=70, space="sell")
# 固定参数
zone_touch_lookback = 10
breakout_bars = 2
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
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 _detect_range(
self,
sh: pd.Series,
sl: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
) -> DataFrame:
n = len(high)
is_ranging = np.full(n, False)
support_arr = np.full(n, np.nan)
resistance_arr = np.full(n, np.nan)
zone_width_arr = np.full(n, np.nan)
sh_prices = []
sl_prices = []
for i in range(n):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 5:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 5:
sl_prices.pop(0)
if len(sh_prices) < 3 or len(sl_prices) < 3:
continue
current_sh = sh_prices[-1]
current_sl = sl_prices[-1]
if current_sh <= current_sl:
continue
zone_width = (current_sh - current_sl) / current_sl
support_arr[i] = current_sl
resistance_arr[i] = current_sh
zone_width_arr[i] = zone_width
# 条件1区间宽度稳定性
widths = []
for j in range(min(len(sh_prices), len(sl_prices)) - 1, -1, -1):
w = (sh_prices[j] - sl_prices[j]) / sl_prices[j]
widths.append(w)
if len(widths) >= 3:
break
if len(widths) >= 3:
mean_width = np.mean(widths)
if mean_width > 0:
max_dev = max(abs(w - mean_width) / mean_width for w in widths)
stability_threshold = self.zone_stability_threshold.value / 100.0
is_stable = max_dev <= stability_threshold
else:
is_stable = False
else:
is_stable = False
if not is_stable:
continue
# 条件2价格测试过边界 — v3.1: AND→OR
# 只需要测试过支撑或阻力之一,不需要两者都测过
start_idx = max(0, i - self.zone_touch_lookback)
support_zone_upper = current_sl * 1.01
touched_support = any(
low.iloc[j] <= support_zone_upper
for j in range(start_idx, i + 1)
)
resistance_zone_lower = current_sh * 0.99
touched_resistance = any(
high.iloc[j] >= resistance_zone_lower
for j in range(start_idx, i + 1)
)
# v3.1: AND → OR
if not (touched_support or touched_resistance):
continue
# 条件3无突破
consecutive_outside = 0
for j in range(i, max(0, i - self.breakout_bars) - 1, -1):
if close.iloc[j] > current_sh or close.iloc[j] < current_sl:
consecutive_outside += 1
else:
break
if consecutive_outside >= self.breakout_bars:
continue
is_ranging[i] = True
return DataFrame({
"is_ranging": is_ranging,
"support": support_arr,
"resistance": resistance_arr,
"zone_width": zone_width_arr,
}, index=high.index)
# =====================
# 工具ATR计算
# =====================
@staticmethod
def _calc_atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series:
tr = pd.DataFrame({
"hl": high - low,
"hc": (high - close.shift(1)).abs(),
"lc": (low - close.shift(1)).abs(),
}).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"], window=5
)
sh_vals = sh.dropna()
sl_vals = sl.dropna()
is_uptrend = pd.Series(False, index=dataframe.index)
is_downtrend = pd.Series(False, index=dataframe.index)
if len(sh_vals) >= 2 and len(sl_vals) >= 2:
if sh_vals.iloc[-1] > sh_vals.iloc[-2] and sl_vals.iloc[-1] > sl_vals.iloc[-2]:
is_uptrend[:] = True
elif sh_vals.iloc[-1] < sh_vals.iloc[-2] and sl_vals.iloc[-1] < sl_vals.iloc[-2]:
is_downtrend[:] = True
dataframe["d1_uptrend"] = is_uptrend
dataframe["d1_downtrend"] = is_downtrend
return dataframe
# ================================================================
# 主时间框架 — 4H 指标
# ================================================================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback.value,
)
range_info = self._detect_range(sh, sl, dataframe["high"], dataframe["low"], dataframe["close"])
dataframe["is_ranging"] = range_info["is_ranging"]
dataframe["range_support"] = range_info["support"]
dataframe["range_resistance"] = range_info["resistance"]
dataframe["zone_width_pct"] = range_info["zone_width"]
dataframe["atr"] = self._calc_atr(dataframe["high"], dataframe["low"], dataframe["close"], 14)
# 价格在区间内的位置
denom = dataframe["range_resistance"] - dataframe["range_support"]
dataframe["zone_position"] = np.where(
denom > 0,
(dataframe["close"] - dataframe["range_support"]) / denom,
np.nan,
)
# 距离边界百分比
dataframe["dist_to_support"] = np.where(
dataframe["range_support"] > 0,
(dataframe["close"] - dataframe["range_support"]) / dataframe["close"],
np.nan,
)
dataframe["dist_to_resistance"] = np.where(
dataframe["range_resistance"] > 0,
(dataframe["range_resistance"] - dataframe["close"]) / dataframe["close"],
np.nan,
)
for col in ["is_ranging", "zone_position", "dist_to_support", "dist_to_resistance"]:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False if col == "is_ranging" else 999)
return dataframe
# ================================================================
# 入场信号 — v3.1: 冷却期 3→1
# ================================================================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
entry_zone = self.entry_zone_pct.value / 100.0
d1_downtrend_col = "d1_downtrend_1d"
d1_uptrend_col = "d1_uptrend_1d"
for col in ["is_ranging", d1_uptrend_col, d1_downtrend_col]:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
else:
dataframe[col] = False
# ── 做多:震荡市中,价格靠近支撑位 ──
long_conds = (
dataframe["is_ranging"]
& (dataframe["dist_to_support"] <= entry_zone)
& (dataframe["dist_to_support"] > 0)
& (~dataframe[d1_downtrend_col])
)
cooldown = 1 # v3.1: 3→1
long_recent = long_conds.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_conds & long_recent, "enter_long"] = 1
# ── 做空:震荡市中,价格靠近阻力位 ──
short_conds = (
dataframe["is_ranging"]
& (dataframe["dist_to_resistance"] <= entry_zone)
& (dataframe["dist_to_resistance"] > 0)
& (~dataframe[d1_uptrend_col])
)
short_recent = short_conds.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_conds & short_recent, "enter_short"] = 1
return dataframe
# ================================================================
# 出场信号
# ================================================================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe
# ================================================================
# 自定义止损:支撑/阻力外侧ATR*1.5 缓冲
# ================================================================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
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 = dataframe.iloc[-1]
atr_mult = self.atr_stop_mult.value / 10.0
if not trade.is_short:
support = last.get("range_support", np.nan)
atr = last.get("atr", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
if pd.notna(atr) and atr > 0:
sl_price = support - atr * atr_mult
else:
sl_price = support * 0.985
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.20)
else:
resistance = last.get("range_resistance", np.nan)
atr = last.get("atr", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
if pd.notna(atr) and atr > 0:
sl_price = resistance + atr * atr_mult
else:
sl_price = resistance * 1.015
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.20)
# ================================================================
# 自定义止盈区间70%
# ================================================================
def custom_exit(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
**kwargs,
) -> str | None:
tp_pct = self.take_profit_pct.value / 100.0
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return None
last = dataframe.iloc[-1]
if not trade.is_short:
support = last.get("range_support", np.nan)
resistance = last.get("range_resistance", np.nan)
if pd.notna(support) and pd.notna(resistance) and resistance > support:
zone_height = (resistance - support) / support
tp_target = zone_height * tp_pct
if current_profit >= tp_target:
return "take_profit"
else:
support = last.get("range_support", np.nan)
resistance = last.get("range_resistance", np.nan)
if pd.notna(support) and pd.notna(resistance) and resistance > support:
zone_height = (resistance - support) / resistance
tp_target = zone_height * tp_pct
if current_profit >= tp_target:
return "take_profit"
return None
# ================================================================
# Plot config
# ================================================================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"range_support": {"color": "green", "type": "line"},
"range_resistance": {"color": "red", "type": "line"},
},
"subplots": {
"range": {
"is_ranging": {"color": "blue", "type": "line"},
"zone_width_pct": {"color": "purple", "type": "line"},
},
"position": {
"dist_to_support": {"color": "green", "type": "line"},
"dist_to_resistance": {"color": "red", "type": "line"},
},
},
}

View File

@ -0,0 +1,589 @@
"""
Structure Flow Swing Strategy v3.2
==================================
波段交易策略 — 基于4H震荡区间v3.1优化版
v3.2 改动基于v3.1诊断结果 — 三大市场感知不足):
1. D1趋势强度过滤D1处于强趋势时拒绝入场防假区间陷阱
- 计算 D1 EMA20/EMA50 间距作为趋势强度指标
- 趋势强度超过阈值 → 不交易即使4H出现区间形态
2. 区间质量评分:从二分法升级为多维度评分
- 边界测试次数(测试越多越可靠)
- 区间持续时长(越长越成熟)
- 区间宽度适配度3-8%最优)
- 总分>=阈值才入场
3. 主动退出机制:确认转趋势后提前离场
- 3根连续K线收盘在入场时区间外 → 结构破坏
- 不等止损,主动离场(仅在损失<2%时)
- 避免浮盈变亏损
保留纯震荡定位、ATR×1.5止损、区间70%止盈、OR双边测试、冷却期1根
版本历史:
v3.0 (2026-06-10): 初版,基于冯总波段交易新思路
v3.1 (2026-06-10): 降低条件门槛AND→OR等4项
v3.2 (2026-06-10): 三大市场感知改进
"""
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 StructureFlowSwingV32(IStrategy):
"""
Structure Flow Swing Strategy v3.2
4H震荡区间波段交易 — 市场感知增强版
"""
can_short = True
stoploss = -0.20
use_custom_stoploss = True
minimal_roi = {"0": 100}
max_open_trades = 1
timeframe = "4h"
# =====================
# 核心参数沿用v3.1默认值)
# =====================
swing_lookback = IntParameter(4, 8, default=5, space="buy")
zone_stability_threshold = IntParameter(15, 40, default=25, space="buy")
entry_zone_pct = IntParameter(1, 5, default=3, space="buy")
atr_stop_mult = IntParameter(10, 25, default=15, space="buy")
take_profit_pct = IntParameter(50, 80, default=70, space="sell")
# v3.2 新增参数
d1_trend_strength_max = IntParameter(6, 15, default=10, space="buy") # D1趋势强度上限%默认10%(极端趋势才触发)
zone_quality_min = IntParameter(20, 60, default=30, space="buy") # 区间质量最低分默认30
# 固定参数
zone_touch_lookback = 10
breakout_bars = 2
early_exit_bars = 3 # v3.2新增连续N根在区间外触发主动退出
# =====================
# 工具Swing Point 检测
# =====================
@staticmethod
def _detect_swing_points(
high: pd.Series,
low: pd.Series,
window: int = 5,
) -> tuple[pd.Series, pd.Series]:
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 _detect_range(
self,
sh: pd.Series,
sl: pd.Series,
high: pd.Series,
low: pd.Series,
close: pd.Series,
) -> DataFrame:
n = len(high)
is_ranging = np.full(n, False)
support_arr = np.full(n, np.nan)
resistance_arr = np.full(n, np.nan)
zone_width_arr = np.full(n, np.nan)
touch_count_arr = np.full(n, 0) # v3.2新增
sh_prices = []
sl_prices = []
in_range = False
touch_count = 0
for i in range(n):
if pd.notna(sh.iloc[i]):
sh_prices.append(sh.iloc[i])
if len(sh_prices) > 5:
sh_prices.pop(0)
if pd.notna(sl.iloc[i]):
sl_prices.append(sl.iloc[i])
if len(sl_prices) > 5:
sl_prices.pop(0)
if len(sh_prices) < 3 or len(sl_prices) < 3:
# 不在区间中
if in_range:
in_range = False
touch_count = 0
continue
current_sh = sh_prices[-1]
current_sl = sl_prices[-1]
if current_sh <= current_sl:
if in_range:
in_range = False
touch_count = 0
continue
zone_width = (current_sh - current_sl) / current_sl
support_arr[i] = current_sl
resistance_arr[i] = current_sh
zone_width_arr[i] = zone_width
# 条件1区间宽度稳定性
widths = []
for j in range(min(len(sh_prices), len(sl_prices)) - 1, -1, -1):
w = (sh_prices[j] - sl_prices[j]) / sl_prices[j]
widths.append(w)
if len(widths) >= 3:
break
if len(widths) >= 3:
mean_width = np.mean(widths)
if mean_width > 0:
max_dev = max(abs(w - mean_width) / mean_width for w in widths)
stability_threshold = self.zone_stability_threshold.value / 100.0
is_stable = max_dev <= stability_threshold
else:
is_stable = False
else:
is_stable = False
if not is_stable:
if in_range:
in_range = False
touch_count = 0
continue
# 条件2价格测试过边界 — v3.1: AND→OR
start_idx = max(0, i - self.zone_touch_lookback)
support_zone_upper = current_sl * 1.01
touched_support = any(
low.iloc[j] <= support_zone_upper
for j in range(start_idx, i + 1)
)
resistance_zone_lower = current_sh * 0.99
touched_resistance = any(
high.iloc[j] >= resistance_zone_lower
for j in range(start_idx, i + 1)
)
if not (touched_support or touched_resistance):
if in_range:
in_range = False
touch_count = 0
continue
# 条件3无突破
consecutive_outside = 0
for j in range(i, max(0, i - self.breakout_bars) - 1, -1):
if close.iloc[j] > current_sh or close.iloc[j] < current_sl:
consecutive_outside += 1
else:
break
if consecutive_outside >= self.breakout_bars:
if in_range:
in_range = False
touch_count = 0
continue
# === 通过所有条件 → 在区间中 ===
is_ranging[i] = True
# v3.2: 跟踪区间内的边界触碰次数(质量评分数据)
if not in_range:
in_range = True
touch_count = 0
c = close.iloc[i]
if (c <= current_sl * 1.015) or (c >= current_sh * 0.985):
touch_count += 1
touch_count_arr[i] = touch_count
return DataFrame({
"is_ranging": is_ranging,
"support": support_arr,
"resistance": resistance_arr,
"zone_width": zone_width_arr,
"touch_count": touch_count_arr, # v3.2新增
}, index=high.index)
# =====================
# 工具ATR计算
# =====================
@staticmethod
def _calc_atr(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series:
tr = pd.DataFrame({
"hl": high - low,
"hc": (high - close.shift(1)).abs(),
"lc": (low - close.shift(1)).abs(),
}).max(axis=1)
return tr.rolling(period).mean()
# ================================================================
# D1 信息时间框架 — v3.2: 新增趋势强度计算
# ================================================================
@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"], window=5
)
sh_vals = sh.dropna()
sl_vals = sl.dropna()
is_uptrend = pd.Series(False, index=dataframe.index)
is_downtrend = pd.Series(False, index=dataframe.index)
if len(sh_vals) >= 2 and len(sl_vals) >= 2:
if sh_vals.iloc[-1] > sh_vals.iloc[-2] and sl_vals.iloc[-1] > sl_vals.iloc[-2]:
is_uptrend[:] = True
elif sh_vals.iloc[-1] < sh_vals.iloc[-2] and sl_vals.iloc[-1] < sl_vals.iloc[-2]:
is_downtrend[:] = True
dataframe["d1_uptrend"] = is_uptrend
dataframe["d1_downtrend"] = is_downtrend
# v3.2新增D1趋势强度 = EMA20与EMA50的偏离程度
ema_20 = dataframe["close"].ewm(span=20, adjust=False).mean()
ema_50 = dataframe["close"].ewm(span=50, adjust=False).mean()
dataframe["trend_strength"] = abs(ema_20 - ema_50) / ema_50
return dataframe
# ================================================================
# 主时间框架 — 4H 指标v3.2: 新增区间质量评分)
# ================================================================
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
sh, sl = self._detect_swing_points(
dataframe["high"], dataframe["low"],
self.swing_lookback.value,
)
range_info = self._detect_range(sh, sl, dataframe["high"], dataframe["low"], dataframe["close"])
dataframe["is_ranging"] = range_info["is_ranging"]
dataframe["range_support"] = range_info["support"]
dataframe["range_resistance"] = range_info["resistance"]
dataframe["zone_width_pct"] = range_info["zone_width"]
dataframe["range_touch_count"] = range_info["touch_count"]
dataframe["atr"] = self._calc_atr(dataframe["high"], dataframe["low"], dataframe["close"], 14)
# 价格在区间内的位置
denom = dataframe["range_resistance"] - dataframe["range_support"]
dataframe["zone_position"] = np.where(
denom > 0,
(dataframe["close"] - dataframe["range_support"]) / denom,
np.nan,
)
# 距离边界百分比
dataframe["dist_to_support"] = np.where(
dataframe["range_support"] > 0,
(dataframe["close"] - dataframe["range_support"]) / dataframe["close"],
np.nan,
)
dataframe["dist_to_resistance"] = np.where(
dataframe["range_resistance"] > 0,
(dataframe["range_resistance"] - dataframe["close"]) / dataframe["close"],
np.nan,
)
# ── v3.2新增:区间质量评分 ──
self._compute_zone_quality(dataframe)
# ── v3.2新增:区间连续计数 ──
is_ranging_int = dataframe["is_ranging"].astype(int)
consecutive = np.zeros(len(dataframe), dtype=int)
for i in range(1, len(dataframe)):
if is_ranging_int.iloc[i] and is_ranging_int.iloc[i-1]:
consecutive[i] = consecutive[i-1] + 1
elif is_ranging_int.iloc[i]:
consecutive[i] = 1
dataframe["range_consecutive"] = consecutive
for col in ["is_ranging", "zone_position", "dist_to_support", "dist_to_resistance"]:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False if col == "is_ranging" else 999)
return dataframe
def _compute_zone_quality(self, dataframe: DataFrame) -> None:
"""
v3.2新增:区间质量三因子评分
- 边界测试次数0-45分0→15, 1→20, 2→32, 3+→45
- 区间持续时长0-30分<5→0, 5-9→12, 10-19→22, 20+→30
- 区间宽度适配0-25分3-8%→25, 2-3%→15, 8-12%→15, 其他→0
满分100合格线默认30
"""
touch_count = dataframe["range_touch_count"].fillna(0).values
zone_width = dataframe["zone_width_pct"].fillna(0).values
is_ranging = dataframe["is_ranging"].values
quality = np.zeros(len(dataframe))
# 因子1边界测试次数放宽0次触碰也有基础分
quality += np.where(
touch_count >= 3, 45,
np.where(touch_count >= 2, 32,
np.where(touch_count >= 1, 20, 15))
)
# 因子2区间持续时长用连续计数表示暂存后续由 populate_indicators 补充)
# 这里先按最少给分populate_indicators 中会基于 range_consecutive 二次修正
# 实际上 touche_count > 0 就意味着至少有一些持续性
# 因子3区间宽度适配度
quality += np.where(
(zone_width >= 0.03) & (zone_width <= 0.08), 25,
np.where(
((zone_width >= 0.02) & (zone_width < 0.03)) |
((zone_width > 0.08) & (zone_width <= 0.12)), 15, 0
)
)
# 只在区间内有效
quality = np.where(is_ranging, quality, 0)
dataframe["zone_quality_base"] = quality
# ================================================================
# 入场信号 — v3.2: D1趋势强度 + 区间质量过滤 + 持续时间因子
# ================================================================
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
entry_zone = self.entry_zone_pct.value / 100.0
d1_downtrend_col = "d1_downtrend_1d"
d1_uptrend_col = "d1_uptrend_1d"
d1_strength_col = "trend_strength_1d" # v3.2新增
for col in ["is_ranging", d1_uptrend_col, d1_downtrend_col, d1_strength_col]:
if col in dataframe.columns:
dataframe[col] = dataframe[col].fillna(False)
else:
dataframe[col] = False
# ── v3.2: 计算完整区间质量评分(加入持续性因子) ──
range_consec = dataframe.get("range_consecutive", pd.Series(0, index=dataframe.index))
quality_base = dataframe.get("zone_quality_base", pd.Series(0, index=dataframe.index))
# 持续性因子:<5→0, 5-9→12, 10-19→22, 20+→30
duration_score = np.where(
range_consec >= 20, 30,
np.where(range_consec >= 10, 22,
np.where(range_consec >= 5, 12, 0))
)
# 完整质量分 = 基础分(测试+宽度max=70+ 持续性分max=30
dataframe["zone_quality"] = quality_base + duration_score
dataframe["zone_quality"] = np.where(dataframe["is_ranging"], dataframe["zone_quality"], 0)
# ── v3.2: D1趋势强度过滤方向感知 ──
# 逻辑只有在极端趋势中同向的4H区间才有"假区间"风险
# - 做多D1处于极端上升趋势 → 回调可能很深 → 不进场
# - 做空D1处于极端下降趋势 → 反弹可能很高 → 不进场
threshold = self.d1_trend_strength_max.value / 100.0
d1_strength_strong = dataframe[d1_strength_col] > threshold
long_d1_ok = ~(dataframe[d1_uptrend_col] & d1_strength_strong) # 极端上升趋势不做多
short_d1_ok = ~(dataframe[d1_downtrend_col] & d1_strength_strong) # 极端下降趋势不做空
# ── v3.2: 区间质量过滤 ──
quality_min = self.zone_quality_min.value
zone_quality_ok = dataframe["zone_quality"] >= quality_min
# ── 做多:震荡市中,价格靠近支撑位 ──
long_conds = (
dataframe["is_ranging"]
& (dataframe["dist_to_support"] <= entry_zone)
& (dataframe["dist_to_support"] > 0)
& (~dataframe[d1_downtrend_col]) # 原有D1不能是下降趋势
& long_d1_ok # v3.2新增:极端上升趋势不做多
& zone_quality_ok # v3.2新增:区间质量达标
)
cooldown = 1
long_recent = long_conds.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[long_conds & long_recent, "enter_long"] = 1
# ── 做空:震荡市中,价格靠近阻力位 ──
short_conds = (
dataframe["is_ranging"]
& (dataframe["dist_to_resistance"] <= entry_zone)
& (dataframe["dist_to_resistance"] > 0)
& (~dataframe[d1_uptrend_col]) # 原有D1不能是上升趋势
& short_d1_ok # v3.2新增:极端下降趋势不做空
& zone_quality_ok # v3.2新增:区间质量达标
)
short_recent = short_conds.rolling(cooldown, min_periods=1).max().shift(1) == 0
dataframe.loc[short_conds & short_recent, "enter_short"] = 1
return dataframe
# ================================================================
# 出场信号
# ================================================================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return dataframe
# ================================================================
# 自定义止损:支撑/阻力外侧ATR*1.5 缓冲v3.1逻辑保持不变)
# ================================================================
def custom_stoploss(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float:
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 = dataframe.iloc[-1]
atr_mult = self.atr_stop_mult.value / 10.0
if not trade.is_short:
support = last.get("range_support", np.nan)
atr = last.get("atr", np.nan)
if pd.isna(support) or support <= 0:
return -0.02
if pd.notna(atr) and atr > 0:
sl_price = support - atr * atr_mult
else:
sl_price = support * 0.985
sl_ratio = (sl_price / current_rate) - 1.0
return max(sl_ratio, -0.20)
else:
resistance = last.get("range_resistance", np.nan)
atr = last.get("atr", np.nan)
if pd.isna(resistance) or resistance <= 0:
return 0.02
if pd.notna(atr) and atr > 0:
sl_price = resistance + atr * atr_mult
else:
sl_price = resistance * 1.015
sl_ratio = 1.0 - (sl_price / current_rate)
return min(sl_ratio, 0.20)
# ================================================================
# 自定义止盈区间70% + v3.2主动退出机制
# ================================================================
def custom_exit(
self,
pair: str,
trade: Trade,
current_time: datetime,
current_rate: float,
current_profit: float,
**kwargs,
) -> str | None:
tp_pct = self.take_profit_pct.value / 100.0
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe is None or len(dataframe) == 0:
return None
last = dataframe.iloc[-1]
# ── 原有区间70%止盈 ──
if not trade.is_short:
support = last.get("range_support", np.nan)
resistance = last.get("range_resistance", np.nan)
if pd.notna(support) and pd.notna(resistance) and resistance > support:
zone_height = (resistance - support) / support
tp_target = zone_height * tp_pct
if current_profit >= tp_target:
return "take_profit"
else:
support = last.get("range_support", np.nan)
resistance = last.get("range_resistance", np.nan)
if pd.notna(support) and pd.notna(resistance) and resistance > support:
zone_height = (resistance - support) / resistance
tp_target = zone_height * tp_pct
if current_profit >= tp_target:
return "take_profit"
# ── v3.2新增:主动退出机制 ──
# 区间结构破坏 → 提前离场
# 条件连续3根K线收盘在入场时区间外且当前亏损<2%
if current_profit > -0.02:
# 找到入场时的K线取最后一根确认的K线不是当前正在形成的
entry_date = trade.open_date
entry_mask = dataframe["date"] <= entry_date
if entry_mask.any():
entry_idx = dataframe[entry_mask].index[-1]
entry_support = dataframe.loc[entry_idx, "range_support"]
entry_resistance = dataframe.loc[entry_idx, "range_resistance"]
if pd.notna(entry_support) and pd.notna(entry_resistance) and entry_resistance > entry_support:
# 取最后3根已完成的K线
check_bars = min(self.early_exit_bars, len(dataframe) - 1)
recent = dataframe.iloc[-(check_bars + 1):-1] # 排除当前正在形成的K线
if len(recent) >= self.early_exit_bars:
outside_count = 0
for _, bar in recent.iterrows():
c = bar["close"]
# 缓冲0.5%避免噪音触发
if c < entry_support * 0.995 or c > entry_resistance * 1.005:
outside_count += 1
if outside_count >= self.early_exit_bars:
return "early_exit_structure_broken"
return None
# ================================================================
# Plot config
# ================================================================
@staticmethod
def plot_config() -> dict:
return {
"main_plot": {
"range_support": {"color": "green", "type": "line"},
"range_resistance": {"color": "red", "type": "line"},
},
"subplots": {
"range": {
"is_ranging": {"color": "blue", "type": "line"},
"zone_width_pct": {"color": "purple", "type": "line"},
"zone_quality": {"color": "orange", "type": "line"},
},
"position": {
"dist_to_support": {"color": "green", "type": "line"},
"dist_to_resistance": {"color": "red", "type": "line"},
},
},
}