1、新增傅里叶策略

2、新增策略管理、策略重启功能
This commit is contained in:
2025-11-20 16:10:16 +08:00
parent 2ae9f2db9e
commit 2c917a467a
19 changed files with 3368 additions and 6643 deletions

File diff suppressed because one or more lines are too long

View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -1,21 +1,32 @@
import numpy as np
import talib
from datetime import datetime
from typing import Optional, Any, List
from scipy.signal import stft
from datetime import datetime, timedelta
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, NormalizedATR, AtrVolatility
from src.strategies.base_strategy import Strategy
# =============================================================================
# 瞬态冲击回调与ATR波幅止盈策略 (V4 - 统一信号与Close价核心逻辑版)
# 策略实现 (SpectralTrendStrategy)
# =============================================================================
class TransientShockATRStrategy(Strategy):
class SpectralTrendStrategy(Strategy):
"""
V4版本更新 (根据专业建议重构):
1. 【核心变更】信号识别逻辑统一:不再区分跳空和大阳线,仅使用 (上一bar.close - 上上bar.close) 的绝对波幅作为市场冲击的唯一衡量标准。
2. 【核心变更】信号计算基于Close价所有信号计算均基于更稳健的收盘价避免High/Low的噪音干扰。
3. 【参数简化】移除了冗余的 signal_gap_pct 参数,使策略更简洁、更易于优化。
频域能量相变策略 - 捕获肥尾趋势
核心哲学:
1. 显式傅里叶变换: 直接分离低频(趋势)、高频(噪音)能量
2. 相变临界点: 仅当低频能量占比 > 阈值时入场
3. 低频交易: 每月仅2-5次信号持仓数日捕获肥尾
4. 完全参数化: 无硬编码,适配任何市场时间结构
参数说明:
- bars_per_day: 市场每日K线数量 (e.g., 23 for 15min US markets)
- low_freq_days: 低频定义下限 (天), 默认2.0
- high_freq_days: 高频定义上限 (天), 默认1.0
"""
def __init__(
@@ -24,197 +35,247 @@ class TransientShockATRStrategy(Strategy):
main_symbol: str,
enable_log: bool,
trade_volume: int,
# --- 【核心参数】---
atr_period: int = 20,
signal_move_atr_mult: float = 2.5, # 【变更】定义“市场冲击”的ATR倍数 (统一信号)
entry_pullback_pct: float = 0.5,
order_expiry_bars: int = 5,
initial_stop_atr_mult: float = 2.0,
exit_signal_atr_mult: float = 2.5,
# --- 【市场结构参数】 ---
bars_per_day: int = 23, # 关键: 适配23根/天的市场
# --- 【频域核心参数】 ---
spectral_window_days: float = 2.0, # STFT窗口大小(天)
low_freq_days: float = 2.0, # 低频下限(天)
high_freq_days: float = 1.0, # 高频上限(天)
trend_strength_threshold: float = 0.8, # 相变临界值
exit_threshold: float = 0.4, # 退出阈值
# --- 【持仓管理】 ---
max_hold_days: int = 10, # 最大持仓天数
# --- 其他 ---
order_direction: Optional[List[str]] = None,
indicators: Optional[List[Indicator]] = None,
model_indicator: Indicator = None,
):
super().__init__(context, main_symbol, enable_log)
if not (atr_period > 0 and signal_move_atr_mult > 0 and entry_pullback_pct > 0 and
initial_stop_atr_mult > 0 and exit_signal_atr_mult > 0):
raise ValueError("所有周期和倍数参数必须大于0")
if order_direction is None:
order_direction = ['BUY', 'SELL']
if indicators is None:
indicators = [Empty(), Empty()] # 保持兼容性
# --- 参数赋值 (完全参数化) ---
self.trade_volume = trade_volume
self.atr_period = atr_period
self.signal_move_atr_mult = signal_move_atr_mult # 【变更】使用新参数
self.entry_pullback_pct = entry_pullback_pct
self.order_expiry_bars = order_expiry_bars
self.initial_stop_atr_mult = initial_stop_atr_mult
self.exit_signal_atr_mult = exit_signal_atr_mult
self.bars_per_day = bars_per_day
self.spectral_window_days = spectral_window_days
self.low_freq_days = low_freq_days
self.high_freq_days = high_freq_days
self.trend_strength_threshold = trend_strength_threshold
self.exit_threshold = exit_threshold
self.max_hold_days = max_hold_days
self.order_direction = order_direction
if model_indicator is None:
model_indicator = Empty()
self.model_indicator = model_indicator
self.pending_order: Optional[dict] = None
self.position_entry_price: float = 0.0
self.initial_stop_price: float = 0.0
self.order_id_counter = 0
# --- 动态计算参数 ---
self.spectral_window = int(self.spectral_window_days * self.bars_per_day)
# 确保窗口大小为偶数 (STFT要求)
self.spectral_window = self.spectral_window if self.spectral_window % 2 == 0 else self.spectral_window + 1
def on_init(self):
"""策略初始化"""
self.log(f"🚀 Strategy On Init: Initializing {self.__class__.__name__} V4...")
self.cancel_all_pending_orders(self.main_symbol)
self.pending_order = None
self.position_entry_price = 0.0
self.initial_stop_price = 0.0
# 频率边界 (cycles/day)
self.low_freq_bound = 1.0 / self.low_freq_days if self.low_freq_days > 0 else float('inf')
self.high_freq_bound = 1.0 / self.high_freq_days if self.high_freq_days > 0 else 0.0
# --- 内部状态变量 ---
self.main_symbol = main_symbol
self.order_id_counter = 0
self.log("✅ Strategy Initialized and State Reset.")
self.indicators = indicators
self.entry_time = None # 入场时间
self.position_direction = None # 'LONG' or 'SHORT'
self.last_trend_strength = 0.0
self.last_dominant_freq = 0.0 # 主导周期(天)
self.log(f"SpectralTrendStrategy Initialized (bars/day={bars_per_day}, window={self.spectral_window} bars)")
def on_open_bar(self, open_price: float, symbol: str):
"""每根K线开盘时被调用"""
self.symbol = symbol
bar_history = self.get_bar_history()
current_time = self.get_current_time()
min_bars = self.atr_period + 5
if len(bar_history) < min_bars:
# 需要足够的数据 (STFT窗口 + 缓冲)
if len(bar_history) < self.spectral_window + 10:
if self.enable_log and len(bar_history) % 50 == 0:
self.log(f"Waiting for {len(bar_history)}/{self.spectral_window + 10} bars")
return
positions = self.get_current_positions()
position_volume = positions.get(self.symbol, 0)
position_volume = self.get_current_positions().get(self.symbol, 0)
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, timeperiod=self.atr_period)[-1]
# 【核心】计算频域趋势强度 (显式傅里叶)
trend_strength, dominant_freq = self.calculate_trend_strength(closes)
self.last_trend_strength = trend_strength
self.last_dominant_freq = dominant_freq
if not self.trading:
# 检查最大持仓时间 (防止极端事件)
if self.entry_time and (current_time - self.entry_time) >= timedelta(days=self.max_hold_days):
self.log(f"Max hold time reached ({self.max_hold_days} days). Forcing exit.")
self.close_all_positions()
self.entry_time = None
self.position_direction = None
return
if position_volume != 0:
self.manage_position(bar_history[-1], position_volume, current_atr)
return
# 核心逻辑:相变入场/退出
if position_volume == 0:
self.evaluate_entry_signal(open_price, trend_strength, dominant_freq)
else:
self.manage_open_position(position_volume, trend_strength, dominant_freq)
if self.pending_order:
self.manage_pending_order()
if self.pending_order:
return
self.identify_new_signal(bar_history, current_atr)
def manage_position(self, last_bar: Bar, position_volume: int, current_atr: float):
"""管理当前持仓 (逻辑不变)"""
# ... (此部分代码与上一版完全相同,保持不变) ...
if position_volume > 0 and last_bar.low <= self.initial_stop_price:
self.log(f"⬇️ LONG STOP LOSS: Low={last_bar.low:.4f} <= Stop={self.initial_stop_price:.4f}")
self.close_position("CLOSE_LONG", abs(position_volume))
return
elif position_volume < 0 and last_bar.high >= self.initial_stop_price:
self.log(f"⬆️ SHORT STOP LOSS: High={last_bar.high:.4f} >= Stop={self.initial_stop_price:.4f}")
self.close_position("CLOSE_SHORT", abs(position_volume))
return
bar_range = last_bar.high - last_bar.low
exit_threshold = current_atr * self.exit_signal_atr_mult
if position_volume > 0 and last_bar.close < last_bar.open and bar_range > exit_threshold:
self.log(
f"⬇️ LONG VOLATILITY EXIT: Strong Bearish Bar. Range={bar_range:.2f} > Threshold={exit_threshold:.2f}")
self.close_position("CLOSE_LONG", abs(position_volume))
elif position_volume < 0 and last_bar.close > last_bar.open and bar_range > exit_threshold:
self.log(
f"⬆️ SHORT VOLATILITY EXIT: Strong Bullish Bar. Range={bar_range:.2f} > Threshold={exit_threshold:.2f}")
self.close_position("CLOSE_SHORT", abs(position_volume))
def manage_pending_order(self):
"""管理挂单 (逻辑不变)"""
# ... (此部分代码与上一版完全相同,保持不变) ...
if not self.pending_order:
return
self.pending_order['bars_waited'] += 1
if self.pending_order['bars_waited'] >= self.order_expiry_bars:
self.log(f"⌛️ PENDING ORDER EXPIRED: Order for {self.pending_order['direction']} "
f"at {self.pending_order['price']:.4f} cancelled after {self.pending_order['bars_waited']} bars.")
self.cancel_order(self.pending_order['id'])
self.pending_order = None
def identify_new_signal(self, bar_history: List[Bar], current_atr: float):
def calculate_trend_strength(self, prices: np.array) -> (float, float):
"""
核心逻辑重构】
识别新的交易信号。信号源现在统一为 (close - prev_close) 的波幅。
显式傅里叶】计算低频能量占比 (完全参数化)
步骤:
1. 价格归一化 (窗口内)
2. 短时傅里叶变换 (STFT) - 采样率=bars_per_day
3. 动态计算频段边界 (基于bars_per_day)
4. 趋势强度 = 低频能量 / (低频+高频能量)
"""
last_bar = bar_history[-1]
prev_bar = bar_history[-2]
# 1. 验证数据长度
if len(prices) < self.spectral_window:
return 0.0, 0.0
# --- CHANGE 1: 定义统一的信号阈值 ---
signal_threshold = current_atr * self.signal_move_atr_mult
# 2. 价格归一化 (仅使用窗口内数据)
window_data = prices[-self.spectral_window:]
normalized = (window_data - np.mean(window_data)) / (np.std(window_data) + 1e-8)
# --- CHANGE 2: 计算核心的 close-to-close 波幅 ---
move_height = last_bar.close - prev_bar.close
# 3. STFT (采样率=bars_per_day)
try:
# fs: 每天的样本数 (bars_per_day)
f, t, Zxx = stft(
normalized,
fs=self.bars_per_day, # 关键: 适配市场结构
nperseg=self.spectral_window,
noverlap=max(0, self.spectral_window // 2),
boundary=None,
padded=False
)
except Exception as e:
self.log(f"STFT calculation error: {str(e)}")
return 0.0, 0.0
# --- 多头信号: 上涨波幅超过阈值 ---
if move_height > signal_threshold:
self.log(f"💡 Bullish Shock Detected: Move={move_height:.2f} > Threshold={signal_threshold:.2f}")
# 回调计算仍然基于 last_bar.high 作为情绪顶点,但回调深度由更稳健的 move_height 决定
entry_price = last_bar.high - (move_height * self.entry_pullback_pct)
stop_price = entry_price - (current_atr * self.initial_stop_atr_mult)
self.place_limit_order("BUY", entry_price, stop_price)
return
# 4. 过滤无效频率 (STFT返回频率范围: 0 到 fs/2)
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
f = f[valid_mask]
Zxx = Zxx[valid_mask, :]
# --- 空头信号: 下跌波幅超过阈值 ---
# 注意: move_height此时为负数
if move_height < -signal_threshold:
down_move_height = abs(move_height)
self.log(f"💡 Bearish Shock Detected: Move={move_height:.2f} < Threshold={-signal_threshold:.2f}")
# 回调计算基于 last_bar.low 作为情绪谷点
entry_price = last_bar.low + (down_move_height * self.entry_pullback_pct)
stop_price = entry_price + (current_atr * self.initial_stop_atr_mult)
self.place_limit_order("SELL", entry_price, stop_price)
if Zxx.size == 0 or Zxx.shape[1] == 0:
return 0.0, 0.0
def place_limit_order(self, direction: str, price: float, stop_price: float):
"""创建、记录并发送一个新的限价挂单"""
# ... (此部分代码与上一版完全相同,保持不变) ...
order_id = self.generate_order_id(direction, "OPEN")
self.pending_order = {
"id": order_id, "symbol": self.symbol, "direction": direction,
"volume": self.trade_volume, "price": price, "stop_price": stop_price,
"bars_waited": 0,
}
self.log(f"🆕 Placing Limit Order: {direction} at {price:.4f} (Stop: {stop_price:.4f}).")
# 5. 计算最新时间点的能量
current_energy = np.abs(Zxx[:, -1]) ** 2
order = Order(
id=order_id, symbol=self.symbol, direction=direction,
volume=self.trade_volume, price_type="LIMIT", limit_price=price,
submitted_time=self.get_current_time(), offset="OPEN"
)
self.send_order(order)
# 6. 动态频段定义 (cycles/day)
# 低频: 周期 > low_freq_days → 频率 < 1/low_freq_days
low_freq_mask = f < self.low_freq_bound
# 高频: 周期 < high_freq_days → 频率 > 1/high_freq_days
high_freq_mask = f > self.high_freq_bound
def on_trade(self, trade):
"""处理成交回报 (逻辑不变)"""
# ... (此部分代码与上一版完全相同,保持不变) ...
if self.pending_order and trade.id == self.pending_order['id']:
self.log(
f"✅ Order Filled: {trade.direction} at {trade.price:.4f}. Stop loss set at {self.pending_order['stop_price']:.4f}")
self.position_entry_price = trade.price
self.initial_stop_price = self.pending_order['stop_price']
self.pending_order = None
# 7. 能量计算
low_energy = np.sum(current_energy[low_freq_mask]) if np.any(low_freq_mask) else 0.0
high_energy = np.sum(current_energy[high_freq_mask]) if np.any(high_freq_mask) else 0.0
total_energy = low_energy + high_energy + 1e-8 # 防除零
def generate_order_id(self, direction: str, offset: str) -> str:
# ... (此部分代码与上一版完全相同,保持不变) ...
self.order_id_counter += 1
return f"{self.symbol}_{direction}_{offset}_{self.order_id_counter}_{int(datetime.now().timestamp())}"
# 8. 趋势强度 = 低频能量占比
trend_strength = low_energy / total_energy
# 9. 计算主导趋势周期 (天)
dominant_freq = 0.0
if np.any(low_freq_mask) and low_energy > 0:
# 找到低频段最大能量对应的频率
low_energies = current_energy[low_freq_mask]
max_idx = np.argmax(low_energies)
dominant_freq = 1.0 / (f[low_freq_mask][max_idx] + 1e-8) # 转换为周期(天)
return trend_strength, dominant_freq
def evaluate_entry_signal(self, open_price: float, trend_strength: float, dominant_freq: float):
"""评估相变入场信号"""
# 仅当趋势强度跨越临界点且有明确周期时入场
if trend_strength > self.trend_strength_threshold and dominant_freq > self.low_freq_days:
direction = None
indicator = self.model_indicator
# 做多信号: 价格在窗口均值上方
closes = np.array([b.close for b in self.get_bar_history()[-self.spectral_window:]], dtype=float)
if "BUY" in self.order_direction and np.mean(closes[-5:]) > np.mean(closes):
direction = "BUY" if indicator.is_condition_met(*self.get_indicator_tuple()) else "SELL"
# 做空信号: 价格在窗口均值下方
elif "SELL" in self.order_direction and np.mean(closes[-5:]) < np.mean(closes):
direction = "SELL" if indicator.is_condition_met(*self.get_indicator_tuple()) else "BUY"
if direction:
self.log(
f"Phase Transition Entry: {direction} | Strength={trend_strength:.2f} | Dominant Period={dominant_freq:.1f}d")
self.send_limit_order(direction, open_price, self.trade_volume, "OPEN")
self.entry_time = self.get_current_time()
self.position_direction = "LONG" if direction == "BUY" else "SHORT"
def manage_open_position(self, volume: int, trend_strength: float, dominant_freq: float):
"""管理持仓:仅当相变逆转时退出"""
# 相变逆转条件: 趋势强度 < 退出阈值
if trend_strength < self.exit_threshold:
direction = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
self.log(f"Phase Transition Exit: {direction} | Strength={trend_strength:.2f} < {self.exit_threshold}")
self.close_position(direction, abs(volume))
self.entry_time = None
self.position_direction = None
# --- 辅助函数区 ---
def close_all_positions(self):
"""强制平仓所有头寸"""
positions = self.get_current_positions()
if self.symbol in positions and positions[self.symbol] != 0:
direction = "CLOSE_LONG" if positions[self.symbol] > 0 else "CLOSE_SHORT"
self.close_position(direction, abs(positions[self.symbol]))
self.log(f"Forced exit of {abs(positions[self.symbol])} contracts")
def close_position(self, direction: str, volume: int):
# ... (此部分代码与上一版完全相同,保持不变) ...
self.send_market_order(direction, volume, offset="CLOSE")
self.position_entry_price = 0.0
self.initial_stop_price = 0.0
def send_market_order(self, direction: str, volume: int, offset: str = "OPEN"):
# ... (此部分代码与上一版完全相同,保持不变) ...
order_id = self.generate_order_id(direction, offset)
self.log(f"➡️ Sending Market Order: {direction} {volume} {self.symbol} ({offset})")
def send_market_order(self, direction: str, volume: int, offset: str):
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
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 cancel_order(self, order_id: str):
# ... (此部分代码与上一版完全相同,保持不变) ...
self.log(f"❌ Sending Cancel Request for Order: {order_id}")
self.context.cancel_order(order_id)
def send_limit_order(self, direction: str, limit_price: float, volume: int, offset: str):
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_init(self):
super().on_init()
self.cancel_all_pending_orders(self.main_symbol)
self.log("Strategy initialized. Waiting for phase transition signals...")
def on_rollover(self, old_symbol: str, new_symbol: str):
super().on_rollover(old_symbol, new_symbol)
self.log(f"Rollover from {old_symbol} to {new_symbol}. Resetting position state.")
self.entry_time = None
self.position_direction = None
self.last_trend_strength = 0.0

View File

@@ -1,6 +1,7 @@
import numpy as np
import talib
from typing import Optional, Any, List
from scipy.signal import stft
from datetime import datetime, timedelta
from typing import Optional, Any, List, Dict
from src.core_data import Bar, Order
from src.indicators.base_indicators import Indicator
@@ -8,137 +9,346 @@ from src.indicators.indicators import Empty
from src.strategies.base_strategy import Strategy
class SuperTrendStrategy(Strategy):
# =============================================================================
# 策略实现 (VolatilityAdaptiveSpectralStrategy)
# =============================================================================
class SpectralTrendStrategy(Strategy):
"""
SuperTrend 策略
- 基于 ATR 和价格波动构建上下轨
- 价格上穿上轨 → 开多(且多头条件满足)
- 价格下穿下轨 → 开空(且空头条件满足)
- 反向穿越 → 平仓并反手(或仅平仓,支持空仓)
- 标准 SuperTrend 公式:使用 ATR * multiplier
波动率自适应频域趋势策略
核心哲学:
1. 显式傅里叶变换: 分离低频(趋势)、高频(噪音)能量
2. 波动率条件信号: 根据波动率环境动态调整交易方向
- 低波动环境: 趋势策略 (高趋势强度 → 延续)
- 高波动环境: 反转策略 (高趋势强度 → 反转)
3. 无硬编码参数: 所有阈值通过配置参数设定
4. 严格无未来函数: 所有计算使用历史数据
参数说明:
- bars_per_day: 市场每日K线数量
- volatility_lookback: 波动率计算窗口(天)
- low_vol_threshold: 低波动环境阈值(0-1)
- high_vol_threshold: 高波动环境阈值(0-1)
"""
def __init__(
self,
context: Any,
main_symbol: str,
enable_log: bool,
trade_volume: int,
atr_period: int = 10,
atr_multiplier: float = 3.0,
order_direction: Optional[List[str]] = None,
indicators: Optional[List[Indicator]] = None,
self,
context: Any,
main_symbol: str,
enable_log: bool,
trade_volume: int,
# --- 【市场结构参数】 ---
bars_per_day: int = 23, # 适配23根/天的市场
# --- 【频域核心参数】 ---
spectral_window_days: float = 2.0, # STFT窗口大小(天)
low_freq_days: float = 2.0, # 低频下限(天)
high_freq_days: float = 1.0, # 高频上限(天)
trend_strength_threshold: float = 0.8, # 趋势强度阈值
exit_threshold: float = 0.5, # 退出阈值
# --- 【波动率参数】 ---
volatility_lookback_days: float = 5.0, # 波动率计算窗口(天)
low_vol_threshold: float = 0.3, # 低波动环境阈值(0-1)
high_vol_threshold: float = 0.7, # 高波动环境阈值(0-1)
# --- 【持仓管理】 ---
max_hold_days: int = 10, # 最大持仓天数
# --- 其他 ---
order_direction: Optional[List[str]] = None,
indicators: Optional[List[Indicator]] = None,
):
super().__init__(context, main_symbol, enable_log)
if order_direction is None:
order_direction = ["BUY", "SELL"]
order_direction = ['BUY', 'SELL']
if indicators is None:
indicators = [Empty(), Empty()]
indicators = [Empty(), Empty()] # 保持兼容性
# --- 参数赋值 (完全参数化) ---
self.trade_volume = trade_volume
self.atr_period = atr_period
self.atr_multiplier = atr_multiplier
self.bars_per_day = bars_per_day
self.spectral_window_days = spectral_window_days
self.low_freq_days = low_freq_days
self.high_freq_days = high_freq_days
self.trend_strength_threshold = trend_strength_threshold
self.exit_threshold = exit_threshold
self.volatility_lookback_days = volatility_lookback_days
self.low_vol_threshold = low_vol_threshold
self.high_vol_threshold = high_vol_threshold
self.max_hold_days = max_hold_days
self.order_direction = order_direction
self.indicators = indicators
# --- 动态计算参数 ---
self.spectral_window = int(self.spectral_window_days * self.bars_per_day)
self.spectral_window = self.spectral_window if self.spectral_window % 2 == 0 else self.spectral_window + 1
self.volatility_window = int(self.volatility_lookback_days * self.bars_per_day)
# 频率边界 (cycles/day)
self.low_freq_bound = 1.0 / self.low_freq_days if self.low_freq_days > 0 else float('inf')
self.high_freq_bound = 1.0 / self.high_freq_days if self.high_freq_days > 0 else 0.0
# --- 内部状态变量 ---
self.main_symbol = main_symbol
self.order_id_counter = 0
self.min_bars_needed = atr_period + 10
self.log(f"SuperTrendStrategy Initialized | ATR({atr_period}) × {atr_multiplier}")
self.indicators = indicators
self.entry_time = None # 入场时间
self.position_direction = None # 'LONG' or 'SHORT'
self.last_trend_strength = 0.0
self.last_dominant_freq = 0.0 # 主导周期(天)
self.last_volatility = 0.0 # 标准化波动率(0-1)
self.volatility_history = [] # 存储历史波动率
self.log(f"VolatilityAdaptiveSpectralStrategy Initialized (bars/day={bars_per_day}, "
f"window={self.spectral_window} bars, vol_window={self.volatility_window} bars)")
def on_open_bar(self, open_price: float, symbol: str):
"""每根K线开盘时被调用"""
self.symbol = symbol
bar_history = self.get_bar_history()
current_time = self.get_current_time()
if len(bar_history) < self.min_bars_needed or not self.trading:
# 需要足够的数据 (最大窗口 + 缓冲)
min_required = max(self.spectral_window, self.volatility_window) + 10
if len(bar_history) < min_required:
if self.enable_log and len(bar_history) % 50 == 0:
self.log(f"Waiting for {len(bar_history)}/{min_required} bars")
return
position = self.get_current_positions().get(self.symbol, 0)
position_volume = self.get_current_positions().get(self.symbol, 0)
# 提取 OHLC
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)
# 获取必要历史价格 (仅取所需部分)
recent_bars = bar_history[-(max(self.spectral_window, self.volatility_window) + 5):]
closes = np.array([b.close for b in recent_bars], dtype=np.float32)
highs = np.array([b.high for b in recent_bars], dtype=np.float32)
lows = np.array([b.low for b in recent_bars], dtype=np.float32)
# 1. 计算 ATR
atr = talib.ATR(highs, lows, closes, timeperiod=self.atr_period)
# 【核心】计算频域趋势强度 (显式傅里叶)
trend_strength, dominant_freq = self.calculate_trend_strength(closes)
self.last_trend_strength = trend_strength
self.last_dominant_freq = dominant_freq
# 2. 计算基础上下轨
hl2 = (highs + lows) / 2.0
upper_band_basic = hl2 + self.atr_multiplier * atr
lower_band_basic = hl2 - self.atr_multiplier * atr
# 【核心】计算标准化波动率 (0-1范围)
volatility = self.calculate_normalized_volatility(highs, lows, closes)
self.last_volatility = volatility
# 3. 构建 SuperTrend带方向的记忆性逻辑
n = len(closes)
supertrend = np.full(n, np.nan)
direction = np.full(n, 1) # 1 for up, -1 for down
# 检查最大持仓时间 (防止极端事件)
if self.entry_time and (current_time - self.entry_time) >= timedelta(days=self.max_hold_days):
self.log(f"Max hold time reached ({self.max_hold_days} days). Forcing exit.")
self.close_all_positions()
self.entry_time = None
self.position_direction = None
return
# 初始化
supertrend[self.atr_period] = upper_band_basic[self.atr_period]
direction[self.atr_period] = -1 # 初始假设为 downtrend
# 核心逻辑:相变入场/退出
if position_volume == 0:
self.evaluate_entry_signal(open_price, trend_strength, dominant_freq, volatility, recent_bars)
else:
self.manage_open_position(position_volume, trend_strength, volatility)
for i in range(self.atr_period + 1, n):
# 默认继承前值
supertrend[i] = supertrend[i - 1]
direction[i] = direction[i - 1]
def calculate_trend_strength(self, closes: np.array) -> (float, float):
"""
【显式傅里叶】计算低频能量占比 (完全参数化)
"""
if len(closes) < self.spectral_window:
return 0.0, 0.0
# 如果前一根是 downtrend
if direction[i - 1] == -1:
if closes[i] > upper_band_basic[i - 1]:
direction[i] = 1
supertrend[i] = lower_band_basic[i]
else:
supertrend[i] = min(upper_band_basic[i], supertrend[i - 1])
else: # 前一根是 uptrend
if closes[i] < lower_band_basic[i - 1]:
direction[i] = -1
supertrend[i] = upper_band_basic[i]
else:
supertrend[i] = max(lower_band_basic[i], supertrend[i - 1])
# 仅使用窗口内数据
window_data = closes[-self.spectral_window:]
window_mean = np.mean(window_data)
window_std = np.std(window_data)
if window_std < 1e-8:
return 0.0, 0.0
# 获取最新状态
current_direction = direction[-1]
prev_direction = direction[-2] if len(direction) >= 2 else current_direction
normalized = (window_data - window_mean) / window_std
# 4. 确定目标仓位
target_position = 0
if current_direction == 1:
if self.indicators[0].is_condition_met(*self.get_indicator_tuple()):
target_position = self.trade_volume # 做多
elif current_direction == -1:
if self.indicators[1].is_condition_met(*self.get_indicator_tuple()):
target_position = -self.trade_volume # 做空
try:
f, t, Zxx = stft(
normalized,
fs=self.bars_per_day,
nperseg=self.spectral_window,
noverlap=max(0, self.spectral_window // 2),
boundary=None,
padded=False
)
except Exception as e:
self.log(f"STFT calculation error: {str(e)}")
return 0.0, 0.0
# 5. 平仓逻辑:方向翻转即平仓(即使不开反手,也先平)
current_position = position
should_close_long = (current_position > 0) and (current_direction == -1)
should_close_short = (current_position < 0) and (current_direction == 1)
# 过滤无效频率
max_freq = self.bars_per_day / 2
valid_mask = (f >= 0) & (f <= max_freq)
if not np.any(valid_mask):
return 0.0, 0.0
# 6. 执行订单
if should_close_long or should_close_short or (target_position != current_position):
# 先平旧仓
if current_position > 0:
self.close_position("CLOSE_LONG", current_position)
elif current_position < 0:
self.close_position("CLOSE_SHORT", -current_position)
f = f[valid_mask]
Zxx = Zxx[valid_mask, :]
# 再开新仓(如果条件满足)
if target_position > 0:
self.send_market_order("BUY", target_position, "OPEN")
self.log(f"📈 SuperTrend Long | ATR={atr[-1]:.2f}, Dir=+1")
elif target_position < 0:
self.send_market_order("SELL", -target_position, "OPEN")
self.log(f"📉 SuperTrend Short | ATR={atr[-1]:.2f}, Dir=-1")
if Zxx.size == 0 or Zxx.shape[1] == 0:
return 0.0, 0.0
# --- 模板方法 ---
def on_init(self):
super().on_init()
self.cancel_all_pending_orders(self.main_symbol)
# 计算最新时间点的能量
current_energy = np.abs(Zxx[:, -1]) ** 2
# 动态频段定义
low_freq_mask = f < self.low_freq_bound
high_freq_mask = f > self.high_freq_bound
# 能量计算
low_energy = np.sum(current_energy[low_freq_mask]) if np.any(low_freq_mask) else 0.0
high_energy = np.sum(current_energy[high_freq_mask]) if np.any(high_freq_mask) else 0.0
total_energy = low_energy + high_energy + 1e-8
# 趋势强度 = 低频能量占比
trend_strength = low_energy / total_energy
# 计算主导趋势周期 (天)
dominant_freq = 0.0
if np.any(low_freq_mask) and low_energy > 0:
low_energies = current_energy[low_freq_mask]
max_idx = np.argmax(low_energies)
dominant_freq = 1.0 / (f[low_freq_mask][max_idx] + 1e-8)
return float(trend_strength), float(dominant_freq)
def calculate_normalized_volatility(self, highs: np.array, lows: np.array, closes: np.array) -> float:
"""
计算标准化波动率 (0-1范围)
步骤:
1. 计算ATR (真实波幅)
2. 标准化ATR (除以价格)
3. 归一化到0-1范围 (基于历史波动率)
"""
if len(closes) < self.volatility_window + 1:
return 0.5 # 默认中性值
# 1. 计算真实波幅 (TR)
tr1 = highs[-self.volatility_window - 1:] - lows[-self.volatility_window - 1:]
tr2 = np.abs(highs[-self.volatility_window - 1:] - np.roll(closes, 1)[-self.volatility_window - 1:])
tr3 = np.abs(lows[-self.volatility_window - 1:] - np.roll(closes, 1)[-self.volatility_window - 1:])
tr = np.maximum(tr1, np.maximum(tr2, tr3))
# 2. 计算ATR
atr = np.mean(tr[-self.volatility_window:])
# 3. 标准化ATR (除以当前价格)
current_price = closes[-1]
normalized_atr = atr / current_price if current_price > 0 else 0.0
# 4. 归一化到0-1范围 (基于历史波动率)
self.volatility_history.append(normalized_atr)
if len(self.volatility_history) > 1000: # 保留1000个历史值
self.volatility_history.pop(0)
if len(self.volatility_history) < 50: # 需要足够历史数据
return 0.5
# 使用历史50-95百分位进行归一化
low_percentile = np.percentile(self.volatility_history, 50)
high_percentile = np.percentile(self.volatility_history, 95)
if high_percentile - low_percentile < 1e-8:
return 0.5
# 归一化到0-1范围
normalized_vol = (normalized_atr - low_percentile) / (high_percentile - low_percentile + 1e-8)
normalized_vol = max(0.0, min(1.0, normalized_vol)) # 限制在0-1范围内
return normalized_vol
def evaluate_entry_signal(self, open_price: float, trend_strength: float, dominant_freq: float,
volatility: float, recent_bars: List[Bar]):
"""评估波动率条件入场信号"""
# 仅当趋势强度跨越临界点且有明确周期时入场
if trend_strength > self.trend_strength_threshold:
direction = None
trade_type = ""
# 计算价格位置 (短期vs长期均值)
window_closes = np.array([b.close for b in recent_bars[-self.spectral_window:]], dtype=np.float32)
short_avg = np.mean(window_closes[-5:])
long_avg = np.mean(window_closes)
# 添加统计显著性过滤
if abs(short_avg - long_avg) < 0.0005 * long_avg:
return
# 【核心】根据波动率环境决定交易逻辑
if volatility < self.low_vol_threshold:
# 低波动环境: 趋势策略
trade_type = "TREND"
if "BUY" in self.order_direction and short_avg > long_avg:
direction = "BUY"
elif "SELL" in self.order_direction and short_avg < long_avg:
direction = "SELL"
elif volatility > self.high_vol_threshold:
# 高波动环境: 反转策略
trade_type = "REVERSAL"
if "BUY" in self.order_direction and short_avg < long_avg:
direction = "BUY" # 价格低于均值,预期回归
elif "SELL" in self.order_direction and short_avg > long_avg:
direction = "SELL" # 价格高于均值,预期反转
else:
# 中波动环境: 谨慎策略 (需要更强信号)
trade_type = "CAUTIOUS"
if trend_strength > 0.9 and "BUY" in self.order_direction and short_avg > long_avg:
direction = "BUY"
elif trend_strength > 0.9 and "SELL" in self.order_direction and short_avg < long_avg:
direction = "SELL"
if direction:
self.log(
f"Entry: {direction} | Type={trade_type} | Strength={trend_strength:.2f} | "
f"Volatility={volatility:.2f} | ShortAvg={short_avg:.4f} vs LongAvg={long_avg:.4f}"
)
self.send_market_order(direction, self.trade_volume, "OPEN")
self.entry_time = self.get_current_time()
self.position_direction = "LONG" if direction == "BUY" else "SHORT"
def manage_open_position(self, volume: int, trend_strength: float, volatility: float):
"""管理持仓:波动率条件退出"""
# 退出条件1: 趋势强度 < 退出阈值
if trend_strength < self.exit_threshold:
direction = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
self.log(f"Exit (Strength): {direction} | Strength={trend_strength:.2f} < {self.exit_threshold}")
self.close_position(direction, abs(volume))
self.entry_time = None
self.position_direction = None
return
# 退出条件2: 波动率环境突变 (从低波动变为高波动,或反之)
if self.position_direction == "LONG" and volatility > self.high_vol_threshold * 1.2:
# 多头仓位在波动率突增时退出
self.log(
f"Exit (Volatility Spike): CLOSE_LONG | Volatility={volatility:.2f} > {self.high_vol_threshold * 1.2:.2f}")
self.close_position("CLOSE_LONG", abs(volume))
self.entry_time = None
self.position_direction = None
elif self.position_direction == "SHORT" and volatility > self.high_vol_threshold * 1.2:
# 空头仓位在波动率突增时退出
self.log(
f"Exit (Volatility Spike): CLOSE_SHORT | Volatility={volatility:.2f} > {self.high_vol_threshold * 1.2:.2f}")
self.close_position("CLOSE_SHORT", abs(volume))
self.entry_time = None
self.position_direction = None
# --- 辅助函数区 ---
def close_all_positions(self):
"""强制平仓所有头寸"""
positions = self.get_current_positions()
if not positions or self.symbol not in positions or positions[self.symbol] == 0:
return
direction = "CLOSE_LONG" if positions[self.symbol] > 0 else "CLOSE_SHORT"
self.close_position(direction, abs(positions[self.symbol]))
if self.enable_log:
self.log(f"Closed {abs(positions[self.symbol])} contracts")
def close_position(self, direction: str, volume: int):
self.send_market_order(direction, volume, offset="CLOSE")
def send_market_order(self, direction: str, volume: int, offset: str):
order_id = f"{self.symbol}_{direction}_MARKET_{self.order_id_counter}"
order_id = f"{self.symbol}_{direction[-6:]}_{self.order_id_counter}"
self.order_id_counter += 1
order = Order(
id=order_id,
@@ -147,10 +357,21 @@ class SuperTrendStrategy(Strategy):
volume=volume,
price_type="MARKET",
submitted_time=self.get_current_time(),
offset=offset,
offset=offset
)
self.send_order(order)
def on_init(self):
super().on_init()
self.cancel_all_pending_orders(self.main_symbol)
if self.enable_log:
self.log("Strategy initialized. Waiting for volatility-adaptive signals...")
def on_rollover(self, old_symbol: str, new_symbol: str):
super().on_rollover(old_symbol, new_symbol)
self.log("Rollover: SuperTrendStrategy state reset.")
if self.enable_log:
self.log(f"Rollover: {old_symbol} -> {new_symbol}. Resetting state.")
self.entry_time = None
self.position_direction = None
self.last_trend_strength = 0.0
self.volatility_history = [] # 重置波动率历史