232 lines
11 KiB
Python
232 lines
11 KiB
Python
import numpy as np
|
|
import pandas as pd
|
|
from typing import Optional, Dict, Any, List
|
|
|
|
# 假设这些是你项目中的模块
|
|
from src.core_data import Bar, Order
|
|
from src.strategies.base_strategy import Strategy
|
|
from src.algo.TrendLine import calculate_latest_trendline_values_v2
|
|
|
|
|
|
class DualModeVolumeIntensityStrategy(Strategy):
|
|
"""
|
|
双模式成交量强度策略 (V7 - 无状态高效版):
|
|
- 【核心命名】将 "霍克斯过程" 概念替换为更准确的 "成交量强度 (Volume Intensity)"。
|
|
- 【核心优化】使用 NumPy 的卷积操作 (np.convolve) 代替循环,高效地计算滑动窗口的成交量强度,彻底消除路径依赖,同时保证高性能。
|
|
- 策略行为在任何时间点只与最近的固定窗口数据相关,保证了回测与实盘的绝对一致性。
|
|
- 完整保留了双模式(趋势/回归)和冲突解决机制。
|
|
"""
|
|
|
|
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,
|
|
# 【命名修改】参数名也同步调整
|
|
"intensity_kappa": 0.1, # 衰减因子
|
|
"intensity_lookback": 50, # 回看窗口
|
|
"intensity_entry_percent": 0.95,
|
|
"intensity_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]] = {}
|
|
|
|
print("DualModeVolumeIntensityStrategy (V7) initialized.")
|
|
print(f"Enabled modes: {self.enabled_modes}")
|
|
print(f"Conflict resolution: {self.conflict_resolution}")
|
|
print("Volume Intensity calculation is STATELESS and EFFICIENT (using np.convolve).")
|
|
|
|
# --- 【核心修改】使用卷积实现无状态、高效的窗口计算 ---
|
|
def _calculate_volume_intensity_window(self, volumes: np.ndarray, kappa: float, lookback: int) -> np.ndarray:
|
|
"""
|
|
使用一维卷积高效计算成交量强度窗口。
|
|
这是一个纯函数,无任何副作用和路径依赖。
|
|
|
|
:param volumes: 历史成交量序列。长度应为 2*lookback - 1。
|
|
:param kappa: 强度衰减因子。
|
|
:param lookback: 强度计算的回看窗口,也是返回的强度窗口的长度。
|
|
:return: 一个长度为 `lookback` 的成交量强度值窗口。
|
|
"""
|
|
# 权重是指数衰减的,越近的成交量权重越高
|
|
# weights = [exp(-kappa*lookback), ..., exp(-kappa*1)]
|
|
weights = np.exp(-kappa * np.arange(lookback, 0, -1))
|
|
|
|
# 使用'valid'模式的卷积,本质上是一个滑动的点积运算
|
|
# 结果的长度将是 len(volumes) - len(weights) + 1 = (2*lookback-1) - lookback + 1 = lookback
|
|
intensity_unscaled = np.convolve(volumes, weights, mode='valid')
|
|
|
|
return intensity_unscaled * kappa
|
|
|
|
def on_init(self):
|
|
super().on_init()
|
|
self.pos_meta.clear()
|
|
|
|
def on_open_bar(self, open_price: float, symbol: str):
|
|
bar_history = self.get_bar_history()
|
|
|
|
# 确保有足够的数据来满足最长的回看需求
|
|
# 趋势线需要 trendline_n + 1 个价格
|
|
# 强度计算需要 2 * lookback - 1 个成交量
|
|
min_bars_required = max(
|
|
self.trend_params['trendline_n'] + 2, 2 * self.trend_params['intensity_lookback'],
|
|
self.reversion_params['trendline_n'] + 2, 2 * self.reversion_params['intensity_lookback']
|
|
)
|
|
if len(bar_history) < min_bars_required:
|
|
return
|
|
|
|
all_volumes = np.array([b.volume for b in bar_history], dtype=float)
|
|
|
|
# 为每个模式计算其独立的成交量强度窗口
|
|
trend_intensity_window = np.array([], dtype=np.float64)
|
|
if 'TREND' in self.enabled_modes:
|
|
lookback = self.trend_params['intensity_lookback']
|
|
# 截取计算所需的、正确长度的成交量数据
|
|
volumes_slice = all_volumes[-(2 * lookback - 1):]
|
|
trend_intensity_window = self._calculate_volume_intensity_window(
|
|
volumes_slice, self.trend_params['intensity_kappa'], lookback
|
|
)
|
|
|
|
reversion_intensity_window = np.array([], dtype=np.float64)
|
|
if 'REVERSION' in self.enabled_modes:
|
|
lookback = self.reversion_params['intensity_lookback']
|
|
volumes_slice = all_volumes[-(2 * lookback - 1):]
|
|
reversion_intensity_window = self._calculate_volume_intensity_window(
|
|
volumes_slice, self.reversion_params['intensity_kappa'], lookback
|
|
)
|
|
|
|
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 = trend_intensity_window if strategy_mode == 'TREND' else reversion_intensity_window
|
|
|
|
if window_to_use.size > 0:
|
|
latest_intensity_value = window_to_use[-1]
|
|
exit_threshold = np.quantile(window_to_use, params_to_use['intensity_exit_percent'])
|
|
|
|
if latest_intensity_value < exit_threshold:
|
|
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, trend_intensity_window
|
|
) if 'TREND' in self.enabled_modes else None
|
|
|
|
reversion_signal = self._calculate_entry_signal(
|
|
'REVERSION', bar_history, self.reversion_params, reversion_intensity_window
|
|
) if 'REVERSION' in self.enabled_modes else None
|
|
|
|
# ... 冲突解决和下单逻辑保持不变 ...
|
|
final_direction, winning_mode = self.resolve_signals(trend_signal, reversion_signal)
|
|
|
|
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, intensity_window: np.ndarray) -> \
|
|
Optional[str]:
|
|
if intensity_window.size == 0:
|
|
return None
|
|
|
|
# 1. 成交量强度确认
|
|
latest_intensity_value = intensity_window[-1]
|
|
entry_threshold = np.quantile(intensity_window, params['intensity_entry_percent'])
|
|
intensity_confirmation = latest_intensity_value > entry_threshold
|
|
|
|
if not intensity_confirmation:
|
|
return None
|
|
|
|
# 2. 趋势线突破事件
|
|
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"
|
|
if lower_break_event: return "SELL" if mode == 'TREND' else "BUY"
|
|
|
|
return None
|
|
|
|
def resolve_signals(self, trend_signal: Optional[str], reversion_signal: Optional[str]) -> (Optional[str],
|
|
Optional[str]):
|
|
if trend_signal and reversion_signal:
|
|
self.log(f"信号冲突:趋势模式 ({trend_signal}) vs 回归模式 ({reversion_signal})")
|
|
if self.conflict_resolution == 'TREND_PRIORITY':
|
|
return trend_signal, 'TREND'
|
|
elif self.conflict_resolution == 'REVERSION_PRIORITY':
|
|
return reversion_signal, 'REVERSION'
|
|
else:
|
|
self.log("冲突解决策略为'NONE',本次不开仓。")
|
|
return None, None
|
|
elif trend_signal:
|
|
return trend_signal, 'TREND'
|
|
elif reversion_signal:
|
|
return reversion_signal, 'REVERSION'
|
|
return None, None
|
|
|
|
# send_open_order, send_market_order, on_rollover 等辅助函数保持不变
|
|
# ... (代码与之前版本相同)
|
|
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() |