新增实盘策略:ITrendStrategy(SA)

This commit is contained in:
2026-01-25 23:26:03 +08:00
parent 4a37652269
commit fa4749b02e
66 changed files with 44943 additions and 5961 deletions

View File

@@ -1,177 +1,221 @@
import numpy as np
import pandas as pd
import talib
from collections import deque
from typing import Optional, Any, List, Dict
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):
class DualModeKalmanStrategy(Strategy):
"""
内嵌 TVD (Condat 算法) + Z-Score ATR 的趋势突破策略
无任何外部依赖(如 pytv纯 NumPy 实现。
基于卡尔曼因子对称性的双模自适应策略
因子定义: Deviation = (Current_Close - Kalman_Price) / ATR
"""
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,
strategy_mode: str = 'TREND', # 'TREND' 或 'REVERSION'
kalman_process_noise: float = 0.01,
kalman_measurement_noise: float = 0.5,
atr_period: int = 23,
atr_lookback: int = 100,
atr_percentile_threshold: float = 25.0,
entry_threshold_atr: float = 2.5, # 入场偏离倍数
stop_loss_atr: float = 2.0, # 保护性硬止损倍数
trend_trailing_atr: float = 2.5, # 趋势模式下卡尔曼轨道的宽度
order_direction=None,
indicators: Optional[List[Indicator]] = None,
):
super().__init__(context, main_symbol, enable_log)
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.entry_threshold_atr = entry_threshold_atr
self.stop_atr_multiplier = stop_atr_multiplier
if order_direction is None:
order_direction = ['BUY', 'SELL']
self.strategy_mode = strategy_mode.upper()
self.trade_volume = trade_volume
self.atr_period = atr_period
self.atr_lookback = atr_lookback
self.atr_percentile_threshold = atr_percentile_threshold
self.entry_threshold_atr = entry_threshold_atr
self.stop_loss_atr = stop_loss_atr
self.trend_trailing_atr = entry_threshold_atr
# 卡尔曼状态
self.Q = kalman_process_noise
self.R = kalman_measurement_noise
self.P = 1.0
self.x_hat = 0.0
self.kalman_initialized = False
self._atr_history: deque = deque(maxlen=self.atr_lookback)
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}")
self.order_direction = order_direction
if indicators is None:
self.indicators = [Empty(), Empty()]
else:
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
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
self.log(f"Initialized [{self.strategy_mode}] Mode with Kalman Symmetry.")
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) < max(self.atr_period, self.atr_lookback) + 2: 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)
self.cancel_all_pending_orders(symbol)
# === TVD 平滑 ===
tvd_prices = self._tvd_condat(closes, self.tvd_lam)
tvd_price = tvd_prices[-1]
# 1. 计算核心指标
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)
# === Z-Score ATR ===
current_atr = talib.ATR(highs, lows, closes, timeperiod=self.atr_window)[-1]
if current_atr <= 0:
return
current_atr = talib.ATR(highs, lows, closes, self.atr_period)[-1]
self._atr_history.append(current_atr)
deviation = closes[-1] - tvd_price
deviation_in_atr = deviation / current_atr
if current_atr <= 0 or len(self._atr_history) < self.atr_lookback: return
# 2. 更新卡尔曼滤波器
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
# 3. 计算对称性因子:偏离度 (Deviation in ATR)
deviation_in_atr = (closes[-1] - kalman_price) / current_atr
# 4. 状态校验与持仓管理
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)
if self.trading:
if position_volume != 0:
self.manage_logic(position_volume, bar_history[-1], current_atr, kalman_price, deviation_in_atr)
else:
# 波动率过滤:只在波动率处于自身中高水平时入场
# atr_threshold = np.percentile(list(self._atr_history), self.atr_percentile_threshold)
# if current_atr >= atr_threshold:
self.evaluate_entry_signal(bar_history[-1], deviation_in_atr, current_atr, open_price)
def manage_logic(self, volume: int, current_bar: Bar, current_atr: float, kalman_price: float, dev: float):
"""
基于对称因子的出场逻辑
"""
meta = self.position_meta.get(self.symbol)
if not meta: return
# A. 保护性硬止损 (防止跳空或极端行情)
initial_stop = meta.get('initial_stop_price', 0)
if (volume > 0 and current_bar.low <= initial_stop) or (volume < 0 and current_bar.high >= initial_stop):
self.log(f"Hard Stop Hit. Price: {current_bar.close}")
self.close_position("CLOSE_LONG" if volume > 0 else "CLOSE_SHORT", abs(volume))
return
# B. 对称因子出场逻辑
if self.strategy_mode == 'TREND':
if volume > 0:
trend_floor = kalman_price - self.trend_trailing_atr * current_atr
if dev < 0:
self.log(f"TREND: Structural Floor Hit at {trend_floor:.4f}")
self.close_position("CLOSE_LONG", abs(volume))
else:
trend_ceiling = kalman_price + self.trend_trailing_atr * current_atr
if dev > 0:
self.log(f"TREND: Structural Ceiling Hit at {trend_ceiling:.4f}")
self.close_position("CLOSE_SHORT", abs(volume))
elif self.strategy_mode == 'REVERSION':
# 回归模式:出场基于“均值修复成功”或“偏离失控止损”
# 1. 目标达成:回归到均值附近 (止盈)
if volume > 0:
trend_floor = kalman_price - self.trend_trailing_atr * current_atr
if dev > 0:
self.log(f"TREND: Structural Floor Hit at {trend_floor:.4f}")
self.close_position("CLOSE_LONG", abs(volume))
else:
trend_ceiling = kalman_price + self.trend_trailing_atr * current_atr
if dev < 0:
self.log(f"TREND: Structural Ceiling Hit at {trend_ceiling:.4f}")
self.close_position("CLOSE_SHORT", abs(volume))
def evaluate_entry_signal(self, current_bar: Bar, dev: float, current_atr: float, open_price: float):
"""
基于对称因子的入场逻辑
"""
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 dev > self.entry_threshold_atr and self.indicators[0].is_condition_met(*self.get_indicator_tuple()):
direction = "BUY"
elif dev < -self.entry_threshold_atr and self.indicators[1].is_condition_met(*self.get_indicator_tuple()):
direction = "SELL"
elif self.strategy_mode == 'REVERSION':
# 回归:逆着偏离方向入场
if dev > self.entry_threshold_atr and self.indicators[1].is_condition_met(*self.get_indicator_tuple()):
direction = "SELL" # 超买做空
elif dev < -self.entry_threshold_atr and self.indicators[0].is_condition_met(*self.get_indicator_tuple()):
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.send_market_order(direction, self.trade_volume, "OPEN", meta)
self.save_state(self.position_meta)
# 使用最小变动单位修正价格此处假设最小变动为0.5实盘应从context获取
tick_size = 1
entry_price = open_price
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))
# 设置保护性硬止损
stop_offset = self.stop_loss_atr * current_atr
stop_price = entry_price - stop_offset if direction == "BUY" else entry_price + stop_offset
meta = {'entry_price': entry_price, 'initial_stop_price': stop_price}
self.log(f"Entry Signal: {self.strategy_mode} | {direction} | Dev: {dev:.2f}")
self.send_custom_order(entry_price, direction, self.trade_volume, "OPEN", meta)
# --- 辅助函数 ---
def send_custom_order(self, price: float, direction: str, volume: int, offset: str, meta: Dict):
self.position_meta[self.symbol] = meta
order_type = "LIMIT"
order_id = f"{self.symbol}_{direction}_{order_type}_{self.order_id_counter}"
self.order_id_counter += 1
order = Order(
id=order_id, symbol=self.symbol, direction=direction,
volume=volume, price_type=order_type,
submitted_time=self.get_current_time(), offset=offset, limit_price=price
)
self.send_order(order)
self.save_state(self.position_meta)
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.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
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="CLOSE"
)
self.send_order(order)
self.position_meta.pop(self.symbol, None)
self.save_state(self.position_meta)
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._atr_history.clear()
self.log("Rollover: States Reset.")