v2.0 (Scalp): 顺趋势动量剥头皮 - 方向正确但20x杠杆过重

This commit is contained in:
2026-06-10 08:36:00 +08:00
parent 92fdf2c941
commit 89174352e2

View File

@ -1,259 +1,130 @@
"""
Structure Flow Scalp — 震荡市剥头皮策略
==========================================
基于Al Brooks价格行为学
- 在已识别的震荡区间内,支撑位做多、阻力位做空
- 15m级别支撑/阻力决定交易区间5m级别入场
- 100x全仓杠杆每次10%仓位
- 区间高度40%止盈15m支撑/阻力外侧0.3%止损
# 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): 初始版本,完全重写
变更记录:
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 freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, informative
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, informative
import pandas as pd
import numpy as np
from datetime import datetime
from freqtrade.persistence import Trade
class StructureFlowScalp(IStrategy):
class StructureFlowMomentumScalp(IStrategy):
"""
震荡市剥头皮策略 — 5m框架100x全仓杠杆。
去掉4H趋势强度判断——15m支撑阻力本身就是最好的过滤器。
顺趋势剥头皮策略 v2.0
核心逻辑:
- 15m EMA趋势方向过滤只做顺趋势方向的单
- 5m 回调到EMA20或S/R支撑/阻力区域时等待K线信号确认后入场
- 止损 ATR×1.0,止盈 ATR×1.5,时间止损 60 分钟
- 不做方向猜测,不吃鱼头鱼尾,只吃回调结束那一小段
"""
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
# =====================
# ── 交易参数 ──
can_short = True
max_open_trades = 1
stake_amount = "unlimited"
use_custom_stoploss = True
use_exit_signal = False # 出场完全由 custom_stoploss + custom_exit 管理
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)
# ── 合约参数 ──
margin_mode = "cross"
trading_mode = "futures"
# =====================
# 工具查找入场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")
# ── 可优化参数 ──
# 趋势检测
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")
# 区间高度止盈比例(%
profit_zone_pct = IntParameter(20, 60, default=40, space="buy")
# ── 常数 ──
time_stop_minutes = 60 # 最大持仓时间
# =====================
# 工具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
# ── 保护性止损 ──
stoploss = -0.10 # 硬止损 10%
# ================================================================
# 信息时间框架 — 15m 短期支撑阻力(核心过滤器)
# 杠杆
# ================================================================
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:
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"]
"""15m级别EMA趋势方向 + swing point S/R。"""
# ── 活支撑检查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
# ── 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()
# ── 活阻力检查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["trend_up"] = dataframe["ema_fast"] > dataframe["ema_slow"]
dataframe["trend_down"] = dataframe["ema_fast"] < dataframe["ema_slow"]
# 区间高度(用于止盈计算)
dataframe["zone_height"] = (dataframe["resistance"] - dataframe["support"]).fillna(0)
# ── 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 指标
# 主框架 — 5m 级别指标
# ================================================================
def populate_indicators(
self, dataframe: DataFrame, metadata: dict
) -> DataFrame:
"""5m级别ATR + K线形态 + 信号整合。"""
"""5m级别ATR + K线形态 + EMA趋势整合。"""
# ── ATR(14) — 用于动态止损,根据市场波动自适应 ──
# ── ATR(14) ──
high = dataframe["high"]
low = dataframe["low"]
close = dataframe["close"]
@ -264,170 +135,136 @@ class StructureFlowScalp(IStrategy):
(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
# ── 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()
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"]:
# ── 布尔列NaN填充 ──
for col in ["bullish_signal", "bearish_signal"]:
dataframe[col] = dataframe[col].fillna(False)
# ── 做多 ──
# 条件价格贴近15m支撑0.5%范围内)- 使用 low 而非 open
# 因为支撑测试看的是价格是否到达支撑位,不是开盘在哪
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.005) &
(dataframe["low"] >= dataframe["support_15m"] * 0.995)
(dataframe["low"] <= dataframe["support_15m"] * (1.0 + dev)) &
(dataframe["low"] >= dataframe["support_15m"] * (1.0 - dev))
)
pullback_long = near_ema | near_support
long_conditions = (
near_support
& dataframe["support_alive_15m"]
& dataframe["bullish_signal"]
# 条件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))
)
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)
(dataframe["high"] >= dataframe["resistance_15m"] * (1.0 - dev)) &
(dataframe["high"] <= dataframe["resistance_15m"] * (1.0 + dev))
)
pullback_short = near_ema_short | near_resistance
short_conditions = (
near_resistance
& dataframe["resistance_alive_15m"]
& dataframe["bearish_signal"]
)
# 条件3K线止涨信号
signal_short = 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
# 综合入场
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_custom_exit=True
# =====================
# ================================================================
# exit_trendfreqtrade 2025.11 强制要求,即使 use_exit_signal=False
# ================================================================
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""退出逻辑完全由 custom_stoploss + custom_exit 管理。"""
"""出场完全由 custom_stoploss + custom_exit 管理。"""
return dataframe
# =====================
# 动态止损 — 入场价 - ATR×2.0(基于市场波动,非固定比例
# =====================
# ================================================================
# 出场 — 止损ATR动态
# ================================================================
def custom_stoploss(
self,
@ -440,87 +277,39 @@ class StructureFlowScalp(IStrategy):
**kwargs,
) -> float:
"""
止损锚定入场价宽度根据市场波动ATR动态计算而非固定比例。
止损 = 入场价 ± ATR × atr_mult_stop
核心逻辑:
- 做多止损 = 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窄追踪
- 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
# 查找入场时的 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:
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:
# 做多:止损 = 入场价 - 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
sl_price = trade.open_rate - (atr * mult)
sl_ratio = (sl_price / trade.open_rate) - 1.0
return max(sl_ratio, -self.stoploss)
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)
sl_price = trade.open_rate + (atr * mult)
sl_ratio = 1.0 - (sl_price / trade.open_rate)
return min(sl_ratio, self.stoploss)
# 追踪保护(做空对称)
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
# =====================
# 区间高度止盈
# =====================
# ================================================================
# 出场 — 止盈ATR动态+ 时间止损
# ================================================================
def custom_exit(
self,
@ -532,58 +321,195 @@ class StructureFlowScalp(IStrategy):
**kwargs,
) -> str | None:
"""
当利润达到入场时锁定的15m区间高度的设定比例时止盈。
使用入场时锁定的S/R值计算区间高度zone_height而非最新的值
- 入场后如果区间收缩,止盈目标不会跟着变小
- 让入场时确定的止盈逻辑"钉死"
- profit_zone_pct 默认40%即锁定区间高度的40%
出场逻辑:
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
# 查找入场时的 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:
atr = entry_row.get("atr", np.nan)
if pd.isna(atr) or atr <= 0:
return None
# 用锁定的区间高度计算止盈目标(不随市场漂移)
locked_zone_height = resistance - support
target_pct = (locked_zone_height * self.profit_zone_pct.value / 100.0) / trade.open_rate
# 1. ATR 止盈
tp_mult = self.atr_mult_tp.value
tp_ratio = (atr * tp_mult) / trade.open_rate
if current_profit >= target_pct:
return "zone_tp"
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
# =====================
# 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"},
},
},
}
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