Files
NewQuant/src/strategies/smc_pure_h1_long_strategy.py
2025-09-16 09:59:38 +08:00

386 lines
19 KiB
Python
Raw 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.
# 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)