Files
NewQuant/src/strategies/smc_pure_h1_long_strategy.py

386 lines
19 KiB
Python
Raw Normal View History

2025-09-16 09:59:38 +08:00
# 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)