# 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)