386 lines
19 KiB
Python
386 lines
19 KiB
Python
# src/strategies/smc_pure_h1_long_strategy.py
|
||
|
||
from datetime import datetime, timedelta
|
||
from typing import Optional, List, Any, Dict
|
||
import math
|
||
|
||
from .base_strategy import Strategy
|
||
from ..core_data import Bar, Order, Trade
|
||
|
||
|
||
class SMCPureH1LongStrategy(Strategy):
|
||
"""
|
||
基于 15分钟 K线进行交易,但聚合 1小时 K线判断趋势,结合 SMC/PA 入场信号。
|
||
下单手数固定,止盈止损由策略内部管理,止损为固定点数。
|
||
新增波段出场条件(跌破最近15分钟波段低点)。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
context: "BacktestContext",
|
||
main_symbol: str,
|
||
fixed_volume: float,
|
||
fixed_stop_loss_points: float, # 新增:固定止损点数 (例如 10 或 20)
|
||
tick_size: float, # 新增:最小跳动点 (例如 0.5, 1.0 等)
|
||
enable_log: bool = True,
|
||
trend_lookback_bars_1h: int = 30, # 用于判断聚合后1小时趋势的K线数量
|
||
ob_fvg_lookback_bars: int = 10, # 用于识别15分钟OB/FVG和流动性扫荡的K线数量
|
||
liquidity_sweep_wick_ratio: float = 0.5, # 15分钟扫荡K线下影线占其总范围的最小比例
|
||
limit_order_retrace_ratio: float = 0.382, # 15分钟限价单的入场点位于扫荡K线低点到收盘价的斐波那契回撤比例
|
||
tp_rr_ratio: float = 2.0, # 止盈风险回报比
|
||
**params: Any,
|
||
):
|
||
super().__init__(context, main_symbol, enable_log, **params)
|
||
self.fixed_volume = fixed_volume
|
||
self.fixed_stop_loss_points = fixed_stop_loss_points
|
||
self.tick_size = tick_size
|
||
self.trend_lookback_bars_1h = trend_lookback_bars_1h
|
||
self.ob_fvg_lookback_bars = ob_fvg_lookback_bars
|
||
self.liquidity_sweep_wick_ratio = liquidity_sweep_wick_ratio
|
||
self.limit_order_retrace_ratio = limit_order_retrace_ratio
|
||
self.tp_rr_ratio = tp_rr_ratio
|
||
|
||
# 存储当前持仓的止盈止损目标
|
||
self.active_trade_params: Dict[str, Dict[str, float]] = {}
|
||
|
||
# 用于1小时K线聚合
|
||
self.current_1h_bars_data: List[Bar] = [] # 存储当前正在聚合的15分钟K线
|
||
self.aggregated_1h_bars_history: List[Bar] = [] # 存储聚合完成的1小时K线历史
|
||
# 用于确保只处理每个1小时周期的第一个15分钟bar的聚合,防止重复
|
||
self.last_aggregated_1h_bar_end_time: Optional[datetime] = None
|
||
|
||
self.log(f"SMCPureH1LongStrategy for {self.symbol} initialized.")
|
||
self.log(f"Params: Fixed SL Points={self.fixed_stop_loss_points}, Tick Size={self.tick_size}")
|
||
|
||
def on_init(self):
|
||
super().on_init()
|
||
self.cancel_all_pending_orders(main_symbol=self.symbol)
|
||
self.trading = True
|
||
self.active_trade_params.clear()
|
||
self.current_1h_bars_data.clear()
|
||
self.aggregated_1h_bars_history.clear()
|
||
self.last_aggregated_1h_bar_end_time = None
|
||
|
||
def on_open_bar(self, open_price: float, symbol: str):
|
||
"""
|
||
每当新的 15分钟 K线数据到来时调用此方法。
|
||
"""
|
||
if symbol != self.symbol:
|
||
return
|
||
|
||
current_time_15m = self.context.get_current_time()
|
||
|
||
# 获取 15分钟历史K线(用于入场和波段出场判断)
|
||
history_15m: List[Bar] = self.get_bar_history()
|
||
|
||
# 确保有足够的 15分钟历史数据来获取 prev_bar_15m (刚刚收盘的K线)
|
||
if len(history_15m) < 2:
|
||
# self.log(f"历史15分钟数据不足,当前只有 {len(history_15m)} 根K线。")
|
||
return
|
||
prev_bar_15m = history_15m[-1] # 刚刚收盘的 15分钟 K线
|
||
|
||
# --- 1. 聚合 15分钟 K线到 1小时 K线 ---
|
||
# 确保我们正在处理的是新的15分钟K线,并且避免在同一1小时周期内重复聚合
|
||
# K线的start_time通常是整点或15/30/45分
|
||
# 我们聚合的是 prev_bar_15m, 也就是刚刚结束的15分钟K线
|
||
self.current_1h_bars_data.append(prev_bar_15m)
|
||
|
||
# 检查是否凑齐了4根15分钟K线,或者当前15分钟K线是新1小时的开始
|
||
# 1小时K线的结束时间是下一个小时的整点(例如 09:00, 10:00)
|
||
# 我们可以根据prev_bar_15m的结束时间来判断是否完成了一个小时周期
|
||
# 这里的逻辑是,每当15分钟K线的结束时间刚好是一个小时的整点时,就尝试聚合
|
||
# 例如,09:00, 09:15, 09:30, 09:45 对应下一根K线的 open_time。
|
||
# 如果当前prev_bar_15m的end_time是例如 09:00:00, 那么它就是08:45:00-09:00:00的K线。
|
||
# 下一个小时的整点 (e.g., 09:00:00)
|
||
# 调整为基于 K线数量的聚合
|
||
if len(self.current_1h_bars_data) == 4:
|
||
# 聚合这4根15分钟K线
|
||
first_15m_bar = self.current_1h_bars_data[0]
|
||
last_15m_bar = self.current_1h_bars_data[-1]
|
||
|
||
aggregated_1h_bar = Bar(
|
||
symbol=self.symbol,
|
||
datetime=first_15m_bar.datetime,
|
||
open=first_15m_bar.open,
|
||
high=max(b.high for b in self.current_1h_bars_data),
|
||
low=min(b.low for b in self.current_1h_bars_data),
|
||
close=last_15m_bar.close,
|
||
volume=sum(b.volume for b in self.current_1h_bars_data),
|
||
open_oi=0,
|
||
close_oi=0
|
||
)
|
||
self.aggregated_1h_bars_history.append(aggregated_1h_bar)
|
||
self.current_1h_bars_data.clear() # 清空,准备收集下一小时的K线
|
||
# self.log(f"聚合生成新的1小时K线: {aggregated_1h_bar.start_time.strftime('%H:%M')} - {aggregated_1h_bar.close:.2f}")
|
||
|
||
# --- 2. 获取当前仓位信息 ---
|
||
current_positions = self.get_current_positions()
|
||
current_pos_volume = current_positions.get(self.symbol, 0)
|
||
avg_entry_price = self.get_average_position_price(self.symbol)
|
||
|
||
|
||
# --- 3. 首先处理现有仓位的止盈止损及波段出场 ---
|
||
if current_pos_volume > 0 and self.symbol in self.active_trade_params:
|
||
trade_params = self.active_trade_params[self.symbol]
|
||
stop_loss_price = trade_params['stop_loss']
|
||
take_profit_price = trade_params['take_profit']
|
||
|
||
# --- 3.1 检查固定点数止损或止盈 ---
|
||
if open_price <= stop_loss_price:
|
||
self.log(
|
||
f"在 {current_time_15m} 触及固定点数止损!当前开盘价 {open_price:.2f} <= 止损价 {stop_loss_price:.2f}。平仓。")
|
||
self.log(f"持仓均价: {avg_entry_price:.2f}, 止损价: {stop_loss_price:.2f}")
|
||
self._close_position(current_pos_volume, "止损")
|
||
return
|
||
|
||
elif open_price >= take_profit_price:
|
||
self.log(
|
||
f"在 {current_time_15m} 触及止盈!当前开盘价 {open_price:.2f} >= 止盈价 {take_profit_price:.2f}。平仓。")
|
||
self.log(f"持仓均价: {avg_entry_price:.2f}, 止盈价: {take_profit_price:.2f}")
|
||
self._close_position(current_pos_volume, "止盈")
|
||
return
|
||
|
||
# --- 3.2 检查波段出场条件 (15分钟级别结构破坏) ---
|
||
if self._check_swing_exit_condition(history_15m, open_price):
|
||
self.log(f"在 {current_time_15m} 触及波段出场条件(15分钟结构破坏)。平仓。")
|
||
self.log(f"持仓均价: {avg_entry_price:.2f}")
|
||
self._close_position(current_pos_volume, "波段出场")
|
||
return
|
||
|
||
# --- 4. 如果没有持仓,尝试寻找开仓机会 ---
|
||
# 确保有足够的 15分钟历史数据进行入场分析
|
||
required_bars_15m = self.ob_fvg_lookback_bars + 5
|
||
if len(history_15m) < required_bars_15m:
|
||
# self.log(f"15分钟历史数据不足,当前只有 {len(history_15m)} 根K线,需要至少 {required_bars_15m} 根。")
|
||
return
|
||
|
||
# 检查是否存在挂单,如果是,则不重复开仓 (单次下单限制)
|
||
pending_orders = self.get_pending_orders()
|
||
if any(order.symbol == self.symbol for order in pending_orders.values()):
|
||
# self.log(f"已有 {self.symbol} 挂单,不开新仓。")
|
||
return
|
||
|
||
# 如果当前有持仓,并且没有触及任何出场条件,那么就不再开新仓,直接返回
|
||
if current_pos_volume > 0:
|
||
return
|
||
|
||
# --- 4.1 1小时趋势判断 (使用聚合后的K线) ---
|
||
# 确保有足够的1小时K线用于趋势判断
|
||
if len(self.aggregated_1h_bars_history) < self.trend_lookback_bars_1h + 5:
|
||
# self.log(f"聚合1小时K线数据不足,当前只有 {len(self.aggregated_1h_bars_history)} 根,需要至少 {self.trend_lookback_bars_1h + 5} 根。")
|
||
return
|
||
|
||
is_uptrend_1h, swing_lows_1h = self._is_uptrend_1h(
|
||
self.aggregated_1h_bars_history, self.trend_lookback_bars_1h
|
||
)
|
||
if not is_uptrend_1h:
|
||
# self.log(f"在 {current_time_15m},1小时趋势非明确上升趋势,等待。")
|
||
return
|
||
self.log(f"在 {current_time_15m},1小时趋势判断: 明确上升趋势。")
|
||
|
||
# --- 4.2 15分钟 SMC/PA 入场信号 (波段回踩后的流动性扫荡) ---
|
||
# 这里使用 prev_bar_15m 作为扫荡K线候选,因为 on_open_bar 意味着 prev_bar_15m 已经完整
|
||
is_sweep_candle_15m, sweep_low_point_15m, swept_liquidity_level_15m = self._is_bullish_liquidity_sweep_15m(
|
||
prev_bar_15m, history_15m, self.ob_fvg_lookback_bars, self.liquidity_sweep_wick_ratio
|
||
)
|
||
|
||
if not is_sweep_candle_15m:
|
||
# self.log(f"前一根15分钟K线 {prev_bar_15m.start_time} 非扫荡K线,等待。")
|
||
return
|
||
|
||
self.log(
|
||
f"确认15分钟扫荡K线 {prev_bar_15m.datetime},低点 {sweep_low_point_15m:.2f} 刺破流动性 {swept_liquidity_level_15m:.2f}。")
|
||
|
||
# --- 4.3 计算入场价格 (限价单) ---
|
||
# entry_limit_price_candidate = prev_bar_15m.low + (
|
||
# prev_bar_15m.close - prev_bar_15m.low) * self.limit_order_retrace_ratio
|
||
entry_limit_price_candidate = open_price - (
|
||
prev_bar_15m.close - prev_bar_15m.low) * self.limit_order_retrace_ratio
|
||
|
||
# 确保限价单价格在当前15分钟K线开盘价之下,才能以限价单形式成交
|
||
if entry_limit_price_candidate > open_price:
|
||
# self.log(f"计算的限价入场价格 {entry_limit_price_candidate:.2f} 高于当前15分钟开盘价 {open_price:.2f},无法通过限价单成交,放弃。")
|
||
return
|
||
|
||
entry_price = entry_limit_price_candidate
|
||
|
||
# --- 4.4 计算固定点数止损和止盈目标 ---
|
||
# 止损点: 入场价 - 固定点数 * 最小跳动点
|
||
stop_loss_calculated = entry_price - (self.fixed_stop_loss_points * self.tick_size)
|
||
|
||
# 止盈点: 基于风险回报比计算
|
||
risk_per_unit_for_tp = abs(entry_price - stop_loss_calculated)
|
||
if risk_per_unit_for_tp <= 0:
|
||
self.log("计算风险为零或负数,无法下单。")
|
||
return
|
||
|
||
take_profit_calculated = entry_price + (risk_per_unit_for_tp * self.tp_rr_ratio)
|
||
|
||
# 确保止盈价格大于入场价格
|
||
if take_profit_calculated <= entry_price:
|
||
self.log(f"止盈价格 {take_profit_calculated:.2f} 小于或等于入场价格 {entry_price:.2f},无效交易。")
|
||
return
|
||
|
||
# --- 4.5 设置固定头寸数量 ---
|
||
volume = self.fixed_volume
|
||
if volume <= 0:
|
||
self.log("设定的交易量为零或负数,无法下单。")
|
||
return
|
||
|
||
# --- 4.6 发送限价买入订单 ---
|
||
order = Order(
|
||
symbol=self.symbol,
|
||
direction="BUY",
|
||
volume=int(volume),
|
||
price_type="LIMIT",
|
||
limit_price=entry_price,
|
||
)
|
||
|
||
self.cancel_all_pending_orders(main_symbol=self.symbol) # 取消所有之前的挂单
|
||
|
||
sent_order = self.send_order(order)
|
||
if sent_order:
|
||
self.log(
|
||
f"在 {current_time_15m} 发送买入限价订单: {self.symbol}, "
|
||
f"限价 {sent_order.limit_price:.2f}, 数量 {sent_order.volume}"
|
||
)
|
||
# 存储止盈止损目标,以便后续管理
|
||
self.active_trade_params[self.symbol] = {
|
||
'stop_loss': stop_loss_calculated,
|
||
'take_profit': take_profit_calculated,
|
||
'entry_price': entry_price, # 存储理论入场价
|
||
'volume': volume
|
||
}
|
||
self.log(f"预期止损: {stop_loss_calculated:.2f}, 预期止盈: {take_profit_calculated:.2f}")
|
||
else:
|
||
self.log(f"未能发送买入订单:{self.symbol}")
|
||
|
||
def _close_position(self, volume_to_close: float, reason: str):
|
||
"""通用平仓方法"""
|
||
close_order = Order(
|
||
symbol=self.symbol,
|
||
direction="SELL",
|
||
volume=int(volume_to_close),
|
||
price_type="MARKET",
|
||
)
|
||
sent_close_order = self.send_order(close_order)
|
||
if sent_close_order:
|
||
self.log(f"成功发送平仓订单 ({reason}): {self.symbol}, 数量 {volume_to_close}")
|
||
if self.symbol in self.active_trade_params:
|
||
del self.active_trade_params[self.symbol] # 清除止盈止损目标
|
||
else:
|
||
self.log(f"未能发送平仓订单 ({reason}): {self.symbol}")
|
||
|
||
def _is_uptrend_1h(self, history_1h: List[Bar], lookback: int) -> tuple[bool, List[float]]:
|
||
"""
|
||
基于聚合后的 1小时 K线判断上升趋势。
|
||
1. 最新收盘价高于最近均线。
|
||
2. 最后一个波段低点高于第一个波段低点(宽松版 Higher Low)。
|
||
"""
|
||
if len(history_1h) < lookback + 2: # 确保有足够K线用于计算和判断
|
||
return False, []
|
||
|
||
recent_bars = history_1h[-lookback:]
|
||
|
||
# 1. 简单均线判断:最新收盘价高于 SMA
|
||
sum_closes = sum(bar.close for bar in recent_bars)
|
||
avg_close = sum_closes / len(recent_bars)
|
||
if recent_bars[-1].close < avg_close:
|
||
return False, []
|
||
|
||
# 2. 识别 Higher Lows (HL)
|
||
swing_lows = []
|
||
# 查找波段低点(局部最低点,且其左右K线更高)
|
||
for i in range(1, len(recent_bars) - 1): # 遍历中间K线
|
||
if recent_bars[i].low < recent_bars[i - 1].low and \
|
||
recent_bars[i].low < recent_bars[i + 1].low:
|
||
swing_lows.append(recent_bars[i].low)
|
||
|
||
if len(swing_lows) < 2: # 至少需要两个波段低点来判断HL
|
||
return False, []
|
||
|
||
# 调整后的 Higher Lows 检查: 只需要最后一个波段低点高于第一个波段低点即可。
|
||
is_hl_pattern = swing_lows[-1] > swing_lows[0]
|
||
|
||
return is_hl_pattern, swing_lows
|
||
|
||
def _is_bullish_liquidity_sweep_15m(self, current_bar_15m: Bar, history_15m: List[Bar], lookback_bars: int,
|
||
wick_ratio: float) -> tuple[bool, float, float]:
|
||
"""
|
||
判断 `current_bar_15m` 是否为看涨流动性扫荡K线 (15分钟图)。
|
||
"""
|
||
sweep_candidate_bar = current_bar_15m
|
||
|
||
# 1. 识别最近的低点作为潜在的流动性池 (在扫荡K线之前,在 15分钟图上)
|
||
if len(history_15m) < lookback_bars + 1:
|
||
return False, 0.0, 0.0
|
||
|
||
# 取扫荡K线之前的 lookback_bars 范围内的K线来识别流动性池
|
||
# history_15m 包含从最早到最新的K线,[- (lookback_bars + 1) : -1] 确保不包含 sweep_candidate_bar
|
||
relevant_history_for_liquidity = history_15m[-(lookback_bars + 1):-1]
|
||
if not relevant_history_for_liquidity:
|
||
return False, 0.0, 0.0
|
||
|
||
# 找到相关历史中的最低点作为被扫荡的流动性水平
|
||
swept_liquidity_level = min(bar.low for bar in relevant_history_for_liquidity)
|
||
|
||
# 2. 扫荡K线 (sweep_candidate_bar) 的低点是否刺穿了流动性水平
|
||
if sweep_candidate_bar.low >= swept_liquidity_level:
|
||
return False, 0.0, 0.0
|
||
|
||
# 3. 扫荡K线是否形成长下影线并收回
|
||
total_range = sweep_candidate_bar.high - sweep_candidate_bar.low
|
||
if total_range <= 0: return False, 0.0, 0.0
|
||
|
||
lower_wick_length = min(sweep_candidate_bar.open, sweep_candidate_bar.close) - sweep_candidate_bar.low
|
||
|
||
# 下影线必须足够长,且收盘价必须收回至刺破的流动性水平之上
|
||
if (lower_wick_length / total_range) >= wick_ratio and \
|
||
sweep_candidate_bar.close > swept_liquidity_level:
|
||
|
||
# 扫荡K线本身不能是巨幅下跌的K线(实体不能太大,最好是阳线或小阴线)
|
||
# 允许小幅下跌,但不能是明显的空头趋势K线
|
||
if sweep_candidate_bar.close < sweep_candidate_bar.open * (1 - 0.005):
|
||
return False, 0.0, 0.0
|
||
|
||
return True, sweep_candidate_bar.low, swept_liquidity_level
|
||
|
||
return False, 0.0, 0.0
|
||
|
||
def _check_swing_exit_condition(self, history_15m: List[Bar], open_price) -> bool:
|
||
"""
|
||
检查 15分钟图上的波段出场条件(结构破坏)。
|
||
当价格跌破最近的 15分钟波段低点时,视为结构破坏,触发平仓。
|
||
"""
|
||
if len(history_15m) < 5: # 需要至少几根K线来识别波段
|
||
return False
|
||
|
||
# 识别最近的15分钟波段低点 (例如,最近5根K线内的最低点,且其左右K线更高)
|
||
swing_lows_15m = []
|
||
# 查找局部最低点
|
||
for i in range(len(history_15m) - 4, len(history_15m) - 1): # 在最近的几根K线中查找
|
||
if history_15m[i].low < history_15m[i - 1].low and \
|
||
history_15m[i].low < history_15m[i + 1].low:
|
||
swing_lows_15m.append(history_15m[i].low)
|
||
|
||
if not swing_lows_15m: # 如果没有找到波段低点
|
||
return False
|
||
|
||
last_swing_low_15m = min(swing_lows_15m) # 找到最近的(最低的)波段低点
|
||
|
||
# 获取当前 15分钟 K线(即 on_open_bar 传入的 `open_price` 对应的 K线,它还没收盘)
|
||
# 以及前一根完整的 15分钟 K线 (`prev_bar_15m`)
|
||
current_bar_15m_open = open_price
|
||
|
||
# 判断:如果当前 K线(或其开盘价)已经跌破了最近的波段低点
|
||
# 我们可以用当前K线的open_price来判断,因为在on_open_bar事件发生时,我们只有这个价格
|
||
if current_bar_15m_open < last_swing_low_15m:
|
||
self.log(f"15分钟图结构破坏:当前价格 {current_bar_15m_open:.2f} 跌破最近波段低点 {last_swing_low_15m:.2f}。")
|
||
return True
|
||
|
||
return False
|
||
|
||
def on_close_bar(self, bar: Bar):
|
||
# 这个策略主要在on_open_bar中处理逻辑,on_close_bar在这里可以不使用
|
||
self.cancel_all_pending_orders(main_symbol=self.symbol)
|