SpectralStrategy更新:FG品种
This commit is contained in:
1386
futures_trading_strategies/FG/Spectral/SpectralTrendStrategy.ipynb
Normal file
1386
futures_trading_strategies/FG/Spectral/SpectralTrendStrategy.ipynb
Normal file
File diff suppressed because one or more lines are too long
291
futures_trading_strategies/FG/Spectral/SpectralTrendStrategy.py
Normal file
291
futures_trading_strategies/FG/Spectral/SpectralTrendStrategy.py
Normal file
@@ -0,0 +1,291 @@
|
||||
import numpy as np
|
||||
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, ZScoreATR
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 策略实现 (SpectralTrendStrategy)
|
||||
# =============================================================================
|
||||
|
||||
class SpectralTrendStrategy(Strategy):
|
||||
"""
|
||||
频域能量相变策略 - 捕获肥尾趋势
|
||||
|
||||
核心哲学:
|
||||
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__(
|
||||
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.1, # 相变临界值
|
||||
exit_threshold: float = 0.4, # 退出阈值
|
||||
# --- 【持仓管理】 ---
|
||||
max_hold_days: int = 10, # 最大持仓天数
|
||||
# --- 其他 ---
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicators: Indicator = None,
|
||||
model_indicator: Indicator = None,
|
||||
reverse: bool = False,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None:
|
||||
order_direction = ['BUY', 'SELL']
|
||||
if indicators is None:
|
||||
indicators = Empty() # 保持兼容性
|
||||
|
||||
# --- 参数赋值 (完全参数化) ---
|
||||
self.trade_volume = trade_volume
|
||||
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.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
|
||||
|
||||
# 频率边界 (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.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.reverse = reverse
|
||||
|
||||
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()
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
# 需要足够的数据 (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
|
||||
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
# 获取历史价格 (使用完整历史)
|
||||
closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
|
||||
# 【核心】计算频域趋势强度 (显式傅里叶)
|
||||
trend_strength, dominant_freq = self.calculate_trend_strength(closes)
|
||||
self.last_trend_strength = trend_strength
|
||||
self.last_dominant_freq = dominant_freq
|
||||
|
||||
# 检查最大持仓时间 (防止极端事件)
|
||||
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 self.trading:
|
||||
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)
|
||||
|
||||
def calculate_trend_strength(self, prices: np.array) -> (float, float):
|
||||
"""
|
||||
【显式傅里叶】计算低频能量占比 (完全参数化)
|
||||
|
||||
步骤:
|
||||
1. 价格归一化 (窗口内)
|
||||
2. 短时傅里叶变换 (STFT) - 采样率=bars_per_day
|
||||
3. 动态计算频段边界 (基于bars_per_day)
|
||||
4. 趋势强度 = 低频能量 / (低频+高频能量)
|
||||
"""
|
||||
# 1. 验证数据长度
|
||||
if len(prices) < self.spectral_window:
|
||||
return 0.0, 0.0
|
||||
|
||||
# 2. 价格归一化 (仅使用窗口内数据)
|
||||
window_data = prices[-self.spectral_window * 10:]
|
||||
normalized = (window_data - np.mean(window_data)) / (np.std(window_data) + 1e-8)
|
||||
normalized = normalized[-self.spectral_window:]
|
||||
|
||||
# 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
|
||||
|
||||
# 4. 过滤无效频率 (STFT返回频率范围: 0 到 fs/2)
|
||||
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
|
||||
f = f[valid_mask]
|
||||
Zxx = Zxx[valid_mask, :]
|
||||
|
||||
if Zxx.size == 0 or Zxx.shape[1] == 0:
|
||||
return 0.0, 0.0
|
||||
|
||||
# 5. 计算最新时间点的能量
|
||||
current_energy = np.abs(Zxx[:, -1]) ** 2
|
||||
|
||||
# 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
|
||||
|
||||
# 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 # 防除零
|
||||
|
||||
# 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):
|
||||
"""评估相变入场信号"""
|
||||
# 仅当趋势强度跨越临界点且有明确周期时入场
|
||||
self.log(
|
||||
f"Strength={trend_strength:.2f}")
|
||||
if trend_strength > self.trend_strength_threshold:
|
||||
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 and self.indicators.is_condition_met(*self.get_indicator_tuple()):
|
||||
if self.reverse:
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
self.log(f"Direction={direction}, Open Position")
|
||||
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")
|
||||
|
||||
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
|
||||
)
|
||||
self.send_order(order)
|
||||
|
||||
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
|
||||
1915
futures_trading_strategies/FG/Spectral/SpectralTrendStrategy2.ipynb
Normal file
1915
futures_trading_strategies/FG/Spectral/SpectralTrendStrategy2.ipynb
Normal file
File diff suppressed because one or more lines are too long
255
futures_trading_strategies/FG/Spectral/SpectralTrendStrategy2.py
Normal file
255
futures_trading_strategies/FG/Spectral/SpectralTrendStrategy2.py
Normal file
@@ -0,0 +1,255 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
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
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class SpectralTrendStrategy(Strategy):
|
||||
"""
|
||||
频域能量相变策略 - 极简回归版
|
||||
|
||||
核心哲学:
|
||||
1. 频域 (STFT): 负责"判势" —— 现在的市场是震荡(噪音主导)还是趋势(低频主导)?
|
||||
2. 时域 (Regression): 负责"定向" —— 这个低频趋势是向上的还是向下的?
|
||||
|
||||
这种组合避免了频域相位计算的复杂性和不稳定性,回归了量化的本质。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 市场参数 ---
|
||||
bars_per_day: int = 23,
|
||||
# --- 策略参数 ---
|
||||
spectral_window_days: float = 2.0,
|
||||
low_freq_days: float = 2.0,
|
||||
high_freq_days: float = 1.0,
|
||||
trend_strength_threshold: float = 0.2, # 强度阈值
|
||||
exit_threshold: float = 0.1, # 退出阈值
|
||||
slope_threshold: float = 0.0, # 斜率阈值 (0.05表示每根K线移动0.05个标准差)
|
||||
max_hold_days: int = 10,
|
||||
# --- 其他 ---
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicators: Indicator = None,
|
||||
model_indicator: Indicator = None,
|
||||
reverse: bool = False,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None:
|
||||
order_direction = ['BUY', 'SELL']
|
||||
|
||||
self.trade_volume = trade_volume
|
||||
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.slope_threshold = slope_threshold
|
||||
self.max_hold_days = max_hold_days
|
||||
self.order_direction = order_direction
|
||||
self.model_indicator = model_indicator or Empty()
|
||||
self.indicators = indicators or Empty()
|
||||
self.reverse = reverse
|
||||
|
||||
# 计算窗口大小
|
||||
self.spectral_window = int(self.spectral_window_days * self.bars_per_day)
|
||||
# 确保偶数 (STFT偏好)
|
||||
if self.spectral_window % 2 != 0:
|
||||
self.spectral_window += 1
|
||||
|
||||
# 频率边界
|
||||
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.order_id_counter = 0
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
|
||||
self.log(f"SpectralTrendStrategy (Regression) Init. Window: {self.spectral_window} bars")
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
current_time = self.get_current_time()
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
if len(bar_history) < self.spectral_window + 5:
|
||||
return
|
||||
|
||||
# 强制平仓检查
|
||||
if self.entry_time and (current_time - self.entry_time) >= timedelta(days=self.max_hold_days):
|
||||
self.close_all_positions()
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
return
|
||||
|
||||
# 获取数据并归一化
|
||||
closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
|
||||
# 计算核心指标
|
||||
trend_strength, trend_slope = self.calculate_market_state(closes)
|
||||
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
if self.trading:
|
||||
if position_volume == 0:
|
||||
self.evaluate_entry_signal(open_price, trend_strength, trend_slope)
|
||||
else:
|
||||
self.manage_open_position(position_volume, trend_strength, trend_slope)
|
||||
|
||||
def calculate_market_state(self, prices: np.array) -> (float, float):
|
||||
"""
|
||||
【显式傅里叶】计算低频能量占比 (完全参数化)
|
||||
|
||||
步骤:
|
||||
1. 价格归一化 (窗口内)
|
||||
2. 短时傅里叶变换 (STFT) - 采样率=bars_per_day
|
||||
3. 动态计算频段边界 (基于bars_per_day)
|
||||
4. 趋势强度 = 低频能量 / (低频+高频能量)
|
||||
"""
|
||||
# 1. 验证数据长度
|
||||
if len(prices) < self.spectral_window:
|
||||
return 0.0, 0.0
|
||||
|
||||
# 2. 价格归一化 (仅使用窗口内数据)
|
||||
window_data = prices[-self.spectral_window:]
|
||||
normalized = (window_data - np.mean(window_data)) / (np.std(window_data) + 1e-8)
|
||||
normalized = normalized[-self.spectral_window:]
|
||||
|
||||
# 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
|
||||
|
||||
# 4. 过滤无效频率 (STFT返回频率范围: 0 到 fs/2)
|
||||
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
|
||||
f = f[valid_mask]
|
||||
Zxx = Zxx[valid_mask, :]
|
||||
|
||||
if Zxx.size == 0 or Zxx.shape[1] == 0:
|
||||
return 0.0, 0.0
|
||||
|
||||
# 5. 计算最新时间点的能量
|
||||
current_energy = np.abs(Zxx[:, -1]) ** 2
|
||||
|
||||
# 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
|
||||
|
||||
# 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 # 防除零
|
||||
|
||||
# 8. 趋势强度 = 低频能量占比
|
||||
trend_strength = low_energy / total_energy
|
||||
|
||||
# --- 3. 时域分析 (Regression) - 只负责"方向" ---
|
||||
# 使用最小二乘法拟合一条直线 y = kx + b
|
||||
# x 是时间序列 [0, 1, 2...], y 是归一化价格
|
||||
# slope 代表:每经过一根K线,价格变化多少个标准差
|
||||
x = np.arange(len(normalized))
|
||||
slope, intercept = np.polyfit(x, normalized, 1)
|
||||
|
||||
return trend_strength, slope
|
||||
|
||||
def evaluate_entry_signal(self, open_price: float, trend_strength: float, trend_slope: float):
|
||||
"""
|
||||
入场逻辑:
|
||||
当频域告诉我们"有趋势"(Strength高),且时域告诉我们"方向明确"(Slope陡峭)时入场。
|
||||
"""
|
||||
# 1. 滤除噪音震荡 (STFT关卡)
|
||||
if trend_strength > self.trend_strength_threshold:
|
||||
|
||||
direction = None
|
||||
|
||||
# 2. 确认方向 (回归关卡)
|
||||
# slope > 0.05 意味着趋势向上且有一定力度
|
||||
if "BUY" in self.order_direction and trend_slope > self.slope_threshold:
|
||||
direction = "BUY"
|
||||
# slope < -0.05 意味着趋势向下且有一定力度
|
||||
elif "SELL" in self.order_direction and trend_slope < -self.slope_threshold:
|
||||
direction = "SELL"
|
||||
|
||||
if direction:
|
||||
# 辅助指标过滤
|
||||
if not self.indicators.is_condition_met(*self.get_indicator_tuple()):
|
||||
return
|
||||
|
||||
# 反向逻辑
|
||||
direction = direction
|
||||
if not self.model_indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
if self.reverse:
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
|
||||
self.log(f"Signal: {direction} | Strength={trend_strength:.2f} | Slope={trend_slope:.4f}")
|
||||
|
||||
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, trend_slope: float):
|
||||
"""
|
||||
离场逻辑:
|
||||
仅依赖频域能量。只要低频能量依然主导,说明趋势(无论方向)未被破坏。
|
||||
一旦能量降到 exit_threshold 以下,说明市场进入混乱/震荡,离场观望。
|
||||
"""
|
||||
if trend_strength < self.exit_threshold:
|
||||
direction = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
self.log(f"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:
|
||||
dir = "CLOSE_LONG" if positions[self.symbol] > 0 else "CLOSE_SHORT"
|
||||
self.close_position(dir, abs(positions[self.symbol]))
|
||||
|
||||
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}_MKT_{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_limit_order(self, direction: str, limit_price: float, volume: int, offset: str):
|
||||
order_id = f"{self.symbol}_{direction}_LMT_{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)
|
||||
278
futures_trading_strategies/FG/Spectral/SpectralTrendStrategy3.py
Normal file
278
futures_trading_strategies/FG/Spectral/SpectralTrendStrategy3.py
Normal file
@@ -0,0 +1,278 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
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, ADX
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class SpectralTrendStrategy(Strategy):
|
||||
"""
|
||||
频域能量相变策略 - 极简回归版 (动态ATR止损)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 市场参数 ---
|
||||
bars_per_day: int = 23,
|
||||
# --- 策略参数 ---
|
||||
spectral_window_days: float = 2.0,
|
||||
low_freq_days: float = 2.0,
|
||||
high_freq_days: float = 1.0,
|
||||
trend_strength_threshold: float = 0.2,
|
||||
exit_threshold: float = 0.1,
|
||||
slope_threshold: float = 0.0,
|
||||
max_hold_days: int = 10,
|
||||
# --- 风控参数 ---
|
||||
stop_loss_atr_multiplier: float = 2.0, # 止损距离是当前ATR的几倍
|
||||
stop_loss_atr_period: int = 14, # ATR计算周期
|
||||
# --- 其他 ---
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicators: Indicator = None,
|
||||
model_indicator: Indicator = None,
|
||||
reverse: bool = False,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None:
|
||||
order_direction = ['BUY', 'SELL']
|
||||
|
||||
self.trade_volume = trade_volume
|
||||
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.slope_threshold = slope_threshold
|
||||
self.max_hold_days = max_hold_days
|
||||
|
||||
# --- 风控参数 ---
|
||||
self.sl_atr_multiplier = stop_loss_atr_multiplier
|
||||
self.sl_atr_period = stop_loss_atr_period
|
||||
# 注意:移除了 self.stop_loss_price 状态变量,改为实时计算
|
||||
|
||||
self.order_direction = order_direction
|
||||
self.model_indicator = model_indicator or Empty()
|
||||
self.indicators = indicators or Empty()
|
||||
self.reverse = reverse
|
||||
|
||||
# 计算窗口大小
|
||||
self.spectral_window = int(self.spectral_window_days * self.bars_per_day)
|
||||
if self.spectral_window % 2 != 0:
|
||||
self.spectral_window += 1
|
||||
|
||||
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.order_id_counter = 0
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
|
||||
self.log(
|
||||
f"SpectralTrendStrategy Init. Window: {self.spectral_window}, Dynamic ATR SL: {self.sl_atr_multiplier}x")
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
current_time = self.get_current_time()
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
# 确保数据长度足够计算 STFT 和 ATR
|
||||
required_len = max(self.spectral_window, self.sl_atr_period + 5)
|
||||
if len(bar_history) < required_len:
|
||||
return
|
||||
|
||||
# 强制平仓检查 (时间)
|
||||
# if self.entry_time and (current_time - self.entry_time) >= timedelta(days=self.max_hold_days):
|
||||
# self.close_all_positions(reason="MaxHoldDays")
|
||||
# return
|
||||
|
||||
# 获取数据用于 STFT
|
||||
closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
|
||||
# --- 计算 ATR (每一根Bar都计算最新的ATR) ---
|
||||
atr_window = self.sl_atr_period + 10
|
||||
highs_atr = np.array([b.high for b in bar_history[-atr_window:]], dtype=float)
|
||||
lows_atr = np.array([b.low for b in bar_history[-atr_window:]], dtype=float)
|
||||
closes_atr = np.array([b.close for b in bar_history[-atr_window:]], dtype=float)
|
||||
|
||||
try:
|
||||
atr_values = talib.ATR(highs_atr, lows_atr, closes_atr, timeperiod=self.sl_atr_period)
|
||||
current_atr = atr_values[-1]
|
||||
except Exception as e:
|
||||
self.log(f"ATR Calculation Error: {e}")
|
||||
current_atr = 0.0
|
||||
|
||||
|
||||
|
||||
# 计算核心指标
|
||||
trend_strength, trend_slope = self.calculate_market_state(closes)
|
||||
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
if self.trading:
|
||||
if position_volume == 0:
|
||||
self.evaluate_entry_signal(open_price, trend_strength, trend_slope)
|
||||
else:
|
||||
# 传入 current_atr 用于动态止损计算
|
||||
self.manage_open_position(position_volume, trend_strength, trend_slope, open_price, current_atr)
|
||||
|
||||
def calculate_market_state(self, prices: np.array) -> (float, float):
|
||||
# ... (此处逻辑保持不变) ...
|
||||
if len(prices) < self.spectral_window:
|
||||
return 0.0, 0.0
|
||||
|
||||
window_data = prices[-self.spectral_window:]
|
||||
normalized = (window_data - np.mean(window_data)) / (np.std(window_data) + 1e-8)
|
||||
normalized = normalized[-self.spectral_window:]
|
||||
|
||||
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:
|
||||
return 0.0, 0.0
|
||||
|
||||
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
|
||||
f = f[valid_mask]
|
||||
Zxx = Zxx[valid_mask, :]
|
||||
|
||||
if Zxx.size == 0 or Zxx.shape[1] == 0:
|
||||
return 0.0, 0.0
|
||||
|
||||
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
|
||||
|
||||
x = np.arange(len(normalized))
|
||||
slope, intercept = np.polyfit(x, normalized, 1)
|
||||
|
||||
return trend_strength, slope
|
||||
|
||||
def evaluate_entry_signal(self, open_price: float, trend_strength: float, trend_slope: float):
|
||||
"""
|
||||
入场逻辑:不再计算止损价,只负责开仓
|
||||
"""
|
||||
if trend_strength > self.trend_strength_threshold:
|
||||
direction = None
|
||||
|
||||
if "BUY" in self.order_direction and trend_slope > self.slope_threshold:
|
||||
direction = "BUY"
|
||||
elif "SELL" in self.order_direction and trend_slope < -self.slope_threshold:
|
||||
direction = "SELL"
|
||||
|
||||
if direction:
|
||||
if not self.indicators.is_condition_met(*self.get_indicator_tuple()):
|
||||
return
|
||||
|
||||
if not self.model_indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
if self.reverse:
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
|
||||
self.log(f"Signal: {direction} | Strength={trend_strength:.2f} | Slope={trend_slope:.4f}")
|
||||
|
||||
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, trend_slope: float, current_price: float,
|
||||
current_atr: float):
|
||||
"""
|
||||
离场逻辑:实时计算均价止损
|
||||
"""
|
||||
# --- 1. 动态ATR止损检查 ---
|
||||
# 获取持仓均价
|
||||
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
# 确保 ATR 和 均价 有效
|
||||
if current_atr > 0 and avg_entry_price > 0:
|
||||
is_stop_loss = False
|
||||
exit_dir = ""
|
||||
stop_price = 0.0
|
||||
|
||||
sl_distance = current_atr * self.sl_atr_multiplier
|
||||
|
||||
# 多头持仓:止损价 = 均价 - N * ATR
|
||||
if volume > 0:
|
||||
stop_price = avg_entry_price - sl_distance
|
||||
if current_price <= stop_price:
|
||||
is_stop_loss = True
|
||||
exit_dir = "CLOSE_LONG"
|
||||
|
||||
# 空头持仓:止损价 = 均价 + N * ATR
|
||||
elif volume < 0:
|
||||
stop_price = avg_entry_price + sl_distance
|
||||
if current_price >= stop_price:
|
||||
is_stop_loss = True
|
||||
exit_dir = "CLOSE_SHORT"
|
||||
|
||||
if is_stop_loss:
|
||||
self.log(
|
||||
f"ATR STOP LOSS: {exit_dir} | Current={current_price:.2f} | AvgEntry={avg_entry_price:.2f} | ATR={current_atr:.2f} | StopPrice={stop_price:.2f}")
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
return # 止损触发后直接返回
|
||||
|
||||
# --- 2. 信号离场 (原能量逻辑) ---
|
||||
if trend_strength < self.exit_threshold:
|
||||
direction = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
self.log(f"Exit (Signal): {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, reason=""):
|
||||
positions = self.get_current_positions()
|
||||
if self.symbol in positions and positions[self.symbol] != 0:
|
||||
dir = "CLOSE_LONG" if positions[self.symbol] > 0 else "CLOSE_SHORT"
|
||||
self.log(f"Close All ({reason}): {dir}")
|
||||
self.close_position(dir, abs(positions[self.symbol]))
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
|
||||
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}_MKT_{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_limit_order(self, direction: str, limit_price: float, volume: int, offset: str):
|
||||
order_id = f"{self.symbol}_{direction}_LMT_{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)
|
||||
File diff suppressed because one or more lines are too long
111
futures_trading_strategies/FG/Spectral/utils.py
Normal file
111
futures_trading_strategies/FG/Spectral/utils.py
Normal file
@@ -0,0 +1,111 @@
|
||||
import multiprocessing
|
||||
from typing import Tuple, Dict, Any, Optional
|
||||
|
||||
from src.analysis.result_analyzer import ResultAnalyzer
|
||||
from src.backtest_engine import BacktestEngine
|
||||
from src.data_manager import DataManager
|
||||
|
||||
|
||||
# --- 单个回测任务函数 ---
|
||||
# 这个函数将在每个独立的进程中运行,因此它必须是自包含的
|
||||
def run_single_backtest(
|
||||
combination: Tuple[float, float], # 传入当前参数组合
|
||||
common_config: Dict[str, Any] # 传入公共配置 (如数据路径, 初始资金等)
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
运行单个参数组合的回测任务。
|
||||
此函数将在一个独立的进程中执行。
|
||||
"""
|
||||
p1_value, p2_value = combination
|
||||
|
||||
# 从 common_config 中获取必要的配置
|
||||
symbol = common_config['symbol']
|
||||
data_path = common_config['data_path']
|
||||
initial_capital = common_config['initial_capital']
|
||||
slippage_rate = common_config['slippage_rate']
|
||||
commission_rate = common_config['commission_rate']
|
||||
start_time = common_config['start_time']
|
||||
end_time = common_config['end_time']
|
||||
roll_over_mode = common_config['roll_over_mode']
|
||||
# bar_duration_seconds = common_config['bar_duration_seconds'] # 如果DataManager需要,可以再传
|
||||
param1_name = common_config['param1_name']
|
||||
param2_name = common_config['param2_name']
|
||||
|
||||
# 每个进程内部独立初始化 DataManager 和 BacktestEngine
|
||||
# 确保每个进程有自己的数据副本和模拟状态
|
||||
data_manager = DataManager(
|
||||
file_path=data_path,
|
||||
symbol=symbol,
|
||||
# bar_duration_seconds=bar_duration_seconds, # 如果DataManager需要,根据数据文件路径推断或者额外参数传入
|
||||
# start_date=start_time.date(), # DataManager 现在通过 file_path 和 symbol 处理数据
|
||||
# end_date=end_time.date(),
|
||||
)
|
||||
# data_manager.load_data() # DataManager 内部加载数据
|
||||
|
||||
strategy_parameters = {
|
||||
'main_symbol': common_config['main_symbol'],
|
||||
'trade_volume': 1,
|
||||
param1_name: p1_value, # 15分钟扫荡K线下影线占其总范围的最小比例。
|
||||
param2_name: p2_value, # 15分钟限价单的入场点位于扫荡K线低点到收盘价的斐波那契回撤比例。
|
||||
'order_direction': common_config['order_direction'],
|
||||
'enable_log': False, # 建议在调试和测试时开启日志
|
||||
}
|
||||
# strategy_parameters['spectral_window_days'] = 2
|
||||
strategy_parameters['low_freq_days'] = strategy_parameters['spectral_window_days']
|
||||
strategy_parameters['high_freq_days'] = int(strategy_parameters['spectral_window_days'] / 2)
|
||||
strategy_parameters['exit_threshold'] = max(strategy_parameters['trend_strength_threshold'] - 0.3, 0)
|
||||
|
||||
if 'reverse' in common_config:
|
||||
strategy_parameters['reverse'] = common_config['reverse']
|
||||
|
||||
# 打印当前进程正在处理的组合信息
|
||||
# 注意:多进程打印会交错显示
|
||||
print(f"--- 正在运行组合: {strategy_parameters} (PID: {multiprocessing.current_process().pid}) ---")
|
||||
|
||||
try:
|
||||
# 初始化回测引擎
|
||||
engine = BacktestEngine(
|
||||
data_manager=data_manager,
|
||||
strategy_class=common_config['strategy'],
|
||||
strategy_params=strategy_parameters,
|
||||
initial_capital=initial_capital,
|
||||
slippage_rate=slippage_rate,
|
||||
commission_rate=commission_rate,
|
||||
roll_over_mode=True, # 保持换月模式
|
||||
start_time=common_config['start_time'],
|
||||
end_time=common_config['end_time']
|
||||
)
|
||||
# 运行回测,传入时间范围
|
||||
engine.run_backtest()
|
||||
|
||||
# 获取回测结果并分析
|
||||
results = engine.get_backtest_results()
|
||||
portfolio_snapshots = results["portfolio_snapshots"]
|
||||
trade_history = results["trade_history"]
|
||||
bars = results["all_bars"]
|
||||
initial_capital_result = results["initial_capital"]
|
||||
|
||||
if portfolio_snapshots:
|
||||
analyzer = ResultAnalyzer(portfolio_snapshots, trade_history, bars, initial_capital_result)
|
||||
|
||||
# analyzer.generate_report()
|
||||
# analyzer.plot_performance()
|
||||
metrics = analyzer.calculate_all_metrics()
|
||||
|
||||
# 将当前组合的参数和性能指标存储起来
|
||||
result_entry = {**strategy_parameters, **metrics}
|
||||
return result_entry
|
||||
else:
|
||||
print(
|
||||
f" 组合 {strategy_parameters} 没有生成投资组合快照,无法进行结果分析。(PID: {multiprocessing.current_process().pid})")
|
||||
# 返回一个包含参数和默认0值的结果,以便追踪失败组合
|
||||
return {**strategy_parameters, "total_return": 0.0, "annualized_return": 0.0, "sharpe_ratio": 0.0,
|
||||
"max_drawdown": 0.0, "error": "No portfolio snapshots"}
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
print(
|
||||
f" 组合 {strategy_parameters} 运行失败: {e}\n{error_trace} (PID: {multiprocessing.current_process().pid})")
|
||||
# 返回错误信息,以便后续处理
|
||||
return {**strategy_parameters, "error": str(e), "traceback": error_trace}
|
||||
|
||||
Reference in New Issue
Block a user