1、新增傅里叶策略
2、新增策略管理、策略重启功能
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,177 +1,249 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
from collections import deque
|
||||
from typing import Optional, Any, List, Dict
|
||||
import bisect
|
||||
|
||||
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
|
||||
|
||||
|
||||
class TVDZScoreStrategy(Strategy):
|
||||
# =============================================================================
|
||||
# 策略实现 (Dual-Mode Kalman Strategy V4 - 滚动窗口修正版)
|
||||
# =============================================================================
|
||||
|
||||
class DualModeKalmanStrategy(Strategy):
|
||||
"""
|
||||
内嵌 TVD (Condat 算法) + Z-Score ATR 的趋势突破策略。
|
||||
无任何外部依赖(如 pytv),纯 NumPy 实现。
|
||||
V4版本更新:
|
||||
1. 【根本性修正】修复了V3版本中因错误使用全局历史数据而引入的前瞻性偏差和
|
||||
路径依赖问题。
|
||||
2. 【正确实现】现在的数据结构严格、精确地只维护当前滚动窗口(vol_lookback)
|
||||
内的数据,确保了策略的可重复性和逻辑正确性。
|
||||
3. 通过bisect库,在保持100%滚动窗口精度的前提下,实现了高效的百分位计算,
|
||||
避免了在每个bar上都进行暴力排序。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
tvd_lam: float = 50.0,
|
||||
atr_window: int = 14,
|
||||
z_window: int = 100,
|
||||
vol_threshold: float = -0.5,
|
||||
entry_threshold_atr: float = 3.0,
|
||||
stop_atr_multiplier: float = 3.0,
|
||||
order_direction: Optional[List[str]] = None,
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# ... (所有策略参数与V2版本完全相同) ...
|
||||
strategy_mode: str = 'TREND',
|
||||
kalman_process_noise: float = 0.01,
|
||||
kalman_measurement_noise: float = 0.5,
|
||||
atr_period: int = 20,
|
||||
vol_lookback: int = 100,
|
||||
vol_percentile_threshold: float = 25.0,
|
||||
entry_threshold_atr: float = 2.5,
|
||||
initial_stop_atr_multiplier: float = 2.0,
|
||||
structural_stop_atr_multiplier: float = 2.5,
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicators: Optional[List[Indicator]] = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
# ... (参数赋值与V2版本完全相同) ...
|
||||
if order_direction is None: order_direction = ['BUY', 'SELL']
|
||||
self.strategy_mode = strategy_mode.upper()
|
||||
self.trade_volume = trade_volume
|
||||
self.order_direction = order_direction or ["BUY", "SELL"]
|
||||
self.tvd_lam = tvd_lam
|
||||
self.atr_window = atr_window
|
||||
self.z_window = z_window
|
||||
self.vol_threshold = vol_threshold
|
||||
self.atr_period = atr_period
|
||||
self.vol_lookback = vol_lookback
|
||||
self.vol_percentile_threshold = vol_percentile_threshold
|
||||
self.entry_threshold_atr = entry_threshold_atr
|
||||
self.stop_atr_multiplier = stop_atr_multiplier
|
||||
self.initial_stop_atr_multiplier = initial_stop_atr_multiplier
|
||||
self.structural_stop_atr_multiplier = structural_stop_atr_multiplier
|
||||
self.order_direction = order_direction
|
||||
|
||||
# --- 【修正后的数据结构】 ---
|
||||
# 1. 严格限定长度的deque,用于维护滚动窗口的原始序列
|
||||
self._vol_history_queue: deque = deque(maxlen=self.vol_lookback)
|
||||
# 2. 一个普通list,我们将手动维护其有序性,并确保其内容与deque完全同步
|
||||
self._sorted_vol_history: List[float] = []
|
||||
|
||||
self.Q = kalman_process_noise
|
||||
self.R = kalman_measurement_noise
|
||||
self.P = 1.0
|
||||
self.x_hat = 0.0
|
||||
self.kalman_initialized = False
|
||||
|
||||
self.position_meta: Dict[str, Any] = self.context.load_state()
|
||||
self.main_symbol = main_symbol
|
||||
self.order_id_counter = 0
|
||||
|
||||
self.log(f"TVDZScoreStrategy Initialized | λ={tvd_lam}, VolThresh={vol_threshold}")
|
||||
if indicators is None: indicators = [Empty(), Empty()]
|
||||
self.indicators = indicators
|
||||
|
||||
@staticmethod
|
||||
def _tvd_condat(y, lam):
|
||||
"""Condat's O(N) TVD algorithm."""
|
||||
n = y.size
|
||||
if n == 0:
|
||||
return y.copy()
|
||||
x = y.astype(np.float64)
|
||||
k = 0
|
||||
k0 = 0
|
||||
vmin = x[0] - lam
|
||||
vmax = x[0] + lam
|
||||
for i in range(1, n):
|
||||
if x[i] < vmin:
|
||||
while k < i:
|
||||
x[k] = vmin
|
||||
k += 1
|
||||
k0 = i
|
||||
vmin = x[i] - lam
|
||||
vmax = x[i] + lam
|
||||
elif x[i] > vmax:
|
||||
while k < i:
|
||||
x[k] = vmax
|
||||
k += 1
|
||||
k0 = i
|
||||
vmin = x[i] - lam
|
||||
vmax = x[i] + lam
|
||||
else:
|
||||
vmin = max(vmin, x[i] - lam)
|
||||
vmax = min(vmax, x[i] + lam)
|
||||
if vmin > vmax:
|
||||
k = k0
|
||||
s = np.sum(x[k0:i+1])
|
||||
s /= (i - k0 + 1)
|
||||
x[k0:i+1] = s
|
||||
k = i + 1
|
||||
k0 = k
|
||||
if k0 < n:
|
||||
vmin = x[k0] - lam
|
||||
vmax = x[k0] + lam
|
||||
while k < n:
|
||||
x[k] = vmin
|
||||
k += 1
|
||||
return x
|
||||
self.log(f"DualModeKalmanStrategy V4 (Corrected Rolling Window) Initialized.")
|
||||
|
||||
def _compute_zscore_atr_last(self, high, low, close) -> float:
|
||||
n = len(close)
|
||||
min_req = self.atr_window + self.z_window - 1
|
||||
if n < min_req:
|
||||
return np.nan
|
||||
start = max(0, n - (self.z_window + self.atr_window))
|
||||
seg_h, seg_l, seg_c = high[start:], low[start:], close[start:]
|
||||
atr_full = talib.ATR(seg_h, seg_l, seg_c, timeperiod=self.atr_window)
|
||||
atr_valid = atr_full[self.atr_window - 1:]
|
||||
if len(atr_valid) < self.z_window:
|
||||
return np.nan
|
||||
window_atr = atr_valid[-self.z_window:]
|
||||
mu = np.mean(window_atr)
|
||||
sigma = np.std(window_atr)
|
||||
last_atr = window_atr[-1]
|
||||
return (last_atr - mu) / sigma if sigma > 1e-12 else 0.0
|
||||
def on_init(self):
|
||||
super().on_init()
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
self.position_meta = self.context.load_state()
|
||||
# 初始化时清空数据结构
|
||||
self._vol_history_queue.clear()
|
||||
self._sorted_vol_history.clear()
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
if len(bar_history) < max(100, self.atr_window + self.z_window):
|
||||
return
|
||||
# 确保有足够的数据来填满第一个完整的窗口
|
||||
if len(bar_history) < self.vol_lookback + self.atr_period: return
|
||||
|
||||
closes = np.array([b.close for b in bar_history], dtype=np.float64)
|
||||
highs = np.array([b.high for b in bar_history], dtype=np.float64)
|
||||
lows = np.array([b.low for b in bar_history], dtype=np.float64)
|
||||
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]
|
||||
|
||||
# === TVD 平滑 ===
|
||||
tvd_prices = self._tvd_condat(closes, self.tvd_lam)
|
||||
tvd_price = tvd_prices[-1]
|
||||
last_close = closes[-1]
|
||||
if last_close <= 0: return
|
||||
current_normalized_atr = current_atr / last_close
|
||||
|
||||
# === Z-Score ATR ===
|
||||
current_atr = talib.ATR(highs, lows, closes, timeperiod=self.atr_window)[-1]
|
||||
if current_atr <= 0:
|
||||
return
|
||||
# --- 【核心修正:正确的滚动窗口维护】 ---
|
||||
# 1. 如果窗口已满,deque会自动从左侧弹出一个旧值。我们需要捕捉这个值。
|
||||
oldest_val = None
|
||||
if len(self._vol_history_queue) == self.vol_lookback:
|
||||
oldest_val = self._vol_history_queue[0]
|
||||
|
||||
deviation = closes[-1] - tvd_price
|
||||
deviation_in_atr = deviation / current_atr
|
||||
# 2. 将新值添加到deque的右侧
|
||||
self._vol_history_queue.append(current_normalized_atr)
|
||||
|
||||
# 3. 更新有序列表,使其与deque的状态严格同步
|
||||
if oldest_val is not None:
|
||||
# a. 先从有序列表中移除旧值
|
||||
# 由于浮点数精度问题,直接remove可能不安全,我们使用bisect查找并移除
|
||||
# 这是一个O(log N) + O(N)的操作,但远快于完全重排
|
||||
idx_to_remove = bisect.bisect_left(self._sorted_vol_history, oldest_val)
|
||||
if idx_to_remove < len(self._sorted_vol_history) and abs(
|
||||
self._sorted_vol_history[idx_to_remove] - oldest_val) < 1e-9:
|
||||
self._sorted_vol_history.pop(idx_to_remove)
|
||||
else:
|
||||
# 备用方案,如果bisect找不到(理论上不应该),则暴力移除
|
||||
try:
|
||||
self._sorted_vol_history.remove(oldest_val)
|
||||
except ValueError:
|
||||
pass # 如果值不存在,忽略
|
||||
|
||||
# b. 将新值高效地插入到有序列表中
|
||||
bisect.insort_left(self._sorted_vol_history, current_normalized_atr)
|
||||
|
||||
# 检查窗口是否已填满
|
||||
if len(self._sorted_vol_history) < self.vol_lookback: return
|
||||
|
||||
# ... (卡尔曼滤波器计算部分保持不变) ...
|
||||
if not self.kalman_initialized: self.x_hat = closes[-1]
|
||||
self.kalman_initialized = True
|
||||
x_hat_minus = self.x_hat
|
||||
P_minus = self.P + self.Q
|
||||
K = P_minus / (P_minus + self.R)
|
||||
self.x_hat = x_hat_minus + K * (closes[-1] - x_hat_minus)
|
||||
self.P = (1 - K) * P_minus
|
||||
kalman_price = self.x_hat
|
||||
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
# ... (持仓同步逻辑不变) ...
|
||||
|
||||
if position_volume != 0:
|
||||
self.manage_open_position(position_volume, bar_history[-1], current_atr, tvd_price)
|
||||
self.manage_open_position(position_volume, bar_history[-1], current_atr, kalman_price)
|
||||
return
|
||||
|
||||
# --- 使用精确的滚动窗口百分位阈值 ---
|
||||
percentile_index = int(self.vol_percentile_threshold / 100.0 * (self.vol_lookback - 1))
|
||||
vol_threshold = self._sorted_vol_history[percentile_index]
|
||||
|
||||
if current_normalized_atr < vol_threshold:
|
||||
return
|
||||
|
||||
self.evaluate_entry_signal(bar_history[-1], kalman_price, current_atr)
|
||||
|
||||
def manage_open_position(self, volume: int, current_bar: Bar, current_atr: float, kalman_price: float):
|
||||
# ... (此部分代码与上一版完全相同,保持不变) ...
|
||||
meta = self.position_meta.get(self.symbol)
|
||||
if not meta: return
|
||||
|
||||
initial_stop_price = meta['initial_stop_price']
|
||||
if (volume > 0 and current_bar.low <= initial_stop_price) or \
|
||||
(volume < 0 and current_bar.high >= initial_stop_price):
|
||||
self.log(f"Initial Stop Loss hit at {initial_stop_price:.4f}")
|
||||
self.close_position("CLOSE_LONG" if volume > 0 else "CLOSE_SHORT", abs(volume))
|
||||
return
|
||||
|
||||
if self.strategy_mode == 'TREND':
|
||||
if volume > 0:
|
||||
stop_price = max(kalman_price - self.structural_stop_atr_multiplier * current_atr, initial_stop_price)
|
||||
if current_bar.low <= stop_price:
|
||||
self.log(f"TREND Mode: Structural Stop hit for LONG at {stop_price:.4f}")
|
||||
self.close_position("CLOSE_LONG", abs(volume))
|
||||
else:
|
||||
stop_price = min(kalman_price + self.structural_stop_atr_multiplier * current_atr, initial_stop_price)
|
||||
if current_bar.high >= stop_price:
|
||||
self.log(f"TREND Mode: Structural Stop hit for SHORT at {stop_price:.4f}")
|
||||
self.close_position("CLOSE_SHORT", abs(volume))
|
||||
|
||||
elif self.strategy_mode == 'REVERSION':
|
||||
if volume > 0 and current_bar.high >= kalman_price:
|
||||
self.log(f"REVERSION Mode: Take Profit for LONG as price reverts to Kalman line at {kalman_price:.4f}")
|
||||
self.close_position("CLOSE_LONG", abs(volume))
|
||||
elif volume < 0 and current_bar.low <= kalman_price:
|
||||
self.log(f"REVERSION Mode: Take Profit for SHORT as price reverts to Kalman line at {kalman_price:.4f}")
|
||||
self.close_position("CLOSE_SHORT", abs(volume))
|
||||
|
||||
def evaluate_entry_signal(self, current_bar: Bar, kalman_price: float, current_atr: float):
|
||||
# ... (此部分代码与上一版完全相同,保持不变) ...
|
||||
deviation = current_bar.close - kalman_price
|
||||
if current_atr <= 0: return
|
||||
deviation_in_atr = deviation / current_atr
|
||||
|
||||
direction = None
|
||||
if "BUY" in self.order_direction and deviation_in_atr > self.entry_threshold_atr:
|
||||
direction = "BUY"
|
||||
elif "SELL" in self.order_direction and deviation_in_atr < -self.entry_threshold_atr:
|
||||
direction = "SELL"
|
||||
|
||||
if self.strategy_mode == 'TREND':
|
||||
if "BUY" in self.order_direction and deviation_in_atr > self.entry_threshold_atr:
|
||||
direction = "BUY"
|
||||
elif "SELL" in self.order_direction and deviation_in_atr < -self.entry_threshold_atr:
|
||||
direction = "SELL"
|
||||
|
||||
elif self.strategy_mode == 'REVERSION':
|
||||
if "SELL" in self.order_direction and deviation_in_atr > self.entry_threshold_atr:
|
||||
direction = "SELL"
|
||||
elif "BUY" in self.order_direction and deviation_in_atr < -self.entry_threshold_atr:
|
||||
direction = "BUY"
|
||||
|
||||
if direction:
|
||||
self.log(f"Signal Fired | Dir: {direction}, Dev: {deviation_in_atr:.2f} ATR")
|
||||
entry_price = closes[-1]
|
||||
stop_loss = (
|
||||
entry_price - self.stop_atr_multiplier * current_atr
|
||||
if direction == "BUY"
|
||||
else entry_price + self.stop_atr_multiplier * current_atr
|
||||
)
|
||||
meta = {"entry_price": entry_price, "stop_loss": stop_loss}
|
||||
self.log(f"{self.strategy_mode} Mode: Entry Signal {direction}. Deviation: {deviation_in_atr:.2f} ATRs.")
|
||||
entry_price = current_bar.close
|
||||
stop_loss_price = entry_price - self.initial_stop_atr_multiplier * current_atr if direction == "BUY" else entry_price + self.initial_stop_atr_multiplier * current_atr
|
||||
meta = {'entry_price': entry_price, 'initial_stop_price': stop_loss_price, 'direction': direction}
|
||||
self.send_market_order(direction, self.trade_volume, "OPEN", meta)
|
||||
self.save_state(self.position_meta)
|
||||
|
||||
def manage_open_position(self, volume: int, current_bar: Bar, current_atr: float, tvd_price: float):
|
||||
meta = self.position_meta.get(self.symbol)
|
||||
if not meta:
|
||||
return
|
||||
stop_loss = meta["stop_loss"]
|
||||
if (volume > 0 and current_bar.low <= stop_loss) or (volume < 0 and current_bar.high >= stop_loss):
|
||||
self.log(f"Stop Loss Hit at {stop_loss:.4f}")
|
||||
self.close_position("CLOSE_LONG" if volume > 0 else "CLOSE_SHORT", abs(volume))
|
||||
|
||||
def close_position(self, direction: str, volume: int):
|
||||
self.send_market_order(direction, volume, offset="CLOSE")
|
||||
if self.symbol in self.position_meta:
|
||||
del self.position_meta[self.symbol]
|
||||
self.position_meta = {}
|
||||
self.save_state(self.position_meta)
|
||||
|
||||
def send_market_order(self, direction: str, volume: int, offset: str, meta: Optional[Dict] = None):
|
||||
if offset == "OPEN" and meta:
|
||||
self.position_meta[self.symbol] = meta
|
||||
if offset == "OPEN" and meta: self.position_meta[self.symbol] = meta
|
||||
order_id = f"{self.symbol}_{direction}_MARKET_{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)
|
||||
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_limit_order(self, limit_price: float, direction: str, volume: int, offset: str,
|
||||
meta: Optional[Dict] = None):
|
||||
if offset == "OPEN" and meta: self.position_meta[self.symbol] = meta
|
||||
order_id = f"{self.symbol}_{direction}_MARKET_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(id=order_id, symbol=self.symbol, direction=direction, volume=volume, price_type="LIMIT",
|
||||
submitted_time=self.get_current_time(), offset=offset, limit_price=limit_price)
|
||||
self.send_order(order)
|
||||
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
super().on_rollover(old_symbol, new_symbol)
|
||||
self.position_meta = {}
|
||||
self.log("Rollover: Strategy state reset.")
|
||||
self.kalman_initialized = False
|
||||
self._sorted_vol_history.clear()
|
||||
self.log("Rollover detected. All strategy states have been reset.")
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user