1、vp策略
This commit is contained in:
@@ -76,4 +76,162 @@ def calculate_latest_trendline_values(prices: np.ndarray) -> Tuple[Optional[floa
|
||||
lower_intercept = low_point_price - best_lower_slope * low_point_idx
|
||||
latest_lower_value = best_lower_slope * (n - 1) + lower_intercept
|
||||
|
||||
return latest_upper_value, latest_lower_value
|
||||
return latest_upper_value, latest_lower_value
|
||||
|
||||
|
||||
def calculate_latest_trendline_values_v2(prices: np.ndarray) -> Tuple[Optional[float], Optional[float]]:
|
||||
"""
|
||||
【V3 最终修正版】
|
||||
根据给定的价格序列,仅计算并返回上、下趋势线在最后一个点的值。
|
||||
优化点:通过从两端向中间搜索凸包/凹包顶点的方式进行剪枝。
|
||||
"""
|
||||
n = len(prices)
|
||||
if n < 2:
|
||||
return None, None
|
||||
|
||||
x = np.arange(n)
|
||||
|
||||
# --- 计算上趋势线 ---
|
||||
high_point_idx = np.argmax(prices)
|
||||
high_point_price = prices[high_point_idx]
|
||||
best_upper_slope = None
|
||||
min_upper_distance_sum = float('inf')
|
||||
|
||||
# --- 修正点: 从最左侧向最高点搜索 ---
|
||||
left_max_price = -1.0
|
||||
for i in range(high_point_idx): # 遍历最高点左侧的所有点
|
||||
if prices[i] > left_max_price:
|
||||
# 这是一个候选点,进行计算
|
||||
candidate_slope = (high_point_price - prices[i]) / (high_point_idx - i)
|
||||
intercept = high_point_price - candidate_slope * high_point_idx
|
||||
candidate_line = candidate_slope * x + intercept
|
||||
if np.all(candidate_line >= prices - 1e-9):
|
||||
distance_sum = np.sum(candidate_line - prices)
|
||||
if distance_sum < min_upper_distance_sum:
|
||||
min_upper_distance_sum = distance_sum
|
||||
best_upper_slope = candidate_slope
|
||||
# 更新左侧迄今为止的最高点
|
||||
left_max_price = prices[i]
|
||||
|
||||
# --- 修正点: 从最右侧向最高点搜索 ---
|
||||
right_max_price = -1.0
|
||||
for i in range(n - 1, high_point_idx, -1): # 遍历最高点右侧的所有点
|
||||
if prices[i] > right_max_price:
|
||||
# 这是一个候选点,进行计算
|
||||
candidate_slope = (prices[i] - high_point_price) / (i - high_point_idx)
|
||||
intercept = high_point_price - candidate_slope * high_point_idx
|
||||
candidate_line = candidate_slope * x + intercept
|
||||
if np.all(candidate_line >= prices - 1e-9):
|
||||
distance_sum = np.sum(candidate_line - prices)
|
||||
if distance_sum < min_upper_distance_sum:
|
||||
min_upper_distance_sum = distance_sum
|
||||
best_upper_slope = candidate_slope
|
||||
# 更新右侧迄今为止的最高点
|
||||
right_max_price = prices[i]
|
||||
|
||||
if best_upper_slope is None:
|
||||
# 如果循环没有找到任何有效的线(例如,只有一个点或所有点在一条直线上)
|
||||
# 这种情况很少见,但为了稳健性,可以默认水平线
|
||||
best_upper_slope = 0.0
|
||||
|
||||
upper_intercept = high_point_price - best_upper_slope * high_point_idx
|
||||
latest_upper_value = best_upper_slope * (n - 1) + upper_intercept
|
||||
|
||||
# --- 计算下趋势线 (逻辑对称) ---
|
||||
low_point_idx = np.argmin(prices)
|
||||
low_point_price = prices[low_point_idx]
|
||||
best_lower_slope = None
|
||||
min_lower_distance_sum = float('inf')
|
||||
|
||||
# --- 修正点: 从最左侧向最低点搜索 ---
|
||||
left_min_price = float('inf')
|
||||
for i in range(low_point_idx):
|
||||
if prices[i] < left_min_price:
|
||||
candidate_slope = (low_point_price - prices[i]) / (low_point_idx - i)
|
||||
intercept = low_point_price - candidate_slope * low_point_idx
|
||||
candidate_line = candidate_slope * x + intercept
|
||||
if np.all(candidate_line <= prices + 1e-9):
|
||||
distance_sum = np.sum(prices - candidate_line)
|
||||
if distance_sum < min_lower_distance_sum:
|
||||
min_lower_distance_sum = distance_sum
|
||||
best_lower_slope = candidate_slope
|
||||
left_min_price = prices[i]
|
||||
|
||||
# --- 修正点: 从最右侧向最低点搜索 ---
|
||||
right_min_price = float('inf')
|
||||
for i in range(n - 1, low_point_idx, -1):
|
||||
if prices[i] < right_min_price:
|
||||
candidate_slope = (prices[i] - low_point_price) / (i - low_point_idx)
|
||||
intercept = low_point_price - candidate_slope * low_point_idx
|
||||
candidate_line = candidate_slope * x + intercept
|
||||
if np.all(candidate_line <= prices + 1e-9):
|
||||
distance_sum = np.sum(prices - candidate_line)
|
||||
if distance_sum < min_lower_distance_sum:
|
||||
min_lower_distance_sum = distance_sum
|
||||
best_lower_slope = candidate_slope
|
||||
right_min_price = prices[i]
|
||||
|
||||
if best_lower_slope is None:
|
||||
best_lower_slope = 0.0
|
||||
|
||||
lower_intercept = low_point_price - best_lower_slope * low_point_idx
|
||||
latest_lower_value = best_lower_slope * (n - 1) + lower_intercept
|
||||
|
||||
return latest_upper_value, latest_lower_value
|
||||
# ==============================================================================
|
||||
# 验证代码
|
||||
# ==============================================================================
|
||||
if __name__ == '__main__':
|
||||
import timeit
|
||||
from tqdm import tqdm
|
||||
|
||||
for i in tqdm(range(1000)):
|
||||
# 1. 生成一段模拟的价格序列
|
||||
np.random.seed(42)
|
||||
n_points = 200 # 使用一个较大的值来体现性能差异
|
||||
base_prices = 100 + np.cumsum(np.random.randn(n_points)) * 0.5
|
||||
noise = np.random.uniform(-1, 1, n_points)
|
||||
sample_prices = base_prices + noise
|
||||
|
||||
# print(f"--- 验证开始 (数据点: {n_points}) ---")
|
||||
|
||||
# 2. 调用 V1 和 V2 版本
|
||||
v1_upper, v1_lower = calculate_latest_trendline_values(sample_prices)
|
||||
v2_upper, v2_lower = calculate_latest_trendline_values_v2(sample_prices)
|
||||
|
||||
# print(f"V1 结果: Upper={v1_upper:.4f}, Lower={v1_lower:.4f}")
|
||||
# print(f"V2 结果: Upper={v2_upper:.4f}, Lower={v2_lower:.4f}")
|
||||
|
||||
# 3. 比对结果
|
||||
# 使用 np.isclose 来处理浮点数的微小误差
|
||||
results_match = np.isclose(v1_upper, v2_upper) and np.isclose(v1_lower, v2_lower)
|
||||
# print(f"\n结果是否一致: {results_match}")
|
||||
if not results_match:
|
||||
print("警告:V1 和 V2 版本计算结果不一致,请检查算法逻辑!")
|
||||
quit(-1)
|
||||
|
||||
|
||||
# 确保 timeit 可以访问到函数和数据
|
||||
setup_code = """
|
||||
import numpy as np
|
||||
from __main__ import calculate_latest_trendline_values, calculate_latest_trendline_values_v2, sample_prices
|
||||
"""
|
||||
|
||||
v1_time = timeit.timeit(
|
||||
"calculate_latest_trendline_values(sample_prices)",
|
||||
setup=setup_code,
|
||||
number=1000
|
||||
)
|
||||
|
||||
v2_time = timeit.timeit(
|
||||
"calculate_latest_trendline_values_v2(sample_prices)",
|
||||
setup=setup_code,
|
||||
number=1000
|
||||
)
|
||||
|
||||
print(f"V1 (原始) 版本总耗时: {v1_time:.6f} 秒")
|
||||
print(f"V2 (优化) 版本总耗时: {v2_time:.6f} 秒")
|
||||
|
||||
if v2_time > 0:
|
||||
speedup = v1_time / v2_time
|
||||
print(f"\n性能提升: V2 版本比 V1 版本快 {speedup:.2f} 倍")
|
||||
@@ -1,10 +1,14 @@
|
||||
# src/core_data.py
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from typing import Dict, Any, List, Optional
|
||||
import uuid # 用于生成唯一订单ID
|
||||
|
||||
from scipy.stats import skew, kurtosis
|
||||
|
||||
|
||||
@dataclass()
|
||||
class Bar:
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib # 【新增】导入 TA-Lib 库
|
||||
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_v2
|
||||
|
||||
|
||||
# --- 【核心修改】使用 TA-Lib 重新实现 ATR 计算函数 ---
|
||||
def calculate_atr_with_talib(bars: List[Bar], period: int) -> Optional[float]:
|
||||
"""
|
||||
使用 TA-Lib 库计算最新的ATR值。
|
||||
:param bars: K线历史数据列表。
|
||||
:param period: ATR计算周期。
|
||||
:return: 最新的ATR值,如果数据不足则返回None。
|
||||
"""
|
||||
# TA-Lib 需要足够的数据来计算,其输出数组的前 `period-1` 个会是 NaN
|
||||
if len(bars) < period:
|
||||
return None
|
||||
|
||||
# 从Bar对象列表中提取Numpy数组,这是TA-Lib需要的格式
|
||||
highs = np.array([b.high for b in bars], dtype=float)
|
||||
lows = np.array([b.low for b in bars], dtype=float)
|
||||
closes = np.array([b.close for b in bars], dtype=float)
|
||||
|
||||
# 调用 talib.ATR 函数
|
||||
atr_values = talib.ATR(highs, lows, closes, timeperiod=period)
|
||||
|
||||
# 提取并返回最后一个非NaN的ATR值
|
||||
latest_atr = atr_values[-1]
|
||||
|
||||
# 如果最后一个值是NaN(可能因为数据窗口刚满足要求),则认为无效
|
||||
if np.isnan(latest_atr):
|
||||
return None
|
||||
|
||||
return latest_atr
|
||||
|
||||
|
||||
class DualModeTrendlineHawkesStrategy(Strategy):
|
||||
"""
|
||||
趋势线与霍克斯过程双模式策略 (V5.2 - 趋势/回归自适应版 + Talib ATR止损):
|
||||
- 支持两套独立的参数配置,包括为每个模式分别配置ATR止损参数。
|
||||
- 【核心修改】使用行业标准的 TA-Lib 库来计算ATR,取代手动实现。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
trade_volume: int = 1,
|
||||
trend_params: Dict[str, Any] = None,
|
||||
reversion_params: Dict[str, Any] = None,
|
||||
enabled_modes: Optional[List[str]] = None,
|
||||
conflict_resolution: str = 'TREND_PRIORITY',
|
||||
enable_log: bool = True,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.main_symbol = main_symbol
|
||||
self.trade_volume = trade_volume
|
||||
|
||||
# 参数结构化 (与之前版本相同)
|
||||
default_params = {
|
||||
"order_direction": ["BUY", "SELL"],
|
||||
"trendline_n": 50,
|
||||
"hawkes_kappa": 0.1,
|
||||
"hawkes_lookback": 50,
|
||||
"hawkes_entry_percent": 0.95,
|
||||
"hawkes_exit_percent": 0.50,
|
||||
"enable_atr_stop_loss": True,
|
||||
"atr_period": 14,
|
||||
"atr_multiplier": 1.0,
|
||||
}
|
||||
self.trend_params = default_params.copy()
|
||||
if trend_params:
|
||||
self.trend_params.update(trend_params)
|
||||
|
||||
self.reversion_params = default_params.copy()
|
||||
if reversion_params:
|
||||
self.reversion_params.update(reversion_params)
|
||||
|
||||
self.enabled_modes = enabled_modes or ['TREND', 'REVERSION']
|
||||
self.conflict_resolution = conflict_resolution
|
||||
self.pos_meta: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# 状态管理 (与之前版本相同)
|
||||
self._trend_last_hawkes_unscaled: float = 0.0
|
||||
self._trend_hawkes_window: np.ndarray = np.array([], dtype=np.float64)
|
||||
self._reversion_last_hawkes_unscaled: float = 0.0
|
||||
self._reversion_hawkes_window: np.ndarray = np.array([], dtype=np.float64)
|
||||
|
||||
print("DualModeTrendlineHawkesStrategy initialized with Talib ATR Stop-Loss module.")
|
||||
# ... (其余初始化代码与之前版本相同) ...
|
||||
|
||||
# 状态初始化和更新辅助函数 (与之前版本相同)
|
||||
def _initialize_hawkes_state(self, params: Dict, initial_volumes: np.ndarray) -> (float, np.ndarray):
|
||||
alpha = np.exp(-params['hawkes_kappa'])
|
||||
kappa = params['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)
|
||||
last_hawkes_unscaled = temp_hawkes_history[-1] if len(temp_hawkes_history) > 0 else 0.0
|
||||
hawkes_window = (temp_hawkes_history * kappa)[-params['hawkes_lookback']:]
|
||||
return last_hawkes_unscaled, hawkes_window
|
||||
|
||||
def _update_hawkes_state_incrementally(self, params: Dict, latest_volume: float, last_unscaled: float,
|
||||
window: np.ndarray) -> (float, np.ndarray):
|
||||
alpha = np.exp(-params['hawkes_kappa'])
|
||||
kappa = params['hawkes_kappa']
|
||||
new_hawkes_unscaled = last_unscaled * alpha + (latest_volume if not np.isnan(latest_volume) else 0.0)
|
||||
new_hawkes_scaled = new_hawkes_unscaled * kappa
|
||||
new_window = np.roll(window, -1)
|
||||
new_window[-1] = new_hawkes_scaled
|
||||
return new_hawkes_unscaled, new_window
|
||||
|
||||
def on_init(self):
|
||||
super().on_init()
|
||||
self.pos_meta.clear()
|
||||
self._trend_last_hawkes_unscaled = 0.0
|
||||
self._trend_hawkes_window = np.array([], dtype=np.float64)
|
||||
self._reversion_last_hawkes_unscaled = 0.0
|
||||
self._reversion_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.trend_params['trendline_n'] + 2, self.trend_params['hawkes_lookback'] + 2,
|
||||
self.reversion_params['trendline_n'] + 2, self.reversion_params['hawkes_lookback'] + 2,
|
||||
self.trend_params['atr_period'] + 1 if self.trend_params['enable_atr_stop_loss'] else 0,
|
||||
self.reversion_params['atr_period'] + 1 if self.reversion_params['enable_atr_stop_loss'] else 0,
|
||||
)
|
||||
if len(bar_history) < min_bars_required:
|
||||
return
|
||||
|
||||
pos = self.get_current_positions().get(symbol, 0)
|
||||
meta = self.pos_meta.get(symbol)
|
||||
|
||||
# ATR 止损检查逻辑 (与之前版本相同)
|
||||
if pos != 0 and meta and 'stop_loss_price' in meta:
|
||||
strategy_mode = meta.get('strategy_mode')
|
||||
params_to_use = self.trend_params if strategy_mode == 'TREND' else self.reversion_params
|
||||
if params_to_use['enable_atr_stop_loss']:
|
||||
latest_bar = bar_history[-1]
|
||||
stop_loss_price = meta['stop_loss_price']
|
||||
stop_triggered = False
|
||||
if meta['direction'] == "BUY" and latest_bar.low <= stop_loss_price:
|
||||
stop_triggered = True
|
||||
self.log(
|
||||
f"[{strategy_mode}模式] ATR止损触发 (多头): 最新Low {latest_bar.low:.2f} <= 止损价 {stop_loss_price:.2f}")
|
||||
elif meta['direction'] == "SELL" and latest_bar.high >= stop_loss_price:
|
||||
stop_triggered = True
|
||||
self.log(
|
||||
f"[{strategy_mode}模式] ATR止损触发 (空头): 最新High {latest_bar.high:.2f} >= 止损价 {stop_loss_price:.2f}")
|
||||
if stop_triggered:
|
||||
self.send_market_order("CLOSE_LONG" if meta['direction'] == "BUY" else "CLOSE_SHORT", abs(pos))
|
||||
del self.pos_meta[symbol]
|
||||
return
|
||||
|
||||
# 状态初始化与更新、平仓和开仓逻辑 (与之前版本完全相同)
|
||||
# ... (代码省略) ...
|
||||
# 状态初始化与更新 (与之前相同)
|
||||
if self._trend_hawkes_window.size == 0 and 'TREND' in self.enabled_modes:
|
||||
initial_volumes = np.array([b.volume for b in bar_history], dtype=float)
|
||||
self._trend_last_hawkes_unscaled, self._trend_hawkes_window = self._initialize_hawkes_state(
|
||||
self.trend_params, initial_volumes[:-1])
|
||||
if self._reversion_hawkes_window.size == 0 and 'REVERSION' in self.enabled_modes:
|
||||
initial_volumes = np.array([b.volume for b in bar_history], dtype=float)
|
||||
self._reversion_last_hawkes_unscaled, self._reversion_hawkes_window = self._initialize_hawkes_state(
|
||||
self.reversion_params, initial_volumes[:-1])
|
||||
|
||||
latest_volume = float(bar_history[-1].volume)
|
||||
if 'TREND' in self.enabled_modes:
|
||||
self._trend_last_hawkes_unscaled, self._trend_hawkes_window = self._update_hawkes_state_incrementally(
|
||||
self.trend_params, latest_volume, self._trend_last_hawkes_unscaled, self._trend_hawkes_window)
|
||||
if 'REVERSION' in self.enabled_modes:
|
||||
self._reversion_last_hawkes_unscaled, self._reversion_hawkes_window = self._update_hawkes_state_incrementally(
|
||||
self.reversion_params, latest_volume, self._reversion_last_hawkes_unscaled,
|
||||
self._reversion_hawkes_window)
|
||||
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
|
||||
# 1. 霍克斯平仓逻辑 (与之前相同)
|
||||
if meta and pos != 0:
|
||||
strategy_mode = meta.get('strategy_mode')
|
||||
params_to_use = self.trend_params if strategy_mode == 'TREND' else self.reversion_params
|
||||
window_to_use = self._trend_hawkes_window if strategy_mode == 'TREND' else self._reversion_hawkes_window
|
||||
if window_to_use.size > 0:
|
||||
latest_hawkes_value = window_to_use[-1]
|
||||
latest_hawkes_lower = np.quantile(window_to_use, params_to_use['hawkes_exit_percent'])
|
||||
if latest_hawkes_value < latest_hawkes_lower:
|
||||
self.log(f"[{strategy_mode}模式] 霍克斯出场信号触发,平仓。")
|
||||
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:
|
||||
trend_signal = self._calculate_entry_signal('TREND', bar_history, self.trend_params,
|
||||
self._trend_hawkes_window) if 'TREND' in self.enabled_modes else None
|
||||
reversion_signal = self._calculate_entry_signal('REVERSION', bar_history, self.reversion_params,
|
||||
self._reversion_hawkes_window) if 'REVERSION' in self.enabled_modes else None
|
||||
final_direction, winning_mode = None, None
|
||||
if trend_signal and reversion_signal:
|
||||
self.log(f"信号冲突:趋势模式 ({trend_signal}) vs 回归模式 ({reversion_signal})")
|
||||
if self.conflict_resolution == 'TREND_PRIORITY':
|
||||
final_direction, winning_mode = trend_signal, 'TREND'
|
||||
elif self.conflict_resolution == 'REVERSION_PRIORITY':
|
||||
final_direction, winning_mode = reversion_signal, 'REVERSION'
|
||||
else:
|
||||
self.log("冲突解决策略为'NONE',本次不开仓。")
|
||||
elif trend_signal:
|
||||
final_direction, winning_mode = trend_signal, 'TREND'
|
||||
elif reversion_signal:
|
||||
final_direction, winning_mode = reversion_signal, 'REVERSION'
|
||||
if final_direction and winning_mode:
|
||||
params_to_use = self.trend_params if winning_mode == 'TREND' else self.reversion_params
|
||||
if final_direction in params_to_use['order_direction']:
|
||||
self.log(f"[{winning_mode}模式] 开仓信号确认: {final_direction}")
|
||||
self.send_open_order(final_direction, open_price, self.trade_volume, winning_mode)
|
||||
|
||||
def _calculate_entry_signal(self, mode: str, bar_history: List[Bar], params: Dict, hawkes_window: np.ndarray) -> \
|
||||
Optional[str]:
|
||||
# (与之前版本相同)
|
||||
if hawkes_window.size == 0: return None
|
||||
hawkes_confirmation = hawkes_window[-1] > np.quantile(hawkes_window, params['hawkes_entry_percent'])
|
||||
if not hawkes_confirmation: return None
|
||||
close_prices = np.array([b.close for b in bar_history])
|
||||
prices_for_trendline = close_prices[-params['trendline_n'] - 1:-1]
|
||||
trend_upper, trend_lower = calculate_latest_trendline_values_v2(prices_for_trendline)
|
||||
if trend_upper is not None and trend_lower is not None:
|
||||
prev_close, last_close = bar_history[-2].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
|
||||
if upper_break_event: return "BUY" if mode == 'TREND' else "SELL"
|
||||
if lower_break_event: return "SELL" if mode == 'TREND' else "BUY"
|
||||
return None
|
||||
|
||||
def send_open_order(self, direction: str, entry_price: float, volume: int, strategy_mode: str):
|
||||
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="BUY" if direction == "BUY" else "SELL", 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,
|
||||
"strategy_mode": strategy_mode}
|
||||
self.log(f"发送开仓订单 ({strategy_mode}): {direction} {volume}手 @ Market Price (执行价约 {entry_price:.2f})")
|
||||
|
||||
# --- 【核心修改】调用新的 `calculate_atr_with_talib` 函数 ---
|
||||
params_to_use = self.trend_params if strategy_mode == 'TREND' else self.reversion_params
|
||||
if params_to_use['enable_atr_stop_loss']:
|
||||
bar_history = self.get_bar_history()
|
||||
atr_period = params_to_use['atr_period']
|
||||
atr_multiplier = params_to_use['atr_multiplier']
|
||||
|
||||
# 使用 talib 版本的函数进行计算
|
||||
latest_atr = calculate_atr_with_talib(bar_history, atr_period)
|
||||
|
||||
if latest_atr is not None:
|
||||
stop_loss_distance = latest_atr * atr_multiplier
|
||||
stop_loss_price = entry_price - stop_loss_distance if direction == "BUY" else entry_price + stop_loss_distance
|
||||
self.pos_meta[self.symbol]['stop_loss_price'] = stop_loss_price
|
||||
self.log(
|
||||
f" - [{strategy_mode}模式] Talib ATR({atr_period}) = {latest_atr:.2f}, 止损乘数 = {atr_multiplier}")
|
||||
self.log(f" - [{strategy_mode}模式] 设置止损位于: {stop_loss_price:.2f}")
|
||||
else:
|
||||
self.log(f" - [{strategy_mode}模式] 警告: Talib ATR计算失败,数据不足,未设置止损。")
|
||||
# --------------------------------------------------------
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,259 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib
|
||||
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, calculate_latest_trendline_values_v2
|
||||
|
||||
|
||||
# --- ATR 计算辅助函数 (TA-Lib 实现,保持不变) ---
|
||||
def calculate_atr(bars: List[Bar], period: int) -> Optional[float]:
|
||||
"""
|
||||
使用 TA-Lib 库计算最新的ATR值。
|
||||
"""
|
||||
if len(bars) < period + 1:
|
||||
return None
|
||||
highs = np.array([b.high for b in bars], dtype=np.double)
|
||||
lows = np.array([b.low for b in bars], dtype=np.double)
|
||||
closes = np.array([b.close for b in bars], dtype=np.double)
|
||||
atr_values = talib.ATR(highs, lows, closes, timeperiod=period)
|
||||
latest_atr = atr_values[-1]
|
||||
return None if np.isnan(latest_atr) else latest_atr
|
||||
|
||||
|
||||
class TrendlineHawkesStrategy(Strategy):
|
||||
"""
|
||||
趋势线与霍克斯过程双重确认策略 (V8 - 完全无状态止损版):
|
||||
- 【核心修改】移除了所有持仓相关的状态变量(pos_meta),止损逻辑完全实时计算。
|
||||
- 实现了基于ATR的滑动止损 (Sliding Stop Loss) 功能。
|
||||
- 策略重启后无需恢复任何止损状态,具有极高的鲁棒性。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
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_atr_stop_loss: bool = True,
|
||||
atr_period: int = 14,
|
||||
atr_multiplier: float = 1,
|
||||
enable_log: bool = True,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
# --- 【修改】移除 pos_meta ---
|
||||
# self.pos_meta: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# --- 参数赋值 (保持不变) ---
|
||||
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.enable_atr_stop_loss = enable_atr_stop_loss
|
||||
self.atr_period = atr_period
|
||||
self.atr_multiplier = atr_multiplier
|
||||
|
||||
# --- 霍克斯过程的计算缓存 (保持不变) ---
|
||||
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)
|
||||
|
||||
# _initialize_state 和 _update_state_incrementally 方法保持不变
|
||||
def _initialize_state(self, initial_volumes: np.ndarray):
|
||||
# ... 内容不变 ...
|
||||
self.log("正在从历史数据重建霍克斯状态...")
|
||||
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
|
||||
self._hawkes_window = (temp_hawkes_history * kappa)[-self.hawkes_lookback:]
|
||||
self.log("霍克斯状态重建完成。")
|
||||
|
||||
def _update_state_incrementally(self, latest_volume: float):
|
||||
# ... 内容不变 ...
|
||||
new_hawkes_unscaled = self._last_hawkes_unscaled * self._hawkes_alpha + (
|
||||
latest_volume if not np.isnan(latest_volume) else 0.0)
|
||||
self._last_hawkes_unscaled = new_hawkes_unscaled
|
||||
new_hawkes_scaled = new_hawkes_unscaled * self.hawkes_kappa
|
||||
if self._hawkes_window.size < self.hawkes_lookback:
|
||||
self._hawkes_window = np.append(self._hawkes_window, new_hawkes_scaled)
|
||||
else:
|
||||
self._hawkes_window = np.roll(self._hawkes_window, -1)
|
||||
self._hawkes_window[-1] = new_hawkes_scaled
|
||||
|
||||
def on_init(self):
|
||||
"""
|
||||
【修改】策略初始化或重启时调用。
|
||||
现在不再需要恢复任何持仓状态,逻辑大大简化。
|
||||
"""
|
||||
super().on_init()
|
||||
self.log("策略正在初始化或重启...")
|
||||
|
||||
# 仅需重建霍克斯计算状态
|
||||
bar_history = self.get_bar_history()
|
||||
min_bars_for_hawkes = self.hawkes_lookback + 2
|
||||
if len(bar_history) >= min_bars_for_hawkes:
|
||||
initial_volumes = np.array([b.volume for b in bar_history], dtype=float)
|
||||
self._initialize_state(initial_volumes)
|
||||
else:
|
||||
self.log(f"历史K线数量不足 ({len(bar_history)}),无法初始化霍克斯状态。等待更多数据...")
|
||||
self._last_hawkes_unscaled = 0.0
|
||||
self._hawkes_window = np.array([], dtype=np.float64)
|
||||
|
||||
self.log("初始化完成。")
|
||||
|
||||
def _check_and_execute_atr_stop(self) -> bool:
|
||||
"""
|
||||
【新增】实时计算并检查滑动ATR止损。
|
||||
此函数完全无状态,仅依赖当前的持仓和K线数据。
|
||||
:return: 如果止损被触发并已发送平仓订单,返回 True,否则返回 False。
|
||||
"""
|
||||
# 1. 实时获取持仓
|
||||
pos = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
|
||||
# 如果不启用止损或没有持仓,直接返回
|
||||
if not self.enable_atr_stop_loss or pos == 0:
|
||||
return False
|
||||
|
||||
# 2. 实时获取计算所需数据
|
||||
bar_history = self.get_bar_history()
|
||||
latest_atr = calculate_atr(bar_history, self.atr_period)
|
||||
|
||||
# 如果无法计算ATR(数据不足),则无法执行止损
|
||||
if latest_atr is None:
|
||||
return False
|
||||
|
||||
# 3. 实时计算止损价
|
||||
latest_bar = bar_history[-1]
|
||||
stop_loss_distance = latest_atr * self.atr_multiplier
|
||||
direction = "BUY" if pos > 0 else "SELL"
|
||||
stop_triggered = False
|
||||
stop_loss_price = 0.0
|
||||
|
||||
if direction == "BUY":
|
||||
stop_loss_price = avg_entry_price - stop_loss_distance
|
||||
if latest_bar.low <= stop_loss_price:
|
||||
stop_triggered = True
|
||||
else: # SELL
|
||||
stop_loss_price = avg_entry_price + stop_loss_distance
|
||||
if latest_bar.high >= stop_loss_price:
|
||||
stop_triggered = True
|
||||
|
||||
# 打印实时计算的止损信息,便于调试
|
||||
self.log(
|
||||
f"实时计算滑动止损: 方向={direction}, 最新收盘={latest_bar.close:.2f}, ATR={latest_atr:.2f}, 止损价={stop_loss_price:.2f}")
|
||||
|
||||
# 4. 如果触发,执行平仓
|
||||
if stop_triggered:
|
||||
self.log(
|
||||
f"滑动止损触发: Low/High {latest_bar.low:.2f}/{latest_bar.high:.2f} 触及止损价 {stop_loss_price:.2f}")
|
||||
self.send_market_order("CLOSE_LONG" if direction == "BUY" else "CLOSE_SHORT", abs(pos))
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
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, self.atr_period + 2)
|
||||
if len(bar_history) < min_bars_required:
|
||||
return
|
||||
|
||||
# --- 1. 实时ATR止损检查 (最高优先级) ---
|
||||
if self._check_and_execute_atr_stop():
|
||||
return # 止损已触发并处理,结束本根bar的逻辑
|
||||
|
||||
# --- 2. 霍克斯过程的状态更新 ---
|
||||
# (这部分逻辑保持不变)
|
||||
if self._hawkes_window.size == 0:
|
||||
initial_volumes = np.array([b.volume for b in bar_history[:-1]], dtype=float)
|
||||
self._initialize_state(initial_volumes)
|
||||
self._update_state_incrementally(float(bar_history[-1].volume))
|
||||
if self._hawkes_window.size < self.hawkes_lookback:
|
||||
return
|
||||
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
|
||||
# --- 3. 计算指标 ---
|
||||
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)
|
||||
|
||||
# 【修改】实时获取持仓用于平仓和开仓判断
|
||||
pos = self.get_current_positions().get(symbol, 0)
|
||||
|
||||
# --- 4. 平仓逻辑 (基于霍克斯) ---
|
||||
if pos != 0:
|
||||
if latest_hawkes_value < latest_hawkes_lower:
|
||||
direction = "BUY" if pos > 0 else "SELL"
|
||||
self.log(f"霍克斯出场信号触发,平仓...")
|
||||
self.send_market_order("CLOSE_LONG" if direction == "BUY" else "CLOSE_SHORT", abs(pos))
|
||||
return
|
||||
|
||||
# --- 5. 开仓逻辑 ---
|
||||
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_v2(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)
|
||||
|
||||
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.log(f"发送开仓订单: {direction} {volume}手 @ Market Price (执行价约 {entry_price:.2f})")
|
||||
# 注意:开仓后不再需要设置初始止损,因为下一根bar会自动实时计算。
|
||||
|
||||
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)
|
||||
# 不再有 pos_meta 需要清空
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,328 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from typing import Optional, Dict, Any, List, Union
|
||||
import talib
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.indicators.indicators import Empty
|
||||
from src.strategies.base_strategy import Strategy
|
||||
from src.algo.TrendLine import calculate_latest_trendline_values
|
||||
|
||||
|
||||
class _SignalGenerator:
|
||||
"""
|
||||
内部帮助类,用于封装单个策略(趋势或回归)的霍克斯过程信号生成所需的所有状态和逻辑。
|
||||
"""
|
||||
|
||||
def __init__(self, hawkes_kappa: float, hawkes_lookback: int, volume_norm_n: int):
|
||||
self.hawkes_kappa = hawkes_kappa
|
||||
self.hawkes_lookback = hawkes_lookback
|
||||
self.volume_norm_n = volume_norm_n
|
||||
|
||||
# 状态变量
|
||||
self._last_hawkes_unscaled: float = 0.0
|
||||
self._hawkes_window: np.ndarray = np.array([], dtype=np.float64)
|
||||
self._hawkes_alpha: float = np.exp(-self.hawkes_kappa)
|
||||
self._volume_window: np.ndarray = np.zeros(self.volume_norm_n, dtype=np.float64)
|
||||
self._volume_sum: float = 0.0
|
||||
self._volume_sum_sq: float = 0.0
|
||||
self._volume_pointer: int = 0
|
||||
self._is_volume_window_full: bool = False
|
||||
|
||||
def reset(self):
|
||||
"""重置所有状态"""
|
||||
self._last_hawkes_unscaled = 0.0
|
||||
self._hawkes_window = np.array([], dtype=np.float64)
|
||||
self._volume_window.fill(0)
|
||||
self._volume_sum = 0.0
|
||||
self._volume_sum_sq = 0.0
|
||||
self._volume_pointer = 0
|
||||
self._is_volume_window_full = False
|
||||
|
||||
def initialize_state(self, initial_volumes: np.ndarray):
|
||||
"""用历史数据批量初始化状态"""
|
||||
normalized_volumes = []
|
||||
for vol in initial_volumes:
|
||||
self._update_volume_stats_incrementally(vol)
|
||||
mean, std = self._get_current_volume_stats()
|
||||
z_score = 0.0 if std <= 1e-9 else (vol - mean) / std
|
||||
normalized_volumes.append(z_score)
|
||||
|
||||
temp_hawkes_history = np.zeros_like(normalized_volumes, dtype=np.float64)
|
||||
if len(normalized_volumes) > 0:
|
||||
temp_hawkes_history[0] = normalized_volumes[0]
|
||||
for i in range(1, len(normalized_volumes)):
|
||||
temp_hawkes_history[i] = temp_hawkes_history[i - 1] * self._hawkes_alpha + normalized_volumes[i]
|
||||
|
||||
self._last_hawkes_unscaled = temp_hawkes_history[-1] if len(temp_hawkes_history) > 0 else 0.0
|
||||
self._hawkes_window = (temp_hawkes_history * self.hawkes_kappa)[-self.hawkes_lookback:]
|
||||
|
||||
def update_state_incrementally(self, latest_volume: float):
|
||||
"""在每个bar上增量更新状态"""
|
||||
self._update_volume_stats_incrementally(latest_volume)
|
||||
mean, std = self._get_current_volume_stats()
|
||||
normalized_volume = 0.0 if std <= 1e-9 else (latest_volume - mean) / std
|
||||
|
||||
new_hawkes_unscaled = self._last_hawkes_unscaled * self._hawkes_alpha + normalized_volume
|
||||
self._last_hawkes_unscaled = new_hawkes_unscaled
|
||||
|
||||
new_hawkes_scaled = new_hawkes_unscaled * self.hawkes_kappa
|
||||
if self._hawkes_window.size < self.hawkes_lookback:
|
||||
self._hawkes_window = np.append(self._hawkes_window, new_hawkes_scaled)
|
||||
else:
|
||||
self._hawkes_window = np.roll(self._hawkes_window, -1)
|
||||
self._hawkes_window[-1] = new_hawkes_scaled
|
||||
|
||||
def _update_volume_stats_incrementally(self, latest_volume: float):
|
||||
oldest_volume = self._volume_window[self._volume_pointer]
|
||||
self._volume_sum += latest_volume - oldest_volume
|
||||
self._volume_sum_sq += latest_volume ** 2 - oldest_volume ** 2
|
||||
self._volume_window[self._volume_pointer] = latest_volume
|
||||
self._volume_pointer = (self._volume_pointer + 1) % self.volume_norm_n
|
||||
if not self._is_volume_window_full and self._volume_pointer == 0:
|
||||
self._is_volume_window_full = True
|
||||
|
||||
def _get_current_volume_stats(self) -> (float, float):
|
||||
n = self.volume_norm_n if self._is_volume_window_full else self._volume_pointer
|
||||
if n == 0: return 0.0, 0.0
|
||||
mean = self._volume_sum / n
|
||||
variance = max(0, (self._volume_sum_sq / n) - mean ** 2)
|
||||
std = np.sqrt(variance)
|
||||
return mean, std
|
||||
|
||||
def get_latest_hawkes_value(self) -> Optional[float]:
|
||||
return self._hawkes_window[-1] if self._hawkes_window.size > 0 else None
|
||||
|
||||
def get_hawkes_quantile(self, percentile: float) -> Optional[float]:
|
||||
return np.quantile(self._hawkes_window, percentile) if self._hawkes_window.size > 0 else None
|
||||
|
||||
|
||||
class DualModeTrendlineHawkesStrategy(Strategy):
|
||||
"""
|
||||
趋势线与霍克斯过程双重确认策略 (V11 - 完全独立信号版):
|
||||
- 为趋势(Trend)和均值回归(Reversion)策略分别维护一套完全独立的信号生成器。
|
||||
- 每个策略使用各自的 trendline_n, hawkes_kappa, hawkes_lookback, volume_norm_n 参数。
|
||||
- 信号生成完全分离,确保逻辑独立性。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
trend_enabled: bool = True,
|
||||
reversion_enabled: bool = True,
|
||||
conflict_resolution_mode: str = 'trend_priority',
|
||||
trend_params: Dict[str, Any] = None,
|
||||
reversion_params: Dict[str, Any] = None,
|
||||
enable_log: bool = True,
|
||||
indicators: Union[Indicator, List[Indicator]] = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.main_symbol = main_symbol
|
||||
self.trend_enabled = trend_enabled
|
||||
self.reversion_enabled = reversion_enabled
|
||||
if conflict_resolution_mode not in ['trend_priority', 'reversion_priority', 'none']:
|
||||
raise ValueError("conflict_resolution_mode 必须是 'trend_priority', 'reversion_priority', 或 'none'")
|
||||
self.conflict_resolution_mode = conflict_resolution_mode
|
||||
|
||||
default_params = {
|
||||
"trade_volume": 1,
|
||||
"order_direction": ["BUY", "SELL"],
|
||||
"hawkes_entry_percent": 0.95,
|
||||
"hawkes_exit_percent": 0.50,
|
||||
"enable_atr_stop_loss": True,
|
||||
"atr_period": 14,
|
||||
"atr_multiplier": 2.0,
|
||||
"trendline_n": 50,
|
||||
"hawkes_kappa": 0.1,
|
||||
"hawkes_lookback": 50,
|
||||
"volume_norm_n": 50,
|
||||
}
|
||||
self.trend_params = default_params.copy()
|
||||
if trend_params: self.trend_params.update(trend_params)
|
||||
|
||||
self.reversion_params = default_params.copy()
|
||||
if reversion_params: self.reversion_params.update(reversion_params)
|
||||
|
||||
# --- 【核心修改】创建两个独立的信号生成器实例 ---
|
||||
self.trend_generator = _SignalGenerator(
|
||||
hawkes_kappa=self.trend_params['hawkes_kappa'],
|
||||
hawkes_lookback=self.trend_params['hawkes_lookback'],
|
||||
volume_norm_n=self.trend_params['volume_norm_n']
|
||||
)
|
||||
self.reversion_generator = _SignalGenerator(
|
||||
hawkes_kappa=self.reversion_params['hawkes_kappa'],
|
||||
hawkes_lookback=self.reversion_params['hawkes_lookback'],
|
||||
volume_norm_n=self.reversion_params['volume_norm_n']
|
||||
)
|
||||
|
||||
self.pos_meta: Dict[str, Dict[str, Any]] = {}
|
||||
self.indicators = indicators or [Empty(), Empty()]
|
||||
self._is_initialized = False
|
||||
|
||||
def on_init(self):
|
||||
super().on_init()
|
||||
self.pos_meta.clear()
|
||||
self.trend_generator.reset()
|
||||
self.reversion_generator.reset()
|
||||
self._is_initialized = False
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
bar_history = self.get_bar_history()
|
||||
min_bars_required = max(
|
||||
self.trend_params['trendline_n'] + 2, self.reversion_params['trendline_n'] + 2,
|
||||
self.trend_params['hawkes_lookback'] + 2, self.reversion_params['hawkes_lookback'] + 2,
|
||||
self.trend_params['volume_norm_n'] + 2, self.reversion_params['volume_norm_n'] + 2,
|
||||
self.trend_params['atr_period'] + 2, self.reversion_params['atr_period'] + 2
|
||||
)
|
||||
if len(bar_history) < min_bars_required:
|
||||
return
|
||||
|
||||
latest_volume = float(bar_history[-1].volume)
|
||||
# --- 【核心修改】初始化或更新两个独立的生成器 ---
|
||||
if not self._is_initialized:
|
||||
initial_volumes = np.array([b.volume for b in bar_history[:-1]], dtype=float)
|
||||
self.log("首次运行,正在初始化趋势信号生成器...")
|
||||
self.trend_generator.initialize_state(initial_volumes)
|
||||
self.log("正在初始化回归信号生成器...")
|
||||
self.reversion_generator.initialize_state(initial_volumes)
|
||||
self._is_initialized = True
|
||||
|
||||
self.trend_generator.update_state_incrementally(latest_volume)
|
||||
self.reversion_generator.update_state_incrementally(latest_volume)
|
||||
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
pos = self.get_current_positions().get(symbol, 0)
|
||||
|
||||
# --- 平仓逻辑 ---
|
||||
meta = self.pos_meta.get(symbol)
|
||||
if meta and pos != 0:
|
||||
strategy_type = meta.get('strategy_type', 'trend')
|
||||
# 根据开仓类型选择正确的参数和生成器
|
||||
params, generator = (self.trend_params, self.trend_generator) if strategy_type == 'trend' \
|
||||
else (self.reversion_params, self.reversion_generator)
|
||||
|
||||
latest_hawkes_value = generator.get_latest_hawkes_value()
|
||||
latest_hawkes_lower = generator.get_hawkes_quantile(params['hawkes_exit_percent'])
|
||||
|
||||
close_reason = None
|
||||
if latest_hawkes_value is not None and latest_hawkes_lower is not None and latest_hawkes_value < latest_hawkes_lower:
|
||||
close_reason = f"[{strategy_type.upper()}] 霍克斯出场信号(强度: {latest_hawkes_value:.4f} < 阈值: {latest_hawkes_lower:.4f})"
|
||||
|
||||
if params['enable_atr_stop_loss'] and 'stop_loss_price' in meta and meta['stop_loss_price'] is not None:
|
||||
if (meta['direction'] == "BUY" and bar_history[-1].close < meta['stop_loss_price']) or \
|
||||
(meta['direction'] == "SELL" and bar_history[-1].close > meta['stop_loss_price']):
|
||||
close_reason = f"[{strategy_type.upper()}] ATR止损触发"
|
||||
|
||||
if close_reason:
|
||||
self.log(close_reason)
|
||||
self.send_market_order("CLOSE_LONG" if meta['direction'] == "BUY" else "CLOSE_SHORT", abs(pos))
|
||||
if symbol in self.pos_meta: del self.pos_meta[symbol]
|
||||
return
|
||||
|
||||
# --- 开仓逻辑 ---
|
||||
if pos == 0:
|
||||
trend_signal, reversion_signal = None, None
|
||||
close_prices = np.array([b.close for b in bar_history])
|
||||
prev_close, last_close = bar_history[-2].close, bar_history[-1].close
|
||||
|
||||
# 1. 检查趋势策略信号
|
||||
if self.trend_enabled:
|
||||
prices_for_trendline = close_prices[-self.trend_params['trendline_n'] - 1:-1]
|
||||
trend_upper, trend_lower = calculate_latest_trendline_values(prices_for_trendline)
|
||||
if trend_upper is not None:
|
||||
upper_break = last_close > trend_upper and prev_close < trend_upper
|
||||
lower_break = last_close < trend_lower and prev_close > trend_lower
|
||||
|
||||
hawkes_val = self.trend_generator.get_latest_hawkes_value()
|
||||
hawkes_thresh = self.trend_generator.get_hawkes_quantile(self.trend_params['hawkes_entry_percent'])
|
||||
|
||||
if hawkes_val is not None and hawkes_thresh is not None and hawkes_val > hawkes_thresh and (
|
||||
upper_break or lower_break):
|
||||
direction = "BUY" if upper_break else "SELL"
|
||||
if direction in self.trend_params['order_direction']:
|
||||
trend_signal = direction
|
||||
|
||||
# 2. 检查均值回归策略信号
|
||||
if self.reversion_enabled:
|
||||
prices_for_trendline = close_prices[-self.reversion_params['trendline_n'] - 1:-1]
|
||||
trend_upper, trend_lower = calculate_latest_trendline_values(prices_for_trendline)
|
||||
if trend_upper is not None:
|
||||
upper_break = last_close > trend_upper and prev_close < trend_upper
|
||||
lower_break = last_close < trend_lower and prev_close > trend_lower
|
||||
|
||||
hawkes_val = self.reversion_generator.get_latest_hawkes_value()
|
||||
hawkes_thresh = self.reversion_generator.get_hawkes_quantile(
|
||||
self.reversion_params['hawkes_entry_percent'])
|
||||
|
||||
if hawkes_val is not None and hawkes_thresh is not None and hawkes_val > hawkes_thresh and (
|
||||
upper_break or lower_break):
|
||||
direction = "SELL" if upper_break else "BUY" # 方向相反
|
||||
if direction in self.reversion_params['order_direction']:
|
||||
reversion_signal = direction
|
||||
|
||||
# 3. 解决信号冲突和下单
|
||||
final_direction, final_params, strategy_type, generator = None, None, None, None
|
||||
if trend_signal and not reversion_signal:
|
||||
final_direction, final_params, strategy_type, generator = trend_signal, self.trend_params, 'trend', self.trend_generator
|
||||
elif not trend_signal and reversion_signal:
|
||||
final_direction, final_params, strategy_type, generator = reversion_signal, self.reversion_params, 'reversion', self.reversion_generator
|
||||
elif trend_signal and reversion_signal:
|
||||
self.log(
|
||||
f"开仓信号冲突: 趋势={trend_signal}, 回归={reversion_signal}. 模式: {self.conflict_resolution_mode}")
|
||||
if self.conflict_resolution_mode == 'trend_priority':
|
||||
final_direction, final_params, strategy_type, generator = trend_signal, self.trend_params, 'trend', self.trend_generator
|
||||
elif self.conflict_resolution_mode == 'reversion_priority':
|
||||
final_direction, final_params, strategy_type, generator = reversion_signal, self.reversion_params, 'reversion', self.reversion_generator
|
||||
|
||||
if final_direction:
|
||||
sl_price = None
|
||||
if final_params['enable_atr_stop_loss']:
|
||||
atr_val = self._calculate_atr(bar_history[:-1], final_params['atr_period'])
|
||||
if atr_val is not None:
|
||||
sl_price = open_price - atr_val * final_params[
|
||||
'atr_multiplier'] if final_direction == "BUY" else open_price + atr_val * final_params[
|
||||
'atr_multiplier']
|
||||
|
||||
self.log(
|
||||
f"[{strategy_type.upper()}] 开仓信号确认 (霍克斯强度: {generator.get_latest_hawkes_value():.4f} > 阈值: {generator.get_hawkes_quantile(final_params['hawkes_entry_percent']):.4f})")
|
||||
self.send_open_order(final_direction, open_price, final_params['trade_volume'], sl_price, strategy_type)
|
||||
|
||||
# --- 辅助函数 (与之前版本相同) ---
|
||||
def _calculate_atr(self, bar_history: List[Bar], period: int) -> Optional[float]:
|
||||
# ... (代码不变)
|
||||
if len(bar_history) < period + 1: return None
|
||||
highs = np.array([b.high for b in bar_history], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
atr_values = talib.ATR(highs, lows, closes, timeperiod=period)
|
||||
return atr_values[-1] if not np.isnan(atr_values[-1]) else None
|
||||
|
||||
def send_open_order(self, direction: str, entry_price: float, volume: int, stop_loss_price: Optional[float],
|
||||
strategy_type: str):
|
||||
# ... (代码不变)
|
||||
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="BUY" if direction == "BUY" else "SELL", 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,
|
||||
"stop_loss_price": stop_loss_price, "strategy_type": strategy_type}
|
||||
self.log(f"[{strategy_type.upper()}] 发送开仓订单: {direction} {volume}手")
|
||||
|
||||
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}手")
|
||||
|
||||
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()
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,284 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from typing import Optional, Dict, Any, List, Union
|
||||
import talib # <-- 【新增】导入talib库
|
||||
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.indicators.indicators import Empty
|
||||
from src.strategies.base_strategy import Strategy
|
||||
from src.algo.TrendLine import calculate_latest_trendline_values
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from typing import Optional, Dict, Any, List, Union
|
||||
import talib
|
||||
|
||||
|
||||
class TrendlineHawkesStrategy(Strategy):
|
||||
"""
|
||||
趋势线与霍克斯过程双重确认策略 (V8 - O(1) 滚动统计终极版):
|
||||
- 对交易量Z-score的计算进行了极致优化,采用增量方式维护滚动窗口的统计量。
|
||||
- 每次更新均值和标准差的计算复杂度从 O(N) 降为 O(1)。
|
||||
- 这是目前性能最高的实现方式,适用于非常高频的场景。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
# --- 所有参数与V7完全相同 ---
|
||||
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.25,
|
||||
volume_norm_n: int = 50,
|
||||
enable_atr_stop_loss: bool = True,
|
||||
atr_period: int = 14,
|
||||
atr_multiplier: float = 1.0,
|
||||
enable_log: bool = True,
|
||||
indicators: Union[Indicator, List[Indicator]] = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
# --- 参数赋值 (与V7相同) ---
|
||||
# ... (省略) ...
|
||||
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.volume_norm_n = volume_norm_n
|
||||
self.enable_atr_stop_loss = enable_atr_stop_loss
|
||||
self.atr_period = atr_period
|
||||
self.atr_multiplier = atr_multiplier
|
||||
self.pos_meta: Dict[str, Dict[str, Any]] = {}
|
||||
if indicators is None:
|
||||
indicators = [Empty(), Empty()]
|
||||
self.indicators = indicators
|
||||
|
||||
# --- 霍克斯过程状态 (与V7相同) ---
|
||||
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)
|
||||
|
||||
# --- 【核心修改】O(1) 滚动统计状态 ---
|
||||
# 预分配一个固定长度的数组作为循环缓冲区
|
||||
self._volume_window: np.ndarray = np.zeros(self.volume_norm_n, dtype=np.float64)
|
||||
self._volume_sum: float = 0.0 # 窗口内元素的和
|
||||
self._volume_sum_sq: float = 0.0 # 窗口内元素平方的和
|
||||
self._volume_pointer: int = 0 # 指向窗口中最旧元素的指针
|
||||
self._is_volume_window_full: bool = False # 窗口是否已填满的标志
|
||||
|
||||
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)
|
||||
# 【核心修改】重置所有滚动统计状态
|
||||
self._volume_window.fill(0)
|
||||
self._volume_sum = 0.0
|
||||
self._volume_sum_sq = 0.0
|
||||
self._volume_pointer = 0
|
||||
self._is_volume_window_full = False
|
||||
|
||||
# 【核心修改】_initialize_state 和 _update_state_incrementally 被重构
|
||||
def _initialize_state(self, initial_volumes: np.ndarray):
|
||||
"""
|
||||
在策略开始时调用一次,用历史数据填充所有状态。
|
||||
这个函数现在也会以增量方式填充滚动统计量。
|
||||
"""
|
||||
print("首次运行,正在以增量方式初始化所有状态...")
|
||||
|
||||
# 1. 增量填充交易量窗口并计算历史Z-score
|
||||
normalized_volumes = []
|
||||
for vol in initial_volumes:
|
||||
# 调用增量更新函数,该函数会更新窗口、和、平方和
|
||||
self._update_volume_stats_incrementally(vol)
|
||||
# 计算Z-score
|
||||
mean, std = self._get_current_volume_stats()
|
||||
z_score = 0.0
|
||||
if std > 1e-9:
|
||||
z_score = (vol - mean) / std
|
||||
normalized_volumes.append(z_score)
|
||||
|
||||
# 2. 使用标准化的交易量历史来初始化霍克斯过程 (逻辑与V7相同)
|
||||
print("正在基于标准化的交易量初始化霍克斯过程...")
|
||||
alpha = self._hawkes_alpha
|
||||
temp_hawkes_history = np.zeros_like(normalized_volumes, dtype=np.float64)
|
||||
if len(normalized_volumes) > 0:
|
||||
temp_hawkes_history[0] = normalized_volumes[0]
|
||||
for i in range(1, len(normalized_volumes)):
|
||||
temp_hawkes_history[i] = temp_hawkes_history[i - 1] * alpha + normalized_volumes[i]
|
||||
|
||||
# 3. 记录最后的状态
|
||||
self._last_hawkes_unscaled = temp_hawkes_history[-1] if len(temp_hawkes_history) > 0 else 0.0
|
||||
self._hawkes_window = (temp_hawkes_history * self.hawkes_kappa)[-self.hawkes_lookback:]
|
||||
|
||||
print("状态初始化完成。")
|
||||
|
||||
def _update_volume_stats_incrementally(self, latest_volume: float):
|
||||
"""O(1) 增量更新交易量窗口的统计数据"""
|
||||
# 获取即将被替换的最旧的元素
|
||||
oldest_volume = self._volume_window[self._volume_pointer]
|
||||
|
||||
# 更新和与平方和
|
||||
self._volume_sum += latest_volume - oldest_volume
|
||||
self._volume_sum_sq += latest_volume ** 2 - oldest_volume ** 2
|
||||
|
||||
# 在循环缓冲区中替换旧值
|
||||
self._volume_window[self._volume_pointer] = latest_volume
|
||||
|
||||
# 移动指针
|
||||
self._volume_pointer += 1
|
||||
if self._volume_pointer >= self.volume_norm_n:
|
||||
self._volume_pointer = 0
|
||||
self._is_volume_window_full = True # 窗口在指针第一次循环时被填满
|
||||
|
||||
def _get_current_volume_stats(self) -> (float, float):
|
||||
"""O(1) 获取当前的均值和标准差"""
|
||||
# 在窗口未满时,我们按实际元素数量计算
|
||||
n = self.volume_norm_n if self._is_volume_window_full else self._volume_pointer
|
||||
if n == 0:
|
||||
return 0.0, 0.0
|
||||
|
||||
mean = self._volume_sum / n
|
||||
# 为防止浮点误差导致极小的负数,使用 max(0, ...)
|
||||
variance = max(0, (self._volume_sum_sq / n) - mean ** 2)
|
||||
std = np.sqrt(variance)
|
||||
|
||||
return mean, std
|
||||
|
||||
def _update_state_incrementally(self, latest_volume: float):
|
||||
"""【重构】每个Bar上调用的主增量更新函数"""
|
||||
# 1. O(1) 更新交易量统计
|
||||
self._update_volume_stats_incrementally(latest_volume)
|
||||
|
||||
# 2. O(1) 计算最新Z-score
|
||||
mean, std = self._get_current_volume_stats()
|
||||
normalized_volume = 0.0
|
||||
if std > 1e-9:
|
||||
normalized_volume = (latest_volume - mean) / std
|
||||
|
||||
# 3. 更新霍克斯过程 (逻辑与V7相同)
|
||||
new_hawkes_unscaled = self._last_hawkes_unscaled * self._hawkes_alpha + normalized_volume
|
||||
self._last_hawkes_unscaled = new_hawkes_unscaled
|
||||
|
||||
new_hawkes_scaled = new_hawkes_unscaled * self.hawkes_kappa
|
||||
if self._hawkes_window.size < self.hawkes_lookback:
|
||||
self._hawkes_window = np.append(self._hawkes_window, new_hawkes_scaled)
|
||||
else:
|
||||
self._hawkes_window = np.roll(self._hawkes_window, -1)
|
||||
self._hawkes_window[-1] = new_hawkes_scaled
|
||||
|
||||
# on_open_bar 逻辑不变,它只负责调用 _update_state_incrementally
|
||||
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, self.volume_norm_n + 2,
|
||||
self.atr_period + 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])
|
||||
|
||||
self._update_state_incrementally(float(bar_history[-1].volume))
|
||||
|
||||
# --- 后续交易逻辑 (与V7完全相同) ---
|
||||
# ... (此处省略,代码与V7的 on_open_bar 后半部分完全一样) ...
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
pos = self.get_current_positions().get(symbol, 0)
|
||||
latest_hawkes_value = self._hawkes_window[-1]
|
||||
latest_hawkes_lower = np.quantile(self._hawkes_window, self.hawkes_exit_percent)
|
||||
meta = self.pos_meta.get(symbol)
|
||||
if meta and pos != 0:
|
||||
close_reason = None
|
||||
if latest_hawkes_value < latest_hawkes_lower:
|
||||
close_reason = f"霍克斯出场信号(强度: {latest_hawkes_value:.4f} < 阈值: {latest_hawkes_lower:.4f})"
|
||||
if self.enable_atr_stop_loss and 'stop_loss_price' in meta and meta['stop_loss_price'] is not None:
|
||||
last_close = bar_history[-1].close
|
||||
stop_loss_price = meta['stop_loss_price']
|
||||
if (meta['direction'] == "BUY" and last_close < stop_loss_price) or \
|
||||
(meta['direction'] == "SELL" and last_close > stop_loss_price):
|
||||
close_reason = f"ATR止损触发(收盘价: {last_close:.2f}, 止损价: {stop_loss_price:.2f})"
|
||||
if close_reason:
|
||||
self.log(close_reason)
|
||||
self.send_market_order("CLOSE_LONG" if meta['direction'] == "BUY" else "CLOSE_SHORT", abs(pos))
|
||||
if symbol in self.pos_meta: del self.pos_meta[symbol]
|
||||
return
|
||||
if pos == 0:
|
||||
latest_hawkes_upper = np.quantile(self._hawkes_window, self.hawkes_entry_percent)
|
||||
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, last_close = bar_history[-2].close, bar_history[-1].close
|
||||
upper_break = last_close > trend_upper and prev_close < trend_upper and self.indicators[0].is_condition_met(*self.get_indicator_tuple())
|
||||
lower_break = last_close < trend_lower and prev_close > trend_lower and self.indicators[1].is_condition_met(*self.get_indicator_tuple())
|
||||
hawkes_confirm = latest_hawkes_value > latest_hawkes_upper
|
||||
if hawkes_confirm and (upper_break or lower_break):
|
||||
direction = "BUY"
|
||||
if upper_break:
|
||||
direction = "SELL" if self.reverse_logic else "BUY"
|
||||
elif lower_break:
|
||||
direction = "BUY" if self.reverse_logic else "SELL"
|
||||
if direction in self.order_direction:
|
||||
sl_price = None
|
||||
if self.enable_atr_stop_loss:
|
||||
atr_val = self._calculate_atr(bar_history[:-1], self.atr_period)
|
||||
if atr_val is not None:
|
||||
sl_price = open_price - atr_val * self.atr_multiplier if direction == "BUY" else open_price + atr_val * self.atr_multiplier
|
||||
self.log(f"ATR({self.atr_period})={atr_val:.4f}, 止损价设置为: {sl_price:.2f}")
|
||||
self.log(
|
||||
f"开仓信号确认(霍克斯强度: {latest_hawkes_value:.4f} > 阈值: {latest_hawkes_upper:.4f})")
|
||||
self.send_open_order(direction, open_price, self.trade_volume, sl_price)
|
||||
|
||||
# ATR计算函数及其他下单函数与V7完全相同
|
||||
def _calculate_atr(self, bar_history: List[Bar], period: int) -> Optional[float]:
|
||||
if len(bar_history) < period + 1: return None
|
||||
highs = np.array([b.high for b in bar_history], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
atr_values = talib.ATR(highs, lows, closes, timeperiod=period)
|
||||
latest_atr = atr_values[-1]
|
||||
return latest_atr if not np.isnan(latest_atr) else None
|
||||
|
||||
def send_open_order(self, direction: str, entry_price: float, volume: int, stop_loss_price: Optional[float] = None):
|
||||
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,
|
||||
"stop_loss_price": stop_loss_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()
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,279 @@
|
||||
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_v2
|
||||
|
||||
|
||||
class DualModeTrendlineHawkesStrategy(Strategy):
|
||||
"""
|
||||
趋势线与霍克斯过程双模式策略 (V5 - 趋势/回归自适应版):
|
||||
- 支持两套独立的参数配置,分别对应趋势跟踪和均值回归逻辑。
|
||||
- 开平仓条件共享,但交易方向相反。
|
||||
- 内置冲突解决机制,用于处理两种模式同时发出开仓信号的情况。
|
||||
- 保持了V4版本高效的增量计算特性。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
trade_volume: int = 1,
|
||||
# 【核心修改】使用字典来配置两种模式
|
||||
trend_params: Dict[str, Any] = None,
|
||||
reversion_params: Dict[str, Any] = None,
|
||||
# 【新增】模式启用开关
|
||||
enabled_modes: Optional[List[str]] = None,
|
||||
# 【新增】信号冲突解决方案: 'TREND_PRIORITY', 'REVERSION_PRIORITY', 'NONE'
|
||||
conflict_resolution: str = 'TREND_PRIORITY',
|
||||
enable_log: bool = True,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.main_symbol = main_symbol
|
||||
self.trade_volume = trade_volume
|
||||
|
||||
# --- 【核心修改】参数结构化 ---
|
||||
# 提供默认参数,防止用户未提供
|
||||
default_params = {
|
||||
"order_direction": ["BUY", "SELL"],
|
||||
"trendline_n": 50,
|
||||
"hawkes_kappa": 0.1,
|
||||
"hawkes_lookback": 50,
|
||||
"hawkes_entry_percent": 0.95,
|
||||
"hawkes_exit_percent": 0.50,
|
||||
}
|
||||
self.trend_params = default_params.copy()
|
||||
if trend_params:
|
||||
self.trend_params.update(trend_params)
|
||||
|
||||
self.reversion_params = default_params.copy()
|
||||
if reversion_params:
|
||||
self.reversion_params.update(reversion_params)
|
||||
|
||||
self.enabled_modes = enabled_modes or ['TREND', 'REVERSION']
|
||||
self.conflict_resolution = conflict_resolution
|
||||
|
||||
self.pos_meta: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
# --- 【核心修改】为每个模式维护独立的状态 ---
|
||||
# 趋势模式状态
|
||||
self._trend_last_hawkes_unscaled: float = 0.0
|
||||
self._trend_hawkes_window: np.ndarray = np.array([], dtype=np.float64)
|
||||
self._trend_hawkes_alpha = np.exp(-self.trend_params['hawkes_kappa'])
|
||||
|
||||
# 回归模式状态
|
||||
self._reversion_last_hawkes_unscaled: float = 0.0
|
||||
self._reversion_hawkes_window: np.ndarray = np.array([], dtype=np.float64)
|
||||
self._reversion_hawkes_alpha = np.exp(-self.reversion_params['hawkes_kappa'])
|
||||
|
||||
print("DualModeTrendlineHawkesStrategy initialized.")
|
||||
print(f"Enabled modes: {self.enabled_modes}")
|
||||
print(f"Conflict resolution: {self.conflict_resolution}")
|
||||
|
||||
# --- 辅助函数,用于状态管理 (可复用) ---
|
||||
def _initialize_hawkes_state(self, params: Dict, initial_volumes: np.ndarray) -> (float, np.ndarray):
|
||||
"""根据给定参数和历史成交量,初始化霍克斯状态。"""
|
||||
print(f"Initializing Hawkes state with lookback {params['hawkes_lookback']}...")
|
||||
alpha = np.exp(-params['hawkes_kappa'])
|
||||
kappa = params['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)
|
||||
|
||||
last_hawkes_unscaled = temp_hawkes_history[-1] if len(temp_hawkes_history) > 0 else 0.0
|
||||
hawkes_window = (temp_hawkes_history * kappa)[-params['hawkes_lookback']:]
|
||||
return last_hawkes_unscaled, hawkes_window
|
||||
|
||||
def _update_hawkes_state_incrementally(self, params: Dict, latest_volume: float, last_unscaled: float,
|
||||
window: np.ndarray) -> (float, np.ndarray):
|
||||
"""根据给定参数,增量更新霍克斯状态。"""
|
||||
alpha = np.exp(-params['hawkes_kappa'])
|
||||
kappa = params['hawkes_kappa']
|
||||
|
||||
new_hawkes_unscaled = last_unscaled * alpha + (latest_volume if not np.isnan(latest_volume) else 0.0)
|
||||
new_hawkes_scaled = new_hawkes_unscaled * kappa
|
||||
|
||||
new_window = np.roll(window, -1)
|
||||
new_window[-1] = new_hawkes_scaled
|
||||
|
||||
return new_hawkes_unscaled, new_window
|
||||
|
||||
def on_init(self):
|
||||
super().on_init()
|
||||
self.pos_meta.clear()
|
||||
# 重置所有状态
|
||||
self._trend_last_hawkes_unscaled = 0.0
|
||||
self._trend_hawkes_window = np.array([], dtype=np.float64)
|
||||
self._reversion_last_hawkes_unscaled = 0.0
|
||||
self._reversion_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.trend_params['trendline_n'] + 2, self.trend_params['hawkes_lookback'] + 2,
|
||||
self.reversion_params['trendline_n'] + 2, self.reversion_params['hawkes_lookback'] + 2
|
||||
)
|
||||
if len(bar_history) < min_bars_required:
|
||||
return
|
||||
|
||||
# --- 状态初始化与更新 ---
|
||||
# 首次运行时,为两个启用的模式初始化状态
|
||||
if self._trend_hawkes_window.size == 0 and 'TREND' in self.enabled_modes:
|
||||
initial_volumes = np.array([b.volume for b in bar_history], dtype=float)
|
||||
self._trend_last_hawkes_unscaled, self._trend_hawkes_window = self._initialize_hawkes_state(
|
||||
self.trend_params, initial_volumes[:-1]
|
||||
)
|
||||
if self._reversion_hawkes_window.size == 0 and 'REVERSION' in self.enabled_modes:
|
||||
initial_volumes = np.array([b.volume for b in bar_history], dtype=float)
|
||||
self._reversion_last_hawkes_unscaled, self._reversion_hawkes_window = self._initialize_hawkes_state(
|
||||
self.reversion_params, initial_volumes[:-1]
|
||||
)
|
||||
|
||||
# 增量更新两个模式的状态
|
||||
latest_volume = float(bar_history[-1].volume)
|
||||
if 'TREND' in self.enabled_modes:
|
||||
self._trend_last_hawkes_unscaled, self._trend_hawkes_window = self._update_hawkes_state_incrementally(
|
||||
self.trend_params, latest_volume, self._trend_last_hawkes_unscaled, self._trend_hawkes_window
|
||||
)
|
||||
if 'REVERSION' in self.enabled_modes:
|
||||
self._reversion_last_hawkes_unscaled, self._reversion_hawkes_window = self._update_hawkes_state_incrementally(
|
||||
self.reversion_params, latest_volume, self._reversion_last_hawkes_unscaled,
|
||||
self._reversion_hawkes_window
|
||||
)
|
||||
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
pos = self.get_current_positions().get(symbol, 0)
|
||||
|
||||
# --- 1. 平仓逻辑 ---
|
||||
meta = self.pos_meta.get(symbol)
|
||||
if meta and pos != 0:
|
||||
strategy_mode = meta.get('strategy_mode')
|
||||
params_to_use = self.trend_params if strategy_mode == 'TREND' else self.reversion_params
|
||||
window_to_use = self._trend_hawkes_window if strategy_mode == 'TREND' else self._reversion_hawkes_window
|
||||
|
||||
if window_to_use.size > 0:
|
||||
latest_hawkes_value = window_to_use[-1]
|
||||
latest_hawkes_lower = np.quantile(window_to_use, params_to_use['hawkes_exit_percent'])
|
||||
|
||||
if latest_hawkes_value < latest_hawkes_lower:
|
||||
self.log(f"[{strategy_mode}模式] 霍克斯出场信号触发,平仓。")
|
||||
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:
|
||||
trend_signal = None
|
||||
reversion_signal = None
|
||||
|
||||
# 分别计算两个模式的信号
|
||||
if 'TREND' in self.enabled_modes:
|
||||
trend_signal = self._calculate_entry_signal(
|
||||
'TREND', bar_history, self.trend_params, self._trend_hawkes_window
|
||||
)
|
||||
if 'REVERSION' in self.enabled_modes:
|
||||
reversion_signal = self._calculate_entry_signal(
|
||||
'REVERSION', bar_history, self.reversion_params, self._reversion_hawkes_window
|
||||
)
|
||||
|
||||
final_direction = None
|
||||
winning_mode = None
|
||||
|
||||
# --- 信号冲突解决 ---
|
||||
if trend_signal and reversion_signal:
|
||||
self.log(f"信号冲突:趋势模式 ({trend_signal}) vs 回归模式 ({reversion_signal})")
|
||||
if self.conflict_resolution == 'TREND_PRIORITY':
|
||||
final_direction = trend_signal
|
||||
winning_mode = 'TREND'
|
||||
elif self.conflict_resolution == 'REVERSION_PRIORITY':
|
||||
final_direction = reversion_signal
|
||||
winning_mode = 'REVERSION'
|
||||
else: # 'NONE'
|
||||
self.log("冲突解决策略为'NONE',本次不开仓。")
|
||||
elif trend_signal:
|
||||
final_direction = trend_signal
|
||||
winning_mode = 'TREND'
|
||||
elif reversion_signal:
|
||||
final_direction = reversion_signal
|
||||
winning_mode = 'REVERSION'
|
||||
|
||||
# 执行最终决策
|
||||
if final_direction and winning_mode:
|
||||
params_to_use = self.trend_params if winning_mode == 'TREND' else self.reversion_params
|
||||
if final_direction in params_to_use['order_direction']:
|
||||
self.log(f"[{winning_mode}模式] 开仓信号确认: {final_direction}")
|
||||
self.send_open_order(final_direction, open_price, self.trade_volume, winning_mode)
|
||||
|
||||
def _calculate_entry_signal(self, mode: str, bar_history: List[Bar], params: Dict, hawkes_window: np.ndarray) -> \
|
||||
Optional[str]:
|
||||
"""计算单个模式的入场信号,返回 'BUY', 'SELL' 或 None。"""
|
||||
if hawkes_window.size == 0:
|
||||
return None
|
||||
|
||||
# 霍克斯确认
|
||||
latest_hawkes_value = hawkes_window[-1]
|
||||
latest_hawkes_upper = np.quantile(hawkes_window, params['hawkes_entry_percent'])
|
||||
hawkes_confirmation = latest_hawkes_value > latest_hawkes_upper
|
||||
|
||||
if not hawkes_confirmation:
|
||||
return None
|
||||
|
||||
# 趋势线突破事件
|
||||
close_prices = np.array([b.close for b in bar_history])
|
||||
prices_for_trendline = close_prices[-params['trendline_n'] - 1:-1]
|
||||
trend_upper, trend_lower = calculate_latest_trendline_values_v2(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
|
||||
|
||||
if upper_break_event:
|
||||
# 趋势模式:向上突破 -> 买入
|
||||
# 回归模式:向上突破 -> 卖出 (认为是假突破,价格将回归)
|
||||
return "BUY" if mode == 'TREND' else "SELL"
|
||||
elif lower_break_event:
|
||||
# 趋势模式:向下突破 -> 卖出
|
||||
# 回归模式:向下突破 -> 买入 (认为是超卖,价格将反弹)
|
||||
return "SELL" if mode == 'TREND' else "BUY"
|
||||
|
||||
return None
|
||||
|
||||
def send_open_order(self, direction: str, entry_price: float, volume: int, strategy_mode: str):
|
||||
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,
|
||||
"strategy_mode": strategy_mode
|
||||
}
|
||||
self.log(f"发送开仓订单 ({strategy_mode}): {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()
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,194 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from typing import Optional, Dict, Any, List, Union
|
||||
|
||||
# 假设这些是你项目中的模块
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.indicators.indicators import Empty
|
||||
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,
|
||||
indicators: Union[Indicator, List[Indicator]] = None,
|
||||
):
|
||||
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]] = {}
|
||||
if indicators is None:
|
||||
indicators = [Empty(), Empty()]
|
||||
self.indicators = indicators
|
||||
|
||||
# --- 【核心修改】状态缓存重构 ---
|
||||
# 只缓存上一个时间点的霍克斯强度值 (未缩放)
|
||||
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 and self.indicators[0].is_condition_met(*self.get_indicator_tuple())
|
||||
lower_break_event = last_close < trend_lower and prev_close > trend_lower and self.indicators[1].is_condition_met(*self.get_indicator_tuple())
|
||||
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()
|
||||
File diff suppressed because one or more lines are too long
9350
src/strategies/ValueMigrationStrategy/ValueMigrationStrategy.ipynb
Normal file
9350
src/strategies/ValueMigrationStrategy/ValueMigrationStrategy.ipynb
Normal file
File diff suppressed because one or more lines are too long
288
src/strategies/ValueMigrationStrategy/ValueMigrationStrategy.py
Normal file
288
src/strategies/ValueMigrationStrategy/ValueMigrationStrategy.py
Normal file
@@ -0,0 +1,288 @@
|
||||
# =====================================================================================
|
||||
# 以下是新增的 ValueMigrationStrategy 策略代码
|
||||
# =====================================================================================
|
||||
from collections import deque
|
||||
from datetime import timedelta, time
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from typing import List, Any, Optional, Dict
|
||||
|
||||
import talib
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.strategies.ValueMigrationStrategy.data_class import ProfileStats, calculate_profile_from_bars
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
# = ===================================================================
|
||||
# 全局辅助函数 (Global Helper Functions)
|
||||
# 将这些函数放在文件顶部,以便所有策略类都能调用
|
||||
# =====================================================================
|
||||
|
||||
def compute_price_volume_distribution(bars: List[Bar], tick_size: float) -> Optional[pd.Series]:
|
||||
"""
|
||||
[全局函数] 从K线数据中计算出原始的价格-成交量分布。
|
||||
"""
|
||||
if not bars:
|
||||
return None
|
||||
|
||||
data = []
|
||||
# 为了性能,我们只处理有限数量的bars,防止内存问题
|
||||
# 在实际应用中,更高效的实现是必要的
|
||||
for bar in bars[-500:]: # 添加一个安全限制
|
||||
price_range = np.arange(bar.low, bar.high + tick_size, tick_size)
|
||||
if len(price_range) == 0 or bar.volume == 0: continue
|
||||
|
||||
# 将成交量近似分布到K线覆盖的每个tick上
|
||||
volume_per_tick = bar.volume / len(price_range)
|
||||
for price in price_range:
|
||||
data.append({'price': price, 'volume': volume_per_tick})
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
df = pd.DataFrame(data)
|
||||
if df.empty:
|
||||
return None
|
||||
|
||||
return df.groupby('price')['volume'].sum().sort_index()
|
||||
|
||||
|
||||
# 确保在文件顶部导入
|
||||
from scipy.signal import find_peaks
|
||||
|
||||
|
||||
def find_hvns_with_distance(price_volume_dist: pd.Series, distance_in_ticks: int) -> List[float]:
|
||||
"""
|
||||
[全局函数] 使用峰值查找算法,根据峰值间的最小距离来识别HVNs。
|
||||
|
||||
Args:
|
||||
price_volume_dist: 价格-成交量分布序列。
|
||||
distance_in_ticks: 两个HVN之间必须间隔的最小tick数量。
|
||||
|
||||
Returns:
|
||||
一个包含所有被识别出的HVN价格的列表。
|
||||
"""
|
||||
if price_volume_dist.empty or len(price_volume_dist) < 3:
|
||||
return []
|
||||
|
||||
# distance参数确保找到的峰值之间至少相隔N个点
|
||||
peaks_indices, _ = find_peaks(price_volume_dist.values, distance=distance_in_ticks)
|
||||
|
||||
if len(peaks_indices) == 0:
|
||||
return [price_volume_dist.idxmax()] # 默认返回POC
|
||||
|
||||
hvn_prices = price_volume_dist.index[peaks_indices].tolist()
|
||||
return hvn_prices
|
||||
|
||||
|
||||
class ValueMigrationStrategy(Strategy):
|
||||
# 确保在文件顶部导入
|
||||
from scipy.signal import find_peaks
|
||||
|
||||
# =====================================================================================
|
||||
# 以下是全新的、基于HVN回测逻辑的 HVNPullbackStrategy 策略代码
|
||||
# =====================================================================================
|
||||
|
||||
"""
|
||||
一个基于动态HVN突破后回测的量化交易策略。(适配无回调函数的框架)
|
||||
|
||||
该策略首先动态识别出市场中重要的成交量密集区(HVNs)。当价格
|
||||
明确穿越一个HVN后,它并不立即追逐,而是预期价格会有一个短暂的
|
||||
回测行为,并在HVN附近的一个偏移位置挂限价单,以更高概率顺势入场。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
tick_size: float = 1,
|
||||
profile_period: int = 100,
|
||||
recalc_interval: int = 4,
|
||||
hvn_distance_ticks: int = 1,
|
||||
entry_offset_atr: float = 0.2,
|
||||
stop_loss_atr: float = 1.0,
|
||||
take_profit_atr: float = 1.0,
|
||||
atr_period: int = 14,
|
||||
order_direction=None,
|
||||
indicators=[None, None],
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None:
|
||||
order_direction = ['BUY', 'SELL']
|
||||
|
||||
self.trade_volume = trade_volume
|
||||
self.tick_size = tick_size
|
||||
self.profile_period = profile_period
|
||||
self.recalc_interval = recalc_interval
|
||||
self.hvn_distance_ticks = hvn_distance_ticks
|
||||
self.entry_offset_atr = entry_offset_atr
|
||||
self.stop_loss_atr = stop_loss_atr
|
||||
self.take_profit_atr = take_profit_atr
|
||||
self.atr_period = atr_period
|
||||
|
||||
self.order_direction = order_direction
|
||||
self.indicator_long = indicators[0]
|
||||
self.indicator_short = indicators[1]
|
||||
|
||||
self.main_symbol = main_symbol
|
||||
self.order_id_counter = 0
|
||||
|
||||
self._bar_counter = 0
|
||||
self._cached_hvns: List[float] = []
|
||||
self._last_order_id: Optional[str] = None
|
||||
|
||||
# 元数据存储:
|
||||
self.position_meta: Dict[str, Any] = {} # 存储已成交持仓的止盈止损
|
||||
self._pending_order_meta: Dict[str, Any] = {} # 存储未成交挂单的预设参数
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
self._bar_counter += 1
|
||||
bar_history = self.get_bar_history()
|
||||
|
||||
required_len = max(self.profile_period, self.atr_period) + 1
|
||||
if len(bar_history) < required_len:
|
||||
return
|
||||
|
||||
# # --- 1. 取消上一根K线未成交的限价单 ---
|
||||
# if self._last_order_id and self._last_order_id in self.get_pending_orders():
|
||||
# self.cancel_order(self._last_order_id)
|
||||
# self.log(f"已取消上一根K线的挂单: {self._last_order_id}")
|
||||
# # 如果挂单被取消,清除对应的预设元数据
|
||||
# if self._last_order_id in self._pending_order_meta:
|
||||
# del self._pending_order_meta[self._last_order_id]
|
||||
# self._last_order_id = None
|
||||
self.cancel_all_pending_orders(self.symbol)
|
||||
|
||||
# --- 2. 管理现有持仓 (逻辑核心调整) ---
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
if position_volume != 0:
|
||||
self.manage_open_position(position_volume, open_price)
|
||||
return # 有持仓则不进行新的开仓评估
|
||||
|
||||
# --- 3. 周期性地计算并缓存所有的HVNs ---
|
||||
if self._bar_counter % self.recalc_interval == 1:
|
||||
profile_bars = bar_history[-self.profile_period:]
|
||||
dist = compute_price_volume_distribution(profile_bars, self.tick_size)
|
||||
if dist is not None and not dist.empty:
|
||||
self._cached_hvns = find_hvns_with_distance(dist, self.hvn_distance_ticks)
|
||||
self.log(f"New HVNs identified at: {[f'{p:.2f}' for p in self._cached_hvns]}")
|
||||
|
||||
if not self._cached_hvns: return
|
||||
|
||||
# --- 4. 评估新机会 (穿越后挂单逻辑) ---
|
||||
self.evaluate_entry_signal(bar_history)
|
||||
|
||||
def manage_open_position(self, volume: int, current_price: float):
|
||||
"""在on_open_bar中主动管理已开仓位的止盈止损。"""
|
||||
|
||||
# [关键逻辑]: 检测是否为新成交的持仓
|
||||
if self.symbol not in self.position_meta:
|
||||
# 这是一个新持仓。我们必须从挂单的元数据中恢复止盈止损参数。
|
||||
# 这里假设只有一个挂单能成交。如果有多个,需要更复杂的匹配逻辑。
|
||||
if not self._pending_order_meta:
|
||||
self.log("Error: New position detected but no pending order meta found.")
|
||||
# 紧急情况:立即平仓或设置默认止损
|
||||
return
|
||||
|
||||
# 从挂单元数据中获取参数,并“过户”到持仓元数据
|
||||
# 由于我们每次只挂一个单,取第一个即可
|
||||
order_id = next(iter(self._pending_order_meta))
|
||||
meta = self._pending_order_meta.pop(order_id) # 取出并从pending中删除
|
||||
self.position_meta[self.symbol] = meta
|
||||
self.log(f"新持仓确认。已设置TP/SL: {meta}")
|
||||
|
||||
# [常规逻辑]: 检查止盈止损
|
||||
meta = self.position_meta[self.symbol]
|
||||
sl_price = meta['sl_price']
|
||||
tp_price = meta['tp_price']
|
||||
|
||||
if volume > 0: # 多头
|
||||
if current_price <= sl_price:
|
||||
self.log(f"多头止损触发 at {current_price:.2f}")
|
||||
self.close_position("CLOSE_LONG", abs(volume))
|
||||
elif current_price >= tp_price:
|
||||
self.log(f"多头止盈触发 at {current_price:.2f}")
|
||||
self.close_position("CLOSE_LONG", abs(volume))
|
||||
elif volume < 0: # 空头
|
||||
if current_price >= sl_price:
|
||||
self.log(f"空头止损触发 at {current_price:.2f}")
|
||||
self.close_position("CLOSE_SHORT", abs(volume))
|
||||
elif current_price <= tp_price:
|
||||
self.log(f"空头止盈触发 at {current_price:.2f}")
|
||||
self.close_position("CLOSE_SHORT", abs(volume))
|
||||
|
||||
def evaluate_entry_signal(self, bar_history: List[Bar]):
|
||||
prev_close = bar_history[-2].close
|
||||
current_close = bar_history[-1].close
|
||||
|
||||
highs = np.array([b.high for b in bar_history], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
current_atr = talib.ATR(highs, lows, closes, self.atr_period)[-1]
|
||||
if current_atr < self.tick_size: return
|
||||
|
||||
for hvn in sorted(self._cached_hvns):
|
||||
if "BUY" in self.order_direction and (prev_close < hvn < current_close):
|
||||
if self.indicator_long is None or self.indicator_long.is_condition_met(*self.get_indicator_tuple()):
|
||||
limit_price = hvn + self.entry_offset_atr * current_atr
|
||||
self.log(f"价格向上穿越HVN({hvn:.2f}). 在 {limit_price:.2f} 挂限价买单。")
|
||||
self.send_hvn_limit_order("BUY", limit_price, current_atr)
|
||||
return
|
||||
|
||||
if "SELL" in self.order_direction and (prev_close > hvn > current_close):
|
||||
if self.indicator_short is None or self.indicator_short.is_condition_met(
|
||||
*self.get_indicator_tuple()):
|
||||
limit_price = hvn - self.entry_offset_atr * current_atr
|
||||
self.log(f"价格向下穿越HVN({hvn:.2f}). 在 {limit_price:.2f} 挂限价卖单。")
|
||||
self.send_hvn_limit_order("SELL", limit_price, current_atr)
|
||||
return
|
||||
|
||||
def send_hvn_limit_order(self, direction: str, limit_price: float, entry_atr: float):
|
||||
# 预先计算止盈止损价格
|
||||
sl_price = limit_price - self.stop_loss_atr * entry_atr if direction == "BUY" else limit_price + self.stop_loss_atr * entry_atr
|
||||
tp_price = limit_price + self.take_profit_atr * entry_atr if direction == "BUY" else limit_price - self.take_profit_atr * entry_atr
|
||||
|
||||
order_id = f"{self.symbol}_{direction}_LIMIT_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 将这些参数存储到 pending_order_meta 中
|
||||
self._pending_order_meta[order_id] = {'sl_price': sl_price, 'tp_price': tp_price}
|
||||
|
||||
order = Order(
|
||||
id=order_id, symbol=self.symbol, direction=direction, volume=self.trade_volume,
|
||||
price_type="LIMIT", limit_price=limit_price, submitted_time=self.get_current_time(),
|
||||
offset="OPEN"
|
||||
)
|
||||
sent_order = self.send_order(order)
|
||||
if sent_order:
|
||||
self._last_order_id = sent_order.id
|
||||
|
||||
def close_position(self, direction: str, volume: int):
|
||||
self.send_market_order(direction, volume)
|
||||
if self.symbol in self.position_meta:
|
||||
del self.position_meta[self.symbol] # 平仓后清理持仓元数据
|
||||
|
||||
def send_market_order(self, direction: str, volume: int, offset: str = "CLOSE"):
|
||||
order_id = f"{self.symbol}_{direction}_{offset}_{self.get_current_time().strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(
|
||||
id=order_id, symbol=self.symbol, direction=direction, volume=volume,
|
||||
price_type="MARKET", submitted_time=self.get_current_time(), offset=offset
|
||||
)
|
||||
self.send_order(order)
|
||||
|
||||
|
||||
def send_market_order(self, direction: str, volume: int, offset: str = "CLOSE"):
|
||||
# ... (与之前版本相同) ...
|
||||
order_id = f"{self.symbol}_{direction}_{offset}_{self.get_current_time().strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(
|
||||
id=order_id, symbol=self.symbol, direction=direction, volume=volume,
|
||||
price_type="MARKET", submitted_time=self.get_current_time(), offset=offset
|
||||
)
|
||||
self.send_order(order)
|
||||
62
src/strategies/ValueMigrationStrategy/data_class.py
Normal file
62
src/strategies/ValueMigrationStrategy/data_class.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from dataclasses import dataclass
|
||||
from scipy.stats import kurtosis, skew
|
||||
from datetime import time, timedelta
|
||||
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
from src.core_data import Bar
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfileStats:
|
||||
"""封装剖面图的所有核心统计量。"""
|
||||
vah: float
|
||||
val: float
|
||||
poc: float
|
||||
|
||||
|
||||
def calculate_profile_from_bars(bars: List[Bar], tick_size: float, va_percentage: int = 70) -> Optional[ProfileStats]:
|
||||
"""
|
||||
[全局核心函数] 从给定的K线列表计算剖面图统计量 (VAH, VAL, POC)。
|
||||
"""
|
||||
if not bars:
|
||||
return None
|
||||
|
||||
data = []
|
||||
for bar in bars:
|
||||
price_range = np.arange(bar.low, bar.high + tick_size, tick_size)
|
||||
if len(price_range) == 0 or bar.volume == 0: continue
|
||||
volume_per_tick = bar.volume / len(price_range)
|
||||
for price in price_range:
|
||||
data.append({'price': price, 'volume': volume_per_tick})
|
||||
if not data: return None
|
||||
df = pd.DataFrame(data)
|
||||
if df.empty: return None
|
||||
price_volume_dist = df.groupby('price')['volume'].sum().sort_index()
|
||||
|
||||
if price_volume_dist.empty or len(price_volume_dist) < 3: return None
|
||||
|
||||
poc = price_volume_dist.idxmax()
|
||||
total_volume = price_volume_dist.sum()
|
||||
value_area_volume_target = total_volume * (va_percentage / 100.0)
|
||||
current_va_volume = price_volume_dist.loc[poc]
|
||||
vah, val = poc, poc
|
||||
prices_above = price_volume_dist.index[price_volume_dist.index > poc]
|
||||
prices_below = price_volume_dist.index[price_volume_dist.index < poc].sort_values(ascending=False)
|
||||
idx_above, idx_below = 0, 0
|
||||
while current_va_volume < value_area_volume_target:
|
||||
vol_above = price_volume_dist.loc[prices_above[idx_above]] if idx_above < len(prices_above) else 0
|
||||
vol_below = price_volume_dist.loc[prices_below[idx_below]] if idx_below < len(prices_below) else 0
|
||||
if vol_above == 0 and vol_below == 0: break
|
||||
if vol_above > vol_below:
|
||||
current_va_volume += vol_above
|
||||
vah = prices_above[idx_above]
|
||||
idx_above += 1
|
||||
else:
|
||||
current_va_volume += vol_below
|
||||
val = prices_below[idx_below]
|
||||
idx_below += 1
|
||||
|
||||
return ProfileStats(vah=vah, val=val, poc=poc)
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user