188 lines
8.9 KiB
Python
188 lines
8.9 KiB
Python
import numpy as np
|
||
import pandas as pd
|
||
from typing import Optional, Dict, Any, List
|
||
|
||
# 假设这些是你项目中的模块
|
||
from src.core_data import Bar, Order
|
||
from src.strategies.base_strategy import Strategy
|
||
from src.algo.TrendLine import calculate_latest_trendline_values
|
||
|
||
|
||
class TrendlineHawkesStrategy(Strategy):
|
||
"""
|
||
趋势线与霍克斯过程双重确认策略 (V4 - 终极性能版):
|
||
- 霍克斯过程和滚动分位数都实现为高效的有状态增量计算。
|
||
- 使用固定长度的Numpy数组作为滚动窗口,避免Pandas.rolling的开销和不一致性。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
context: Any,
|
||
main_symbol: str,
|
||
# ... 参数与V3完全相同 ...
|
||
trade_volume: int = 1,
|
||
order_direction: Optional[List[str]] = None,
|
||
reverse_logic: bool = False,
|
||
trendline_n: int = 50,
|
||
hawkes_kappa: float = 0.1,
|
||
hawkes_lookback: int = 50,
|
||
hawkes_entry_percent: float = 0.95,
|
||
hawkes_exit_percent: float = 0.50,
|
||
enable_log: bool = True,
|
||
):
|
||
super().__init__(context, main_symbol, enable_log)
|
||
# ... 参数赋值与V3完全相同 ...
|
||
self.main_symbol = main_symbol
|
||
self.trade_volume = trade_volume
|
||
self.order_direction = order_direction or ["BUY", "SELL"]
|
||
self.reverse_logic = reverse_logic
|
||
self.trendline_n = trendline_n
|
||
self.hawkes_kappa = hawkes_kappa
|
||
self.hawkes_lookback = hawkes_lookback
|
||
self.hawkes_entry_percent = hawkes_entry_percent
|
||
self.hawkes_exit_percent = hawkes_exit_percent
|
||
self.pos_meta: Dict[str, Dict[str, Any]] = {}
|
||
|
||
# --- 【核心修改】状态缓存重构 ---
|
||
# 只缓存上一个时间点的霍克斯强度值 (未缩放)
|
||
self._last_hawkes_unscaled: float = 0.0
|
||
# 只维护一个固定长度的滚动窗口,用于计算分位数
|
||
self._hawkes_window: np.ndarray = np.array([], dtype=np.float64)
|
||
# 衰减因子
|
||
self._hawkes_alpha = np.exp(-self.hawkes_kappa)
|
||
|
||
# ... 日志与V3相同 ...
|
||
|
||
def _initialize_state(self, initial_volumes: np.ndarray):
|
||
"""
|
||
仅在策略开始时调用一次,用于填充初始的滚动窗口。
|
||
"""
|
||
print("首次运行,正在初始化霍克斯状态和滚动窗口...")
|
||
alpha = self._hawkes_alpha
|
||
kappa = self.hawkes_kappa
|
||
|
||
# 完整计算一次历史强度,只为填充窗口
|
||
temp_hawkes_history = np.zeros_like(initial_volumes, dtype=np.float64)
|
||
if len(initial_volumes) > 0:
|
||
temp_hawkes_history[0] = initial_volumes[0] if not np.isnan(initial_volumes[0]) else 0.0
|
||
for i in range(1, len(initial_volumes)):
|
||
temp_hawkes_history[i] = temp_hawkes_history[i - 1] * alpha + (
|
||
initial_volumes[i] if not np.isnan(initial_volumes[i]) else 0.0)
|
||
|
||
# 记录最后一个点的强度值,作为下一次增量计算的起点
|
||
self._last_hawkes_unscaled = temp_hawkes_history[-1] if len(temp_hawkes_history) > 0 else 0.0
|
||
|
||
# 用历史强度值的最后 hawkes_lookback 个点来填充滚动窗口
|
||
self._hawkes_window = (temp_hawkes_history * kappa)[-self.hawkes_lookback:]
|
||
print("状态初始化完成。")
|
||
|
||
def _update_state_incrementally(self, latest_volume: float):
|
||
"""
|
||
【增量计算】在每个新的Bar上调用,更新强度值和滚动窗口。
|
||
"""
|
||
# 1. 计算最新的霍克斯强度值 (未缩放)
|
||
new_hawkes_unscaled = self._last_hawkes_unscaled * self._hawkes_alpha + (
|
||
latest_volume if not np.isnan(latest_volume) else 0.0)
|
||
|
||
# 2. 更新上一个点的状态,为下一次计算做准备
|
||
self._last_hawkes_unscaled = new_hawkes_unscaled
|
||
|
||
# 3. 将新的缩放后的强度值推入滚动窗口
|
||
new_hawkes_scaled = new_hawkes_unscaled * self.hawkes_kappa
|
||
|
||
# np.roll 会高效地将数组元素移动,然后我们将新值放在第一个位置
|
||
# 这比 append + delete 的效率高得多
|
||
self._hawkes_window = np.roll(self._hawkes_window, -1)
|
||
self._hawkes_window[-1] = new_hawkes_scaled
|
||
|
||
def on_init(self):
|
||
super().on_init()
|
||
self.pos_meta.clear()
|
||
# 重置状态
|
||
self._last_hawkes_unscaled = 0.0
|
||
self._hawkes_window = np.array([], dtype=np.float64)
|
||
|
||
def on_open_bar(self, open_price: float, symbol: str):
|
||
bar_history = self.get_bar_history()
|
||
min_bars_required = max(self.trendline_n + 2, self.hawkes_lookback + 2)
|
||
if len(bar_history) < min_bars_required:
|
||
return
|
||
|
||
# --- 【核心修改】霍克斯过程的状态更新 ---
|
||
# 检查是否是第一次运行
|
||
if self._hawkes_window.size == 0:
|
||
initial_volumes = np.array([b.volume for b in bar_history], dtype=float)
|
||
self._initialize_state(initial_volumes[:-1]) # 用到上一根bar为止的数据初始化
|
||
|
||
# 增量更新当前bar的状态
|
||
self._update_state_incrementally(float(bar_history[-1].volume))
|
||
|
||
# --- 后续逻辑使用更新后的状态进行计算 ---
|
||
self.cancel_all_pending_orders(symbol)
|
||
pos = self.get_current_positions().get(symbol, 0)
|
||
|
||
# 【核心修改】直接在固定长度的窗口上计算分位数
|
||
# 这比pandas.rolling快几个数量级,且结果稳定
|
||
latest_hawkes_value = self._hawkes_window[-1]
|
||
latest_hawkes_upper = np.quantile(self._hawkes_window, self.hawkes_entry_percent)
|
||
latest_hawkes_lower = np.quantile(self._hawkes_window, self.hawkes_exit_percent)
|
||
|
||
# 1. 平仓逻辑 (完全不变)
|
||
meta = self.pos_meta.get(symbol)
|
||
if meta and pos != 0:
|
||
if latest_hawkes_value < latest_hawkes_lower:
|
||
self.log(f"霍克斯出场信号...") # 日志简化
|
||
self.send_market_order("CLOSE_LONG" if meta['direction'] == "BUY" else "CLOSE_SHORT", abs(pos))
|
||
del self.pos_meta[symbol]
|
||
return
|
||
|
||
# 2. 开仓逻辑 (完全不变)
|
||
if pos == 0:
|
||
close_prices = np.array([b.close for b in bar_history])
|
||
prices_for_trendline = close_prices[-self.trendline_n - 1:-1]
|
||
trend_upper, trend_lower = calculate_latest_trendline_values(prices_for_trendline)
|
||
|
||
if trend_upper is not None and trend_lower is not None:
|
||
prev_close = bar_history[-2].close
|
||
last_close = bar_history[-1].close
|
||
upper_break_event = last_close > trend_upper and prev_close < trend_upper
|
||
lower_break_event = last_close < trend_lower and prev_close > trend_lower
|
||
hawkes_confirmation = latest_hawkes_value > latest_hawkes_upper
|
||
|
||
if hawkes_confirmation and (upper_break_event or lower_break_event):
|
||
trade_direction = None
|
||
if upper_break_event:
|
||
trade_direction = "SELL" if self.reverse_logic else "BUY"
|
||
elif lower_break_event:
|
||
trade_direction = "BUY" if self.reverse_logic else "SELL"
|
||
|
||
if trade_direction and trade_direction in self.order_direction:
|
||
self.log(f"开仓信号确认...") # 日志简化
|
||
self.send_open_order(trade_direction, open_price, self.trade_volume)
|
||
|
||
# send_open_order, send_market_order, on_rollover 等方法保持不变
|
||
# ... (代码省略,与之前版本相同) ...
|
||
|
||
# send_open_order, send_market_order, on_rollover 等方法保持不变
|
||
def send_open_order(self, direction: str, entry_price: float, volume: int):
|
||
current_time = self.get_current_time()
|
||
order_id = f"{self.symbol}_{direction}_{current_time.strftime('%Y%m%d%H%M%S')}"
|
||
order_direction = "BUY" if direction == "BUY" else "SELL"
|
||
order = Order(id=order_id, symbol=self.symbol, direction=order_direction, volume=volume, price_type="MARKET",
|
||
submitted_time=current_time, offset="OPEN")
|
||
self.send_order(order)
|
||
self.pos_meta[self.symbol] = {"direction": direction, "volume": volume, "entry_price": entry_price}
|
||
self.log(f"发送开仓订单: {direction} {volume}手 @ Market Price (执行价约 {entry_price:.2f})")
|
||
|
||
def send_market_order(self, direction: str, volume: int):
|
||
current_time = self.get_current_time()
|
||
order_id = f"{self.symbol}_{direction}_{current_time.strftime('%Y%m%d%H%M%S')}"
|
||
order = Order(id=order_id, symbol=self.symbol, direction=direction, volume=volume, price_type="MARKET",
|
||
submitted_time=current_time, offset="CLOSE")
|
||
self.send_order(order)
|
||
self.log(f"发送平仓订单: {direction} {volume}手 @ Market Price")
|
||
|
||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||
super().on_rollover(old_symbol, new_symbol)
|
||
self.cancel_all_pending_orders(new_symbol)
|
||
self.pos_meta.clear() |