Files
Chan/strategies/PriceActionStrategy.py
2026-03-06 22:08:24 +08:00

396 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
PriceActionStrategy - 纯价格行为策略
核心原则:
零指标。不用 EMA、RSI、MACD、ATR 或任何计算指标。
只看 K线本身(Open/High/Low/Close/Volume)。
价格行为判断方法:
1. 市场结构(趋势):用 Swing High / Swing Low 判断
- 上升趋势 = Higher High + Higher Low
- 下降趋势 = Lower High + Lower Low
2. 入场信号:纯K线形态
- Pin Bar(锤子线/射击之星)
- 吞没形态(Engulfing
- Inside Bar 突破
3. 出场:用前一个 Swing High/Low 作为止盈目标
4. 止损:放在信号K线的另一端
使用时间框架:
- 5m(主时间框架):入场/出场 + 结构判断
"""
import numpy as np
from pandas import DataFrame
from freqtrade.strategy import IStrategy
class PriceActionStrategy(IStrategy):
"""
纯价格行为策略 - 零指标
"""
INTERFACE_VERSION = 3
# === 基础配置 ===
timeframe = "5m"
can_short = True
stoploss = -0.03 # 3% 硬止损安全网
trailing_stop = False
use_custom_stoploss = False
startup_candle_count: int = 100
# 不用 ROI 自动止盈,让价格行为决定出场
minimal_roi = {}
order_types = {
"entry": "market",
"exit": "market",
"stoploss": "market",
"stoploss_on_exchange": False,
}
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# === 参数 ===
swing_lookback = 10 # Swing High/Low 回看K线数
min_body_ratio = 0.55 # 最小实体占比(实体/全幅)
pin_shadow_ratio = 2.5 # Pin Bar 影线至少是实体的 N 倍
engulf_body_ratio = 1.2 # 吞没K线实体至少是前一根的 N 倍
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
纯价格行为 — 只从 OHLCV 提取结构信息,不计算任何技术指标。
"""
df = dataframe
# ========== K线基础属性 ==========
df["body"] = abs(df["close"] - df["open"])
df["candle_range"] = df["high"] - df["low"]
df["body_ratio"] = df["body"] / (df["candle_range"] + 1e-10)
df["upper_shadow"] = df["high"] - df[["close", "open"]].max(axis=1)
df["lower_shadow"] = df[["close", "open"]].min(axis=1) - df["low"]
df["is_bull"] = (df["close"] > df["open"]).astype(int)
df["is_bear"] = (df["close"] < df["open"]).astype(int)
# ========== Swing High / Swing Low ==========
# Swing High: 当前 high 是前后 N 根K线中最高的
# Swing Low: 当前 low 是前后 N 根K线中最低的
n = self.swing_lookback
df["swing_high"] = df["high"].rolling(window=2 * n + 1, center=True).apply(
lambda x: 1 if x.iloc[n] == x.max() else 0, raw=False
)
df["swing_low"] = df["low"].rolling(window=2 * n + 1, center=True).apply(
lambda x: 1 if x.iloc[n] == x.min() else 0, raw=False
)
# 记录最近的 Swing High/Low 价格
df["last_swing_high"] = np.nan
df["last_swing_low"] = np.nan
df["prev_swing_high"] = np.nan
df["prev_swing_low"] = np.nan
df.loc[df["swing_high"] == 1, "last_swing_high"] = df["high"]
df["last_swing_high"] = df["last_swing_high"].ffill()
df.loc[df["swing_low"] == 1, "last_swing_low"] = df["low"]
df["last_swing_low"] = df["last_swing_low"].ffill()
# 前一个 Swing High/Low(用于判断 HH/HL/LH/LL
swing_high_prices = df.loc[df["swing_high"] == 1, "high"]
swing_low_prices = df.loc[df["swing_low"] == 1, "low"]
# 构建 prev_swing_high: 每个 swing high 点对应的上一个 swing high
sh_idx = swing_high_prices.index.tolist()
for i in range(1, len(sh_idx)):
df.loc[sh_idx[i], "prev_swing_high"] = swing_high_prices.loc[sh_idx[i - 1]]
df["prev_swing_high"] = df["prev_swing_high"].ffill()
sl_idx = swing_low_prices.index.tolist()
for i in range(1, len(sl_idx)):
df.loc[sl_idx[i], "prev_swing_low"] = swing_low_prices.loc[sl_idx[i - 1]]
df["prev_swing_low"] = df["prev_swing_low"].ffill()
# ========== 市场结构(趋势)==========
# Higher High + Higher Low = 上升趋势
# Lower High + Lower Low = 下降趋势
df["higher_high"] = (df["last_swing_high"] > df["prev_swing_high"]).astype(int)
df["higher_low"] = (df["last_swing_low"] > df["prev_swing_low"]).astype(int)
df["lower_high"] = (df["last_swing_high"] < df["prev_swing_high"]).astype(int)
df["lower_low"] = (df["last_swing_low"] < df["prev_swing_low"]).astype(int)
df["uptrend"] = ((df["higher_high"] == 1) & (df["higher_low"] == 1)).astype(int)
df["downtrend"] = ((df["lower_high"] == 1) & (df["lower_low"] == 1)).astype(int)
# ========== 价格行为形态 ==========
# --- Pin Bar(锤子线 / 射击之星)---
# 看涨 Pin Bar: 长下影线,短上影线,实体在上半部分
df["bullish_pin"] = (
(df["lower_shadow"] > df["body"] * self.pin_shadow_ratio)
& (df["lower_shadow"] > df["upper_shadow"] * 2)
& (df["body_ratio"] > 0.15) # 不是十字星
& (df["is_bull"] == 1)
).astype(int)
# 看跌 Pin Bar: 长上影线,短下影线,实体在下半部分
df["bearish_pin"] = (
(df["upper_shadow"] > df["body"] * self.pin_shadow_ratio)
& (df["upper_shadow"] > df["lower_shadow"] * 2)
& (df["body_ratio"] > 0.15)
& (df["is_bear"] == 1)
).astype(int)
# --- 吞没形态(Engulfing---
prev_body = df["body"].shift(1)
prev_open = df["open"].shift(1)
prev_close = df["close"].shift(1)
# 看涨吞没: 前一根阴线,当前阳线完全包住前一根
df["bullish_engulf"] = (
(df["is_bull"] == 1)
& (prev_close < prev_open) # 前一根是阴线
& (df["open"] <= prev_close) # 开盘 <= 前收盘(低开或平开)
& (df["close"] >= prev_open) # 收盘 >= 前开盘(完全吞没)
& (df["body"] > prev_body * self.engulf_body_ratio) # 实体更大
).astype(int)
# 看跌吞没
df["bearish_engulf"] = (
(df["is_bear"] == 1)
& (prev_close > prev_open) # 前一根是阳线
& (df["open"] >= prev_close) # 开盘 >= 前收盘
& (df["close"] <= prev_open) # 收盘 <= 前开盘
& (df["body"] > prev_body * self.engulf_body_ratio)
).astype(int)
# --- Inside Bar 突破 ---
# Inside Bar: 当前K线的 high/low 完全在前一根范围内
prev_high = df["high"].shift(1)
prev_low = df["low"].shift(1)
df["inside_bar"] = (
(df["high"] <= prev_high)
& (df["low"] >= prev_low)
).astype(int)
# Inside Bar 之后的突破
# 向上突破: 前一根是 inside bar,当前收盘 > 母线(前两根)的 high
mother_high = df["high"].shift(2)
mother_low = df["low"].shift(2)
df["inside_break_up"] = (
(df["inside_bar"].shift(1) == 1)
& (df["close"] > mother_high)
& (df["is_bull"] == 1)
).astype(int)
df["inside_break_down"] = (
(df["inside_bar"].shift(1) == 1)
& (df["close"] < mother_low)
& (df["is_bear"] == 1)
).astype(int)
# --- 支撑/阻力突破 ---
# 突破前一个 Swing High(做多)
df["break_swing_high"] = (
(df["close"] > df["last_swing_high"].shift(1))
& (df["close"].shift(1) <= df["last_swing_high"].shift(1))
& (df["is_bull"] == 1)
& (df["body_ratio"] > self.min_body_ratio) # 实体饱满(有力度)
).astype(int)
# 跌破前一个 Swing Low(做空)
df["break_swing_low"] = (
(df["close"] < df["last_swing_low"].shift(1))
& (df["close"].shift(1) >= df["last_swing_low"].shift(1))
& (df["is_bear"] == 1)
& (df["body_ratio"] > self.min_body_ratio)
).astype(int)
# --- 成交量确认(只用原始 volume 对比,不算均线)---
# 当前成交量 > 前3根的最大成交量 = 放量
df["vol_expand"] = (
df["volume"] > df["volume"].rolling(3).max().shift(1)
).astype(int)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
入场条件 — 纯价格行为
做多条件(满足任一组):
A) 上升趋势 + 看涨 Pin Bar(回调到支撑后反弹信号)
B) 上升趋势 + 看涨吞没(回调后强势反转)
C) 上升趋势 + Inside Bar 向上突破(蓄力后爆发)
D) 突破 Swing High + 放量(结构性突破)
做空条件(镜像)
"""
df = dataframe
# ===== 做多 =====
conditions_long = []
# A) 上升趋势 + 看涨 Pin Bar
conditions_long.append(
(df["uptrend"] == 1)
& (df["bullish_pin"] == 1)
& (df["vol_expand"] == 1)
)
# B) 上升趋势 + 看涨吞没
conditions_long.append(
(df["uptrend"] == 1)
& (df["bullish_engulf"] == 1)
)
# C) 上升趋势 + Inside Bar 向上突破
conditions_long.append(
(df["uptrend"] == 1)
& (df["inside_break_up"] == 1)
& (df["vol_expand"] == 1)
)
# D) 突破 Swing High + 放量(不需要已确认趋势,突破本身建立趋势)
conditions_long.append(
(df["break_swing_high"] == 1)
& (df["vol_expand"] == 1)
)
if conditions_long:
import pandas as pd
combined = pd.concat(conditions_long, axis=1).any(axis=1)
dataframe.loc[combined, "enter_long"] = 1
# ===== 做空 =====
conditions_short = []
# A) 下降趋势 + 看跌 Pin Bar
conditions_short.append(
(df["downtrend"] == 1)
& (df["bearish_pin"] == 1)
& (df["vol_expand"] == 1)
)
# B) 下降趋势 + 看跌吞没
conditions_short.append(
(df["downtrend"] == 1)
& (df["bearish_engulf"] == 1)
)
# C) 下降趋势 + Inside Bar 向下突破
conditions_short.append(
(df["downtrend"] == 1)
& (df["inside_break_down"] == 1)
& (df["vol_expand"] == 1)
)
# D) 跌破 Swing Low + 放量
conditions_short.append(
(df["break_swing_low"] == 1)
& (df["vol_expand"] == 1)
)
if conditions_short:
import pandas as pd
combined = pd.concat(conditions_short, axis=1).any(axis=1)
dataframe.loc[combined, "enter_short"] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
出场条件 — 纯价格行为
做多出场:
- 出现看跌吞没
- 出现看跌 Pin Bar
- 市场结构转为下降趋势
- 跌破前一个 Swing Low
做空出场(镜像)
"""
df = dataframe
# 做多出场
dataframe.loc[
(df["bearish_engulf"] == 1)
| (df["bearish_pin"] == 1)
| (df["downtrend"] == 1)
| (df["break_swing_low"] == 1),
"exit_long",
] = 1
# 做空出场
dataframe.loc[
(df["bullish_engulf"] == 1)
| (df["bullish_pin"] == 1)
| (df["uptrend"] == 1)
| (df["break_swing_high"] == 1),
"exit_short",
] = 1
return dataframe
def custom_exit(
self,
pair: str,
trade,
current_time,
current_rate: float,
current_profit: float,
**kwargs,
):
"""
自定义出场 — 基于价格行为的动态止盈
1. 利润 > 2% 且出现反转K线 → 锁利
2. 持仓超过 2 小时且利润 < 0.3% → 超时退出(行情没走出来)
"""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe.empty or len(dataframe) < 2:
return None
last = dataframe.iloc[-1]
trade_duration = (current_time - trade.open_date_utc).total_seconds() / 60
# 1. 有利润 + 反转K线 → 锁利
if not trade.is_short:
if current_profit > 0.02:
if last.get("bearish_pin", 0) == 1 or last.get("bearish_engulf", 0) == 1:
return "reversal_signal_tp"
if current_profit > 0.035:
# 大利润时,任何阴线都考虑锁利
if last.get("is_bear", 0) == 1 and last.get("body_ratio", 0) > 0.6:
return "strong_bear_candle_tp"
else:
if current_profit > 0.02:
if last.get("bullish_pin", 0) == 1 or last.get("bullish_engulf", 0) == 1:
return "reversal_signal_tp"
if current_profit > 0.035:
if last.get("is_bull", 0) == 1 and last.get("body_ratio", 0) > 0.6:
return "strong_bull_candle_tp"
# 2. 超时退出 — 行情没走出来
if trade_duration > 120 and current_profit < 0.003:
return "timeout_exit"
return None
def leverage(
self,
pair: str,
current_time,
current_rate: float,
proposed_leverage: float,
max_leverage: float,
entry_tag: str | None,
side: str,
**kwargs,
) -> float:
return 3.0