SpectralStrategy更新
This commit is contained in:
1375
futures_trading_strategies/MA/Spectral/SpectralTrendStrategy.ipynb
Normal file
1375
futures_trading_strategies/MA/Spectral/SpectralTrendStrategy.ipynb
Normal file
File diff suppressed because one or more lines are too long
291
futures_trading_strategies/MA/Spectral/SpectralTrendStrategy.py
Normal file
291
futures_trading_strategies/MA/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/MA/Spectral/SpectralTrendStrategy2.ipynb
Normal file
1915
futures_trading_strategies/MA/Spectral/SpectralTrendStrategy2.ipynb
Normal file
File diff suppressed because one or more lines are too long
255
futures_trading_strategies/MA/Spectral/SpectralTrendStrategy2.py
Normal file
255
futures_trading_strategies/MA/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)
|
||||
193
futures_trading_strategies/MA/Spectral/SpectralTrendStrategy3.py
Normal file
193
futures_trading_strategies/MA/Spectral/SpectralTrendStrategy3.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import numpy as np
|
||||
from typing import Optional, Any, List
|
||||
from src.core_data import Bar, Order
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class SemiVarianceAsymmetryStrategy(Strategy):
|
||||
"""
|
||||
已实现半方差不对称策略 (RSVA)
|
||||
|
||||
核心原理:
|
||||
放弃"阈值计数",改用"波动能量占比"。
|
||||
因子 = (上行波动能量 - 下行波动能量) / 总波动能量
|
||||
|
||||
优势:
|
||||
1. 自适应:自动适应2021的高波动和2023的低波动,无需调整阈值。
|
||||
2. 灵敏:能捕捉到没有大阳线但持续上涨的"蠕动趋势"。
|
||||
3. 稳健:使用平方项(Variance)而非三次方(Skewness),对异常值更鲁棒。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 窗口参数 ---
|
||||
season_days: int = 20, # 计算日内季节性基准的回溯天数
|
||||
calc_window: int = 120, # 计算不对称因子的窗口 (约5天)
|
||||
cycle_length: int = 23, # 固定周期 (每天23根Bar)
|
||||
|
||||
# --- 信号阈值 ---
|
||||
# RSVA 范围是 [-1, 1]。
|
||||
# 0.2 表示上涨能量比下跌能量多20% (即 60% vs 40%),是一个显著的失衡信号。
|
||||
entry_threshold: float = 0.2,
|
||||
exit_threshold: float = 0.05,
|
||||
|
||||
order_direction: Optional[List[str]] = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None:
|
||||
order_direction = ['BUY', 'SELL']
|
||||
|
||||
self.trade_volume = trade_volume
|
||||
self.season_days = season_days
|
||||
self.calc_window = calc_window
|
||||
self.cycle_length = cycle_length
|
||||
self.entry_threshold = entry_threshold
|
||||
self.exit_threshold = exit_threshold
|
||||
self.order_direction = order_direction
|
||||
|
||||
# 计算最小历史需求
|
||||
# 我们需要: calc_window 个标准化数据
|
||||
# 每个标准化数据需要回溯: season_days * cycle_length
|
||||
self.min_history = self.calc_window + (self.season_days * self.cycle_length)
|
||||
|
||||
# 缓冲区设大一点,避免频繁触发边界检查
|
||||
self.calc_buffer_size = self.min_history + 100
|
||||
|
||||
self.log(f"RSVA Strategy Init: Window={calc_window}, Thresh={entry_threshold}")
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
|
||||
# 1. 获取历史数据 (切片优化)
|
||||
all_history = self.get_bar_history()
|
||||
total_len = len(all_history)
|
||||
|
||||
if total_len < self.min_history:
|
||||
return
|
||||
|
||||
# 只取计算所需的最后一段数据,保证计算复杂度恒定
|
||||
start_idx = max(0, total_len - self.calc_buffer_size)
|
||||
relevant_bars = all_history[start_idx:]
|
||||
|
||||
# 转为 numpy array
|
||||
closes = np.array([b.close for b in relevant_bars])
|
||||
|
||||
# 2. 计算对数收益率 (Log Returns)
|
||||
# 对数收益率消除了价格水平(Price Level)的影响
|
||||
log_rets = np.diff(np.log(closes))
|
||||
current_idx = len(log_rets) - 1
|
||||
|
||||
# 3. 标准化收益率计算 (De-seasonalization)
|
||||
# 这一步至关重要:剔除日内季节性(早盘波动大、午盘波动小)的干扰
|
||||
std_rets = []
|
||||
|
||||
# 循环计算过去 calc_window 个点的标准化值
|
||||
for i in range(self.calc_window):
|
||||
target_idx = current_idx - i
|
||||
|
||||
# 高效切片:利用 stride=cycle_length 提取同一时间槽的历史
|
||||
# slot_history 包含 [t, t-23, t-46, ...]
|
||||
slot_history = log_rets[target_idx::-self.cycle_length]
|
||||
|
||||
# 截取 season_days
|
||||
if len(slot_history) > self.season_days:
|
||||
slot_history = slot_history[:self.season_days]
|
||||
|
||||
# 计算该时刻的基准波动率
|
||||
if len(slot_history) < 5:
|
||||
# 降级处理:样本不足时用近期全局波动率
|
||||
slot_vol = np.std(log_rets[-self.cycle_length:]) + 1e-9
|
||||
else:
|
||||
slot_vol = np.std(slot_history) + 1e-9
|
||||
|
||||
# 标准化 (Z-Score)
|
||||
std_ret = log_rets[target_idx] / slot_vol
|
||||
std_rets.append(std_ret)
|
||||
|
||||
# 转为数组 (注意:std_rets 是倒序的,但这不影响平方和计算)
|
||||
std_rets_arr = np.array(std_rets)
|
||||
|
||||
# 4. 【核心】计算已实现半方差不对称性 (RSVA)
|
||||
|
||||
# 分离正收益和负收益
|
||||
pos_rets = std_rets_arr[std_rets_arr > 0]
|
||||
neg_rets = std_rets_arr[std_rets_arr < 0]
|
||||
|
||||
# 计算上行能量 (Upside Variance) 和 下行能量 (Downside Variance)
|
||||
rv_pos = np.sum(pos_rets ** 2)
|
||||
rv_neg = np.sum(neg_rets ** 2)
|
||||
total_rv = rv_pos + rv_neg + 1e-9 # 防止除零
|
||||
|
||||
# 计算因子: [-1, 1]
|
||||
# > 0 说明上涨更有力(或更频繁),< 0 说明下跌主导
|
||||
rsva_factor = (rv_pos - rv_neg) / total_rv
|
||||
|
||||
# 5. 交易逻辑
|
||||
current_pos = self.get_current_positions().get(symbol, 0)
|
||||
|
||||
self.log_status(rsva_factor, rv_pos, rv_neg, current_pos)
|
||||
|
||||
if current_pos == 0:
|
||||
self.evaluate_entry(rsva_factor)
|
||||
else:
|
||||
self.evaluate_exit(current_pos, rsva_factor)
|
||||
|
||||
def evaluate_entry(self, factor: float):
|
||||
direction = None
|
||||
|
||||
# 因子 > 0.2: 哪怕没有极端K线,只要累计的上涨能量显著压过下跌能量,就开仓
|
||||
if factor > self.entry_threshold:
|
||||
if "BUY" in self.order_direction:
|
||||
direction = "BUY"
|
||||
|
||||
elif factor < -self.entry_threshold:
|
||||
if "SELL" in self.order_direction:
|
||||
direction = "SELL"
|
||||
|
||||
if direction:
|
||||
self.log(f"ENTRY: {direction} | RSVA={factor:.4f}")
|
||||
self.send_market_order(direction, self.trade_volume, "OPEN")
|
||||
|
||||
def evaluate_exit(self, volume: int, factor: float):
|
||||
do_exit = False
|
||||
reason = ""
|
||||
|
||||
# 当多空能量趋于平衡 (因子回到 0 附近),说明趋势动能耗尽,平仓
|
||||
# 这种离场方式对震荡市非常友好:一旦陷入震荡,rv_pos 和 rv_neg 会迅速接近,因子归零
|
||||
if volume > 0 and factor < self.exit_threshold:
|
||||
do_exit = True
|
||||
reason = f"Bull Energy Fade (RSVA={factor:.4f})"
|
||||
|
||||
elif volume < 0 and factor > -self.exit_threshold:
|
||||
do_exit = True
|
||||
reason = f"Bear Energy Fade (RSVA={factor:.4f})"
|
||||
|
||||
if do_exit:
|
||||
direction = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
self.log(f"EXIT: {reason}")
|
||||
self.send_market_order(direction, abs(volume), "CLOSE")
|
||||
|
||||
def send_market_order(self, direction: str, volume: int, offset: str):
|
||||
# 严格遵守要求:使用 get_current_time()
|
||||
current_time = self.get_current_time()
|
||||
|
||||
order = Order(
|
||||
id=f"{self.main_symbol}_{direction}_{current_time.timestamp()}",
|
||||
symbol=self.symbol,
|
||||
direction=direction,
|
||||
volume=volume,
|
||||
price_type="MARKET",
|
||||
submitted_time=current_time,
|
||||
offset=offset
|
||||
)
|
||||
self.send_order(order)
|
||||
|
||||
def log_status(self, factor: float, pos_e: float, neg_e: float, current_pos: int):
|
||||
if self.enable_log:
|
||||
# 仅在有持仓或信号明显时打印
|
||||
if current_pos != 0 or abs(factor) > self.entry_threshold * 0.8:
|
||||
self.log(f"Status: Pos={current_pos} | RSVA={factor:.4f} | Energy(+/-)={pos_e:.1f}/{neg_e:.1f}")
|
||||
File diff suppressed because one or more lines are too long
108
futures_trading_strategies/MA/Spectral/utils.py
Normal file
108
futures_trading_strategies/MA/Spectral/utils.py
Normal file
@@ -0,0 +1,108 @@
|
||||
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)
|
||||
|
||||
# 打印当前进程正在处理的组合信息
|
||||
# 注意:多进程打印会交错显示
|
||||
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}
|
||||
|
||||
@@ -107,7 +107,7 @@ class SpectralTrendStrategy(Strategy):
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
# 获取历史价格 (使用完整历史)
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
|
||||
# 【核心】计算频域趋势强度 (显式傅里叶)
|
||||
trend_strength, dominant_freq = self.calculate_trend_strength(closes)
|
||||
|
||||
26332
futures_trading_strategies/rb/Spectral/SpectralTrendStrategy.ipynb
Normal file
26332
futures_trading_strategies/rb/Spectral/SpectralTrendStrategy.ipynb
Normal file
File diff suppressed because one or more lines are too long
285
futures_trading_strategies/rb/Spectral/SpectralTrendStrategy.py
Normal file
285
futures_trading_strategies/rb/Spectral/SpectralTrendStrategy.py
Normal file
@@ -0,0 +1,285 @@
|
||||
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: Optional[List[Indicator]] = None,
|
||||
model_indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None:
|
||||
order_direction = ['BUY', 'SELL']
|
||||
if indicators is None:
|
||||
indicators = [Empty(), 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.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:]
|
||||
normalized = (window_data - np.mean(window_data)) / (np.std(window_data) + 1e-8)
|
||||
|
||||
# 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
|
||||
and self.model_indicator.is_condition_met(*self.get_indicator_tuple())):
|
||||
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.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
|
||||
200
futures_trading_strategies/rb/Spectral/SpectralTrendStrategy2.py
Normal file
200
futures_trading_strategies/rb/Spectral/SpectralTrendStrategy2.py
Normal file
@@ -0,0 +1,200 @@
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pywt
|
||||
|
||||
from src.core_data import Order
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 策略实现 (WaveletDynamicsStrategy - 全新动态分析策略)
|
||||
# =============================================================================
|
||||
|
||||
class WaveletSignalNoiseStrategy(Strategy):
|
||||
"""
|
||||
小波信噪比策略 (最终版)
|
||||
|
||||
核心哲学:
|
||||
1. 信任小波: 策略完全基于小波变换最独特的“信号/噪音”分离能力。
|
||||
2. 简洁因子: 使用一个核心因子——趋势信噪比(TNR),衡量趋势的质量。
|
||||
3. 可靠逻辑:
|
||||
- 当信噪比高(趋势清晰)时入场。
|
||||
- 当信噪比低(噪音过大)时出场。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 【核心参数】 ---
|
||||
bars_per_day: int = 23,
|
||||
analysis_window_days: float = 2.0, # 窗口长度适中即可
|
||||
wavelet_family: str = 'db4',
|
||||
# --- 【信噪比交易阈值】 ---
|
||||
tnr_entry_threshold: float = 5, # 入场阈值:信号强度至少是噪音的2倍
|
||||
tnr_exit_threshold: float = 5, # 离场阈值:信号强度不再显著高于噪音
|
||||
# --- 【持仓管理】 ---
|
||||
max_hold_days: int = 10,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
# ... (参数赋值) ...
|
||||
self.bars_per_day = bars_per_day
|
||||
self.analysis_window_days = analysis_window_days
|
||||
self.wavelet = wavelet_family
|
||||
self.tnr_entry_threshold = tnr_entry_threshold
|
||||
self.tnr_exit_threshold = tnr_exit_threshold
|
||||
self.trade_volume = trade_volume
|
||||
self.max_hold_days = max_hold_days
|
||||
|
||||
self.analysis_window = int(self.analysis_window_days * self.bars_per_day)
|
||||
self.decomposition_level = pywt.dwt_max_level(self.analysis_window, self.wavelet)
|
||||
|
||||
self.entry_time = None
|
||||
self.order_id_counter = 0
|
||||
self.log("WaveletSignalNoiseStrategy Initialized.")
|
||||
|
||||
def calculate_trend_noise_ratio(self, prices: np.array) -> (float, np.array):
|
||||
"""
|
||||
【最终核心】计算趋势信噪比(TNR)和内在趋势线
|
||||
返回: (tnr_factor, trend_signal)
|
||||
"""
|
||||
if len(prices) < self.analysis_window:
|
||||
return 0.0, None
|
||||
|
||||
window_data = prices[-self.analysis_window:]
|
||||
|
||||
try:
|
||||
coeffs = pywt.wavedec(window_data, self.wavelet, level=self.decomposition_level)
|
||||
|
||||
# 1. 重构内在趋势信号 (Signal)
|
||||
trend_coeffs = [coeffs[0]] + [np.zeros_like(d) for d in coeffs[1:]]
|
||||
trend_signal = pywt.waverec(trend_coeffs, self.wavelet)
|
||||
trend_signal = trend_signal[:len(window_data)]
|
||||
|
||||
# 2. 重构噪音信号 (Noise)
|
||||
noise_coeffs = [np.zeros_like(coeffs[0])] + coeffs[1:]
|
||||
noise_signal = pywt.waverec(noise_coeffs, self.wavelet)
|
||||
noise_signal = noise_signal[:len(window_data)]
|
||||
|
||||
# 3. 计算各自的强度 (标准差)
|
||||
strength_trend = np.std(trend_signal)
|
||||
strength_noise = np.std(noise_signal)
|
||||
|
||||
# 4. 计算信噪比因子
|
||||
if strength_noise < 1e-9: # 避免除以零
|
||||
tnr_factor = np.inf
|
||||
else:
|
||||
tnr_factor = strength_trend / strength_noise
|
||||
|
||||
return tnr_factor, trend_signal
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"TNR calculation error: {e}", "ERROR")
|
||||
return 0.0, None
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
if len(bar_history) < self.analysis_window:
|
||||
return
|
||||
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
tnr_factor, trend_signal = self.calculate_trend_noise_ratio(closes)
|
||||
|
||||
if trend_signal is None: return
|
||||
|
||||
if position_volume == 0:
|
||||
self.evaluate_entry_signal(open_price, tnr_factor, trend_signal)
|
||||
else:
|
||||
self.manage_open_position(position_volume, tnr_factor)
|
||||
|
||||
def evaluate_entry_signal(self, open_price: float, tnr_factor: float, trend_signal: np.array):
|
||||
"""入场逻辑:信噪比达标 + 方向确认"""
|
||||
if tnr_factor < self.tnr_entry_threshold:
|
||||
return
|
||||
|
||||
direction = None
|
||||
# 方向判断:内在趋势线的斜率
|
||||
# if len(trend_signal) < 5: return
|
||||
|
||||
if trend_signal[-1] > trend_signal[-5]:
|
||||
direction = "SELL"
|
||||
elif trend_signal[-1] < trend_signal[-5]:
|
||||
direction = "BUY"
|
||||
|
||||
if direction:
|
||||
self.log(f"Entry Signal: {direction} | Trend-Noise Ratio={tnr_factor:.2f}")
|
||||
self.entry_time = self.get_current_time()
|
||||
self.send_limit_order(direction, open_price, self.trade_volume, "OPEN")
|
||||
|
||||
def manage_open_position(self, volume: int, tnr_factor: float):
|
||||
"""出场逻辑:信噪比低于退出阈值"""
|
||||
if tnr_factor < self.tnr_exit_threshold:
|
||||
direction_str = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
self.log(f"Exit Signal: TNR ({tnr_factor:.2f}) < Threshold ({self.tnr_exit_threshold})")
|
||||
self.close_position(direction_str, abs(volume))
|
||||
self.entry_time = 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
|
||||
File diff suppressed because one or more lines are too long
103
futures_trading_strategies/rb/Spectral/utils.py
Normal file
103
futures_trading_strategies/rb/Spectral/utils.py
Normal file
@@ -0,0 +1,103 @@
|
||||
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, # 建议在调试和测试时开启日志
|
||||
}
|
||||
# 打印当前进程正在处理的组合信息
|
||||
# 注意:多进程打印会交错显示
|
||||
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}
|
||||
|
||||
1479
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy.ipynb
Normal file
1479
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy.ipynb
Normal file
File diff suppressed because one or more lines are too long
291
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy.py
Normal file
291
futures_trading_strategies/ru/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/ru/Spectral/SpectralTrendStrategy2.ipynb
Normal file
1915
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy2.ipynb
Normal file
File diff suppressed because one or more lines are too long
255
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy2.py
Normal file
255
futures_trading_strategies/ru/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)
|
||||
193
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy3.py
Normal file
193
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy3.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import numpy as np
|
||||
from typing import Optional, Any, List
|
||||
from src.core_data import Bar, Order
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class SemiVarianceAsymmetryStrategy(Strategy):
|
||||
"""
|
||||
已实现半方差不对称策略 (RSVA)
|
||||
|
||||
核心原理:
|
||||
放弃"阈值计数",改用"波动能量占比"。
|
||||
因子 = (上行波动能量 - 下行波动能量) / 总波动能量
|
||||
|
||||
优势:
|
||||
1. 自适应:自动适应2021的高波动和2023的低波动,无需调整阈值。
|
||||
2. 灵敏:能捕捉到没有大阳线但持续上涨的"蠕动趋势"。
|
||||
3. 稳健:使用平方项(Variance)而非三次方(Skewness),对异常值更鲁棒。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 窗口参数 ---
|
||||
season_days: int = 20, # 计算日内季节性基准的回溯天数
|
||||
calc_window: int = 120, # 计算不对称因子的窗口 (约5天)
|
||||
cycle_length: int = 23, # 固定周期 (每天23根Bar)
|
||||
|
||||
# --- 信号阈值 ---
|
||||
# RSVA 范围是 [-1, 1]。
|
||||
# 0.2 表示上涨能量比下跌能量多20% (即 60% vs 40%),是一个显著的失衡信号。
|
||||
entry_threshold: float = 0.2,
|
||||
exit_threshold: float = 0.05,
|
||||
|
||||
order_direction: Optional[List[str]] = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None:
|
||||
order_direction = ['BUY', 'SELL']
|
||||
|
||||
self.trade_volume = trade_volume
|
||||
self.season_days = season_days
|
||||
self.calc_window = calc_window
|
||||
self.cycle_length = cycle_length
|
||||
self.entry_threshold = entry_threshold
|
||||
self.exit_threshold = exit_threshold
|
||||
self.order_direction = order_direction
|
||||
|
||||
# 计算最小历史需求
|
||||
# 我们需要: calc_window 个标准化数据
|
||||
# 每个标准化数据需要回溯: season_days * cycle_length
|
||||
self.min_history = self.calc_window + (self.season_days * self.cycle_length)
|
||||
|
||||
# 缓冲区设大一点,避免频繁触发边界检查
|
||||
self.calc_buffer_size = self.min_history + 100
|
||||
|
||||
self.log(f"RSVA Strategy Init: Window={calc_window}, Thresh={entry_threshold}")
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
|
||||
# 1. 获取历史数据 (切片优化)
|
||||
all_history = self.get_bar_history()
|
||||
total_len = len(all_history)
|
||||
|
||||
if total_len < self.min_history:
|
||||
return
|
||||
|
||||
# 只取计算所需的最后一段数据,保证计算复杂度恒定
|
||||
start_idx = max(0, total_len - self.calc_buffer_size)
|
||||
relevant_bars = all_history[start_idx:]
|
||||
|
||||
# 转为 numpy array
|
||||
closes = np.array([b.close for b in relevant_bars])
|
||||
|
||||
# 2. 计算对数收益率 (Log Returns)
|
||||
# 对数收益率消除了价格水平(Price Level)的影响
|
||||
log_rets = np.diff(np.log(closes))
|
||||
current_idx = len(log_rets) - 1
|
||||
|
||||
# 3. 标准化收益率计算 (De-seasonalization)
|
||||
# 这一步至关重要:剔除日内季节性(早盘波动大、午盘波动小)的干扰
|
||||
std_rets = []
|
||||
|
||||
# 循环计算过去 calc_window 个点的标准化值
|
||||
for i in range(self.calc_window):
|
||||
target_idx = current_idx - i
|
||||
|
||||
# 高效切片:利用 stride=cycle_length 提取同一时间槽的历史
|
||||
# slot_history 包含 [t, t-23, t-46, ...]
|
||||
slot_history = log_rets[target_idx::-self.cycle_length]
|
||||
|
||||
# 截取 season_days
|
||||
if len(slot_history) > self.season_days:
|
||||
slot_history = slot_history[:self.season_days]
|
||||
|
||||
# 计算该时刻的基准波动率
|
||||
if len(slot_history) < 5:
|
||||
# 降级处理:样本不足时用近期全局波动率
|
||||
slot_vol = np.std(log_rets[-self.cycle_length:]) + 1e-9
|
||||
else:
|
||||
slot_vol = np.std(slot_history) + 1e-9
|
||||
|
||||
# 标准化 (Z-Score)
|
||||
std_ret = log_rets[target_idx] / slot_vol
|
||||
std_rets.append(std_ret)
|
||||
|
||||
# 转为数组 (注意:std_rets 是倒序的,但这不影响平方和计算)
|
||||
std_rets_arr = np.array(std_rets)
|
||||
|
||||
# 4. 【核心】计算已实现半方差不对称性 (RSVA)
|
||||
|
||||
# 分离正收益和负收益
|
||||
pos_rets = std_rets_arr[std_rets_arr > 0]
|
||||
neg_rets = std_rets_arr[std_rets_arr < 0]
|
||||
|
||||
# 计算上行能量 (Upside Variance) 和 下行能量 (Downside Variance)
|
||||
rv_pos = np.sum(pos_rets ** 2)
|
||||
rv_neg = np.sum(neg_rets ** 2)
|
||||
total_rv = rv_pos + rv_neg + 1e-9 # 防止除零
|
||||
|
||||
# 计算因子: [-1, 1]
|
||||
# > 0 说明上涨更有力(或更频繁),< 0 说明下跌主导
|
||||
rsva_factor = (rv_pos - rv_neg) / total_rv
|
||||
|
||||
# 5. 交易逻辑
|
||||
current_pos = self.get_current_positions().get(symbol, 0)
|
||||
|
||||
self.log_status(rsva_factor, rv_pos, rv_neg, current_pos)
|
||||
|
||||
if current_pos == 0:
|
||||
self.evaluate_entry(rsva_factor)
|
||||
else:
|
||||
self.evaluate_exit(current_pos, rsva_factor)
|
||||
|
||||
def evaluate_entry(self, factor: float):
|
||||
direction = None
|
||||
|
||||
# 因子 > 0.2: 哪怕没有极端K线,只要累计的上涨能量显著压过下跌能量,就开仓
|
||||
if factor > self.entry_threshold:
|
||||
if "BUY" in self.order_direction:
|
||||
direction = "BUY"
|
||||
|
||||
elif factor < -self.entry_threshold:
|
||||
if "SELL" in self.order_direction:
|
||||
direction = "SELL"
|
||||
|
||||
if direction:
|
||||
self.log(f"ENTRY: {direction} | RSVA={factor:.4f}")
|
||||
self.send_market_order(direction, self.trade_volume, "OPEN")
|
||||
|
||||
def evaluate_exit(self, volume: int, factor: float):
|
||||
do_exit = False
|
||||
reason = ""
|
||||
|
||||
# 当多空能量趋于平衡 (因子回到 0 附近),说明趋势动能耗尽,平仓
|
||||
# 这种离场方式对震荡市非常友好:一旦陷入震荡,rv_pos 和 rv_neg 会迅速接近,因子归零
|
||||
if volume > 0 and factor < self.exit_threshold:
|
||||
do_exit = True
|
||||
reason = f"Bull Energy Fade (RSVA={factor:.4f})"
|
||||
|
||||
elif volume < 0 and factor > -self.exit_threshold:
|
||||
do_exit = True
|
||||
reason = f"Bear Energy Fade (RSVA={factor:.4f})"
|
||||
|
||||
if do_exit:
|
||||
direction = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
self.log(f"EXIT: {reason}")
|
||||
self.send_market_order(direction, abs(volume), "CLOSE")
|
||||
|
||||
def send_market_order(self, direction: str, volume: int, offset: str):
|
||||
# 严格遵守要求:使用 get_current_time()
|
||||
current_time = self.get_current_time()
|
||||
|
||||
order = Order(
|
||||
id=f"{self.main_symbol}_{direction}_{current_time.timestamp()}",
|
||||
symbol=self.symbol,
|
||||
direction=direction,
|
||||
volume=volume,
|
||||
price_type="MARKET",
|
||||
submitted_time=current_time,
|
||||
offset=offset
|
||||
)
|
||||
self.send_order(order)
|
||||
|
||||
def log_status(self, factor: float, pos_e: float, neg_e: float, current_pos: int):
|
||||
if self.enable_log:
|
||||
# 仅在有持仓或信号明显时打印
|
||||
if current_pos != 0 or abs(factor) > self.entry_threshold * 0.8:
|
||||
self.log(f"Status: Pos={current_pos} | RSVA={factor:.4f} | Energy(+/-)={pos_e:.1f}/{neg_e:.1f}")
|
||||
File diff suppressed because one or more lines are too long
108
futures_trading_strategies/ru/Spectral/utils.py
Normal file
108
futures_trading_strategies/ru/Spectral/utils.py
Normal file
@@ -0,0 +1,108 @@
|
||||
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)
|
||||
|
||||
# 打印当前进程正在处理的组合信息
|
||||
# 注意:多进程打印会交错显示
|
||||
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}
|
||||
|
||||
322
logs/fu_Open.log
322
logs/fu_Open.log
@@ -1,322 +0,0 @@
|
||||
/home/liaozhaorun/miniconda3/bin/conda run -n quant --no-capture-output python /home/liaozhaorun/.pycharm_helpers/pydev/pydevconsole.py --mode=client --host=127.0.0.1 --port=45113
|
||||
import sys; print('Python %s on %s' % (sys.version, sys.platform))
|
||||
sys.path.extend(['/mnt/d/PyProject/NewQuant'])
|
||||
PyDev console: using IPython 9.3.0
|
||||
Python 3.12.11 | packaged by Anaconda, Inc. | (main, Jun 5 2025, 13:09:17) [GCC 11.2.0] on linux
|
||||
runfile('/mnt/d/PyProject/NewQuant/real_trading/fu_Open.py', wdir='/mnt/d/PyProject/NewQuant/real_trading')
|
||||
/home/liaozhaorun/miniconda3/envs/quant/lib/python3.12/site-packages/requests/__init__.py:86: RequestsDependencyWarning: Unable to find acceptable character detection dependency (chardet or charset_normalizer).
|
||||
warnings.warn(
|
||||
在使用天勤量化之前,默认您已经知晓并同意以下免责条款,如果不同意请立即停止使用:https://www.shinnytech.com/blog/disclaimer/
|
||||
2025-08-01 08:20:50 - INFO - TqSdk free 版剩余 0 天到期,如需续费或升级请访问 https://account.shinnytech.com/ 或联系相关工作人员。
|
||||
sh: 1: dmidecode: not found
|
||||
sh: 1: dmidecode: not found
|
||||
2025-08-01 08:20:51 - INFO - 通知 : 与 wss://free-api.shinnytech.com/t/nfmd/front/mobile 的网络连接已建立
|
||||
2025-08-01 08:20:51 - INFO - 通知 903308830: 与 wss://kqtjd.hongyuanqh.com:37443/trade 的网络连接已建立
|
||||
2025-08-01 08:20:52 - INFO - 通知 903308830: 已经连接到交易前置
|
||||
2025-08-01 08:20:52 - INFO - 通知 903308830: 登录成功
|
||||
初始化 Tqsdk 回测引擎...
|
||||
TqsdkContext: 初始化完成。
|
||||
[2025-08-01 08:20:56] 策略 (fu): 策略初始化: symbol=fu, trade_volume=3, range_factor_l=0.7, profit_factor_l=0.3, range_factor_s=1.9, profit_factor_s=0.2, max_position=10, 止损点=20, 止盈点=10
|
||||
TqsdkContext: 已设置引擎引用。
|
||||
TqsdkEngine: 初始化完成。
|
||||
TqsdkEngine: 开始加载历史数据,加载k线数量50
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.460432] - 当前Open=2868.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.460846] - 当前Open=2889.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.461156] - 当前Open=2883.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.461505] - 当前Open=2869.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.461900] - 当前Open=2879.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.462199] - 当前Open=2882.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.462483] - 当前Open=2862.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.462763] - 当前Open=2854.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.463045] - 当前Open=2865.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.463318] - 当前Open=2856.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.463572] - 当前Open=2855.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.463813] - 当前Open=2860.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.464086] - 当前Open=2856.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.464402] - 当前Open=2879.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.464674] - 当前Open=2875.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.464932] - 当前Open=2889.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.465190] - 当前Open=2912.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2912.0 13.299999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.465190] SHORT信号 - 当前Open=2912.00, 前1Range=7.00, 计算目标SHORT价=2925.30
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.465190] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.465595] - 当前Open=2905.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2905.0 6.3
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.465595] LONG信号 - 当前Open=2905.00, 前1Range=9.00, 计算目标LONG价=2898.70
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.465595] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.466002] - 当前Open=2897.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2897.0 6.3
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.466002] LONG信号 - 当前Open=2897.00, 前1Range=9.00, 计算目标LONG价=2890.70
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.466002] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.466540] - 当前Open=2917.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2917.0 60.8
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.466540] SHORT信号 - 当前Open=2917.00, 前1Range=32.00, 计算目标SHORT价=2977.80
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.466540] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.466842] - 当前Open=2906.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2906.0 26.599999999999998
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.466842] SHORT信号 - 当前Open=2906.00, 前1Range=14.00, 计算目标SHORT价=2932.60
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.466842] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.467183] - 当前Open=2854.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2854.0 58.9
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.467183] SHORT信号 - 当前Open=2854.00, 前1Range=31.00, 计算目标SHORT价=2912.90
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.467183] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.467495] - 当前Open=2832.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2832.0 57.0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.467495] SHORT信号 - 当前Open=2832.00, 前1Range=30.00, 计算目标SHORT价=2889.00
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.467495] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.467812] - 当前Open=2849.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2849.0 30.4
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.467812] SHORT信号 - 当前Open=2849.00, 前1Range=16.00, 计算目标SHORT价=2879.40
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.467812] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.468131] - 当前Open=2861.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2861.0 11.899999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.468131] LONG信号 - 当前Open=2861.00, 前1Range=17.00, 计算目标LONG价=2849.10
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.468131] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.468480] - 当前Open=2856.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2856.0 15.399999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.468480] LONG信号 - 当前Open=2856.00, 前1Range=22.00, 计算目标LONG价=2840.60
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.468480] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.468811] - 当前Open=2858.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2858.0 15.399999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.468811] LONG信号 - 当前Open=2858.00, 前1Range=22.00, 计算目标LONG价=2842.60
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.468811] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.469161] - 当前Open=2894.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2894.0 38.5
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.469161] LONG信号 - 当前Open=2894.00, 前1Range=55.00, 计算目标LONG价=2855.50
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.469161] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.469480] - 当前Open=2918.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2918.0 47.5
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.469480] SHORT信号 - 当前Open=2918.00, 前1Range=25.00, 计算目标SHORT价=2965.50
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.469480] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.469785] - 当前Open=2913.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2913.0 55.099999999999994
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.469785] SHORT信号 - 当前Open=2913.00, 前1Range=29.00, 计算目标SHORT价=2968.10
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.469785] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.470155] - 当前Open=2912.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2912.0 12.6
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.470155] LONG信号 - 当前Open=2912.00, 前1Range=18.00, 计算目标LONG价=2899.40
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.470155] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.470500] - 当前Open=2918.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2918.0 10.5
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.470500] LONG信号 - 当前Open=2918.00, 前1Range=15.00, 计算目标LONG价=2907.50
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.470500] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.471020] - 当前Open=2905.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2905.0 6.3
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.471020] LONG信号 - 当前Open=2905.00, 前1Range=9.00, 计算目标LONG价=2898.70
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.471020] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.471552] - 当前Open=2913.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2913.0 13.299999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.471552] LONG信号 - 当前Open=2913.00, 前1Range=19.00, 计算目标LONG价=2899.70
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.471552] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.471907] - 当前Open=2931.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2931.0 32.9
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.471907] LONG信号 - 当前Open=2931.00, 前1Range=47.00, 计算目标LONG价=2898.10
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.471907] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.472267] - 当前Open=2925.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2925.0 15.399999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.472267] LONG信号 - 当前Open=2925.00, 前1Range=22.00, 计算目标LONG价=2909.60
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.472267] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.472594] - 当前Open=2966.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2966.0 9.799999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.472594] LONG信号 - 当前Open=2966.00, 前1Range=14.00, 计算目标LONG价=2956.20
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.472594] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.472943] - 当前Open=2962.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2962.0 20.9
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.472943] SHORT信号 - 当前Open=2962.00, 前1Range=11.00, 计算目标SHORT价=2982.90
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.472943] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.473285] - 当前Open=2948.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2948.0 7.0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.473285] LONG信号 - 当前Open=2948.00, 前1Range=10.00, 计算目标LONG价=2941.00
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.473285] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.473612] - 当前Open=2947.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2947.0 7.0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.473612] LONG信号 - 当前Open=2947.00, 前1Range=10.00, 计算目标LONG价=2940.00
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.473612] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.473938] - 当前Open=2944.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2944.0 7.0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.473938] LONG信号 - 当前Open=2944.00, 前1Range=10.00, 计算目标LONG价=2937.00
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.473938] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.474267] - 当前Open=2965.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2965.0 11.899999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.474267] LONG信号 - 当前Open=2965.00, 前1Range=17.00, 计算目标LONG价=2953.10
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.474267] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.474630] - 当前Open=2960.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2960.0 19.0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.474630] SHORT信号 - 当前Open=2960.00, 前1Range=10.00, 计算目标SHORT价=2979.00
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.474630] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.475095] - 当前Open=2965.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2965.0 21.7
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.475095] LONG信号 - 当前Open=2965.00, 前1Range=31.00, 计算目标LONG价=2943.30
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.475095] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.475529] - 当前Open=2963.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2963.0 11.899999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.475529] LONG信号 - 当前Open=2963.00, 前1Range=17.00, 计算目标LONG价=2951.10
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.475529] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.475920] - 当前Open=2963.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2963.0 6.3
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.475920] LONG信号 - 当前Open=2963.00, 前1Range=9.00, 计算目标LONG价=2956.70
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.475920] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.476293] - 当前Open=2954.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2954.0 9.799999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.476293] LONG信号 - 当前Open=2954.00, 前1Range=14.00, 计算目标LONG价=2944.20
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.476293] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.476630] - 当前Open=2946.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2946.0 13.299999999999999
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.476630] LONG信号 - 当前Open=2946.00, 前1Range=19.00, 计算目标LONG价=2932.70
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.476630] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.476957] - 当前Open=2923.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2923.0 43.699999999999996
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.476957] SHORT信号 - 当前Open=2923.00, 前1Range=23.00, 计算目标SHORT价=2966.70
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.476957] 策略: 发送订单失败。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.477293] - 当前Open=2909.00,
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): current_pos_volume: 0
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 2909.0 24.7
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.477293] SHORT信号 - 当前Open=2909.00, 前1Range=13.00, 计算目标SHORT价=2933.70
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): [2025-08-01 08:20:56.477293] 策略: 发送订单失败。
|
||||
TqsdkEngine: 加载历史k线完成, bars数量:50,last bar datetime:2025-07-31 21:00:00+08:00
|
||||
Bar(datetime=Timestamp('2025-07-31 10:00:00+0800', tz='Asia/Shanghai'), open=np.float64(2963.0), high=np.float64(2967.0), low=np.float64(2955.0), close=np.float64(2963.0), volume=np.float64(31962.0), open_oi=np.float64(159243.0), close_oi=np.float64(159146.0), symbol='SHFE.fu2509')
|
||||
Bar(datetime=Timestamp('2025-07-31 11:00:00+0800', tz='Asia/Shanghai'), open=np.float64(2963.0), high=np.float64(2965.0), low=np.float64(2953.0), close=np.float64(2957.0), volume=np.float64(18866.0), open_oi=np.float64(159146.0), close_oi=np.float64(156996.0), symbol='SHFE.fu2509')
|
||||
Bar(datetime=Timestamp('2025-07-31 13:00:00+0800', tz='Asia/Shanghai'), open=np.float64(2954.0), high=np.float64(2954.0), low=np.float64(2938.0), close=np.float64(2946.0), volume=np.float64(36186.0), open_oi=np.float64(156996.0), close_oi=np.float64(153873.0), symbol='SHFE.fu2509')
|
||||
Bar(datetime=Timestamp('2025-07-31 14:00:00+0800', tz='Asia/Shanghai'), open=np.float64(2946.0), high=np.float64(2951.0), low=np.float64(2928.0), close=np.float64(2933.0), volume=np.float64(67426.0), open_oi=np.float64(153873.0), close_oi=np.float64(148798.0), symbol='SHFE.fu2509')
|
||||
Bar(datetime=Timestamp('2025-07-31 21:00:00+0800', tz='Asia/Shanghai'), open=np.float64(2923.0), high=np.float64(2925.0), low=np.float64(2890.0), close=np.float64(2909.0), volume=np.float64(158785.0), open_oi=np.float64(148798.0), close_oi=np.float64(141443.0), symbol='SHFE.fu2509')
|
||||
TqsdkEngine: self._last_underlying_symbol:SHFE.fu2509, is_trading_time:False
|
||||
SimpleLimitBuyStrategy 策略初始化回调被调用。
|
||||
[2025-08-01 08:20:56] 策略 (KQ.m@SHFE.fu): 取消0笔订单
|
||||
TqsdkEngine: 开始等待最新数据
|
||||
self.klines.iloc[-1].open: 2909.0
|
||||
TqsdkEngine: open change!, open:2915.0, now: 2025-08-01 09:00:00.179981
|
||||
TqsdkEngine: 新k线产生, k line datetime:2025-08-01 09:00:00+08:00, now: 2025-08-01 09:00:02.092037
|
||||
[2025-08-01 09:00:02] 策略 (SHFE.fu2509): [2025-08-01 09:00:02.092470] - 当前Open=2915.00,
|
||||
[2025-08-01 09:00:02] 策略 (SHFE.fu2509): 取消0笔订单
|
||||
[2025-08-01 09:00:02] 策略 (SHFE.fu2509): current_pos_volume: 0
|
||||
[2025-08-01 09:00:02] 策略 (SHFE.fu2509): 2915.0 19.0
|
||||
[2025-08-01 09:00:02] 策略 (SHFE.fu2509): [2025-08-01 09:00:02.092470] SHORT信号 - 当前Open=2915.00, 前1Range=10.00, 计算目标SHORT价=2934.00
|
||||
Context: 订单已加入队列: Order(symbol='SHFE.fu2509', direction='SELL', volume=3, id='SHFE.fu2509_BUY_20250801090002_34', price_type='LIMIT', limit_price=2934, stop_price=None, submitted_time=datetime.datetime(2025, 8, 1, 9, 0, 2, 92470), offset='OPEN')
|
||||
[2025-08-01 09:00:02] 策略 (SHFE.fu2509): [2025-08-01 09:00:02.092470] 策略: 发送限价SHORT订单 SHFE.fu2509_BUY_20250801090002_34 @ 2934.00
|
||||
Engine: 处理订单请求: Order(symbol='SHFE.fu2509', direction='SELL', volume=3, id='SHFE.fu2509_BUY_20250801090002_34', price_type='LIMIT', limit_price=2934, stop_price=None, submitted_time=datetime.datetime(2025, 8, 1, 9, 0, 2, 92470), offset='OPEN')
|
||||
2025-08-01 09:00:02 - INFO - 通知 903308830: 下单成功,委托,CZCE.MA509,2384,2手,买,开仓
|
||||
2025-08-01 09:00:02 - INFO - 通知 903308830: 下单成功,委托,SHFE.fu2509,2934,3手,卖,开仓
|
||||
TqsdkEngine: open change!, open:2916.0, now: 2025-08-01 09:00:02.167304
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
self.klines.iloc[-1].open: 2916.0
|
||||
@@ -234,7 +234,7 @@ class ResultAnalyzer:
|
||||
# 绘制最大值标注
|
||||
plt.axvline(optimal_indi_value, color="red", linestyle="--", alpha=0.7)
|
||||
plt.annotate(
|
||||
f"Max Cum. PnL: {max_cumulative_pnl:.2f}",
|
||||
f"optimal_indi_value: {optimal_indi_value:.4f}",
|
||||
xy=(optimal_indi_value, max_cumulative_pnl),
|
||||
xytext=(max_xytext_x, max_cumulative_pnl),
|
||||
arrowprops=dict(facecolor="red", shrink=0.05),
|
||||
@@ -247,7 +247,7 @@ class ResultAnalyzer:
|
||||
plt.axvline(min_indi_value_at_pnl, color="blue", linestyle=":", alpha=0.7)
|
||||
min_text_y_offset = -(max_cumulative_pnl - min_cumulative_pnl) * 0.1
|
||||
plt.annotate(
|
||||
f"Min Cum. PnL: {min_cumulative_pnl:.2f}",
|
||||
f"min_indi_value_at_pnl: {min_indi_value_at_pnl:.4f}",
|
||||
xy=(min_indi_value_at_pnl, min_cumulative_pnl),
|
||||
xytext=(min_xytext_x, min_cumulative_pnl + min_text_y_offset),
|
||||
arrowprops=dict(facecolor="blue", shrink=0.05),
|
||||
@@ -265,3 +265,157 @@ class ResultAnalyzer:
|
||||
plt.show()
|
||||
|
||||
print("\n所有指标分析完成。")
|
||||
|
||||
def analyze_indicators_v2(self, profit_offset: float = 0.0) -> None:
|
||||
"""
|
||||
分析指标值区间与盈亏的关系。
|
||||
核心逻辑:
|
||||
1. 按指标值排序。
|
||||
2. 计算累积盈亏。
|
||||
3. 找出累积盈亏曲线上涨幅度最大的一段,即为“最佳盈利区间”。
|
||||
"""
|
||||
|
||||
# 1. 分离开仓和平仓
|
||||
open_trades = [t for t in self.trade_history if t.is_open_trade]
|
||||
close_trades = [t for t in self.trade_history if t.is_close_trade]
|
||||
|
||||
if not close_trades:
|
||||
print("没有平仓交易可供分析。")
|
||||
return
|
||||
|
||||
num_pairs = min(len(open_trades), len(close_trades))
|
||||
if num_pairs == 0:
|
||||
return
|
||||
|
||||
print(f"正在分析 {num_pairs} 组交易...")
|
||||
|
||||
for indicator in self.indicator_list:
|
||||
indicator_name = indicator.get_name()
|
||||
|
||||
indi_values = []
|
||||
pnls = []
|
||||
|
||||
# 2. 收集数据
|
||||
for i in range(num_pairs):
|
||||
open_trade = open_trades[i]
|
||||
close_trade = close_trades[i]
|
||||
|
||||
if (open_trade.indicator_dict is not None and
|
||||
indicator_name in open_trade.indicator_dict):
|
||||
|
||||
value = open_trade.indicator_dict[indicator_name]
|
||||
if not (isinstance(value, float) and np.isnan(value)):
|
||||
indi_values.append(value)
|
||||
pnls.append(close_trade.realized_pnl - profit_offset)
|
||||
|
||||
if not indi_values:
|
||||
continue
|
||||
|
||||
# 3. 创建 DataFrame 并清洗
|
||||
df = pd.DataFrame({
|
||||
"indicator_value": indi_values,
|
||||
"realized_pnl": pnls
|
||||
})
|
||||
|
||||
# 去极值
|
||||
def remove_extreme(d, col='indicator_value', k=3):
|
||||
q1, q3 = d[col].quantile([0.25, 0.75])
|
||||
iqr = q3 - q1
|
||||
mask = d[col].between(q1 - k * iqr, q3 + k * iqr)
|
||||
return d[mask].copy()
|
||||
|
||||
df = remove_extreme(df)
|
||||
if df.empty:
|
||||
continue
|
||||
|
||||
# ==========================================================
|
||||
# 4. 核心计算:排序与累积
|
||||
# ==========================================================
|
||||
df = df.sort_values(by="indicator_value").reset_index(drop=True)
|
||||
df["cumulative_pnl"] = df["realized_pnl"].cumsum()
|
||||
|
||||
x_values = df["indicator_value"].values
|
||||
y_values = df["cumulative_pnl"].values
|
||||
|
||||
# ==========================================================
|
||||
# 5. 寻找“最佳盈利区间”算法
|
||||
# 目标:找到索引 i 和 j (i < j),使得 y[j] - y[i] 最大
|
||||
# ==========================================================
|
||||
min_pnl_so_far = float('inf')
|
||||
min_idx_so_far = -1
|
||||
|
||||
best_profit = -float('inf')
|
||||
start_idx = -1
|
||||
end_idx = -1
|
||||
|
||||
# 简单的线性扫描算法 O(N)
|
||||
for idx, current_pnl in enumerate(y_values):
|
||||
# 更新此前的最低点(作为潜在的起点)
|
||||
if current_pnl < min_pnl_so_far:
|
||||
min_pnl_so_far = current_pnl
|
||||
min_idx_so_far = idx
|
||||
|
||||
# 计算如果在当前点卖出,从最低点买入能赚多少
|
||||
current_drawup = current_pnl - min_pnl_so_far
|
||||
|
||||
if current_drawup > best_profit:
|
||||
best_profit = current_drawup
|
||||
start_idx = min_idx_so_far
|
||||
end_idx = idx
|
||||
|
||||
# 获取最佳区间的数值
|
||||
best_start_val = x_values[start_idx] if start_idx != -1 else x_values[0]
|
||||
best_end_val = x_values[end_idx] if end_idx != -1 else x_values[-1]
|
||||
|
||||
# 同时也获取全局最低点和最高点用于展示
|
||||
global_min_idx = np.argmin(y_values)
|
||||
global_max_idx = np.argmax(y_values)
|
||||
|
||||
# ==========================================================
|
||||
# 6. 绘图
|
||||
# ==========================================================
|
||||
plt.figure(figsize=(12, 7))
|
||||
|
||||
# 绘制主曲线
|
||||
plt.plot(x_values, y_values, label="Cumulative PnL", color='#1f77b4', drawstyle='steps-post')
|
||||
|
||||
# --- 标记 A: 全局最低点 (蓝点) ---
|
||||
plt.plot(x_values[global_min_idx], y_values[global_min_idx], 'v', color='blue', markersize=8,
|
||||
label='Global Min')
|
||||
|
||||
# --- 标记 B: 全局最高点 (红点) ---
|
||||
plt.plot(x_values[global_max_idx], y_values[global_max_idx], '^', color='red', markersize=8,
|
||||
label='Global Max')
|
||||
|
||||
# --- 标记 C: 最佳盈利区间 (绿色阴影区域) ---
|
||||
if start_idx != -1 and end_idx != -1 and start_idx < end_idx:
|
||||
# 在图上画出绿色区间背景
|
||||
plt.axvspan(best_start_val, best_end_val, color='green', alpha=0.15, label='Best Profit Interval')
|
||||
|
||||
# 标注区间信息
|
||||
mid_x = (best_start_val + best_end_val) / 2
|
||||
mid_y = (y_values[start_idx] + y_values[end_idx]) / 2
|
||||
|
||||
plt.annotate(
|
||||
f"Best Interval: [{best_start_val:.2f}, {best_end_val:.2f}]\n"
|
||||
f"Section Profit: {best_profit:.2f}",
|
||||
xy=(best_end_val, y_values[end_idx]),
|
||||
xytext=(20, -40),
|
||||
textcoords="offset points",
|
||||
bbox=dict(boxstyle="round,pad=0.3", fc="white", ec="green", alpha=0.9),
|
||||
arrowprops=dict(arrowstyle="->", connectionstyle="arc3,rad=.2", color='green')
|
||||
)
|
||||
|
||||
# 标记起涨点和止盈点
|
||||
plt.plot(best_start_val, y_values[start_idx], 'go', markersize=6)
|
||||
plt.plot(best_end_val, y_values[end_idx], 'go', markersize=6)
|
||||
|
||||
plt.axhline(0, color='black', linewidth=1, linestyle='--', alpha=0.3)
|
||||
plt.title(f"Indicator: {indicator_name} - Interval Analysis")
|
||||
plt.xlabel("Indicator Value")
|
||||
plt.ylabel("Cumulative PnL")
|
||||
plt.legend(loc='best')
|
||||
plt.grid(True, alpha=0.3)
|
||||
plt.show()
|
||||
|
||||
print("分析完成。")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
from functools import reduce
|
||||
from typing import List, Literal
|
||||
|
||||
import numpy as np
|
||||
|
||||
@@ -47,27 +48,47 @@ class Indicator(ABC):
|
||||
|
||||
|
||||
class CompositeIndicator(Indicator):
|
||||
def __init__(self, indicators: List[Indicator], down_bound=None, up_bound=None, shift_window=0):
|
||||
# 聚合指标通常不使用自身的 bound 和 shift_window,但为兼容基类保留
|
||||
"""
|
||||
组合指标类:支持多种逻辑方式组合多个子指标。
|
||||
|
||||
Modes:
|
||||
- 'and': 所有指标都满足时返回 True (逻辑与)
|
||||
- 'or' : 任一指标满足时返回 True (逻辑或)
|
||||
- 'xnor': 符号乘法逻辑 (同或)。用于策略叠加,如 (False, False) -> True。
|
||||
实现逻辑为累积相等性检查,等同于数学上的符号相乘:
|
||||
T(1) * T(1) = T
|
||||
T(1) * F(-1) = F
|
||||
F(-1) * F(-1) = T
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
indicators: List[Indicator],
|
||||
mode: Literal['and', 'or', 'xnor'] = 'and',
|
||||
down_bound=None,
|
||||
up_bound=None,
|
||||
shift_window=0):
|
||||
|
||||
super().__init__(down_bound=down_bound, up_bound=up_bound, shift_window=shift_window)
|
||||
|
||||
if not indicators:
|
||||
raise ValueError("At least one indicator is required.")
|
||||
|
||||
self.indicators = indicators
|
||||
self.mode = mode.lower()
|
||||
|
||||
valid_modes = ['and', 'or', 'xnor']
|
||||
if self.mode not in valid_modes:
|
||||
raise ValueError(f"Invalid mode '{mode}'. Allowed modes: {valid_modes}")
|
||||
|
||||
def get_values(self, close: np.array, open: np.array, high: np.array, low: np.array, volume: np.array):
|
||||
# 聚合指标本身不产生数值序列,返回空数组或 None
|
||||
# 但为保持类型一致,返回一个长度匹配的 dummy array(如全1)
|
||||
# 或者更合理:返回与输入等长的布尔数组(表示每时刻是否所有条件满足)
|
||||
# 这里选择后者,增强实用性
|
||||
"""
|
||||
这里尝试向量化计算所有历史数据的信号状态。
|
||||
注意:前提是子指标有能力返回布尔数组或可以被向量化判断。
|
||||
如果子指标只能单点判断,这里只能返回 None 或 dummy。
|
||||
"""
|
||||
# 实际工程中,建议子指标都实现一个返回 boolean array 的方法。
|
||||
# 这里为了保持兼容性,依然返回 dummy,或者你可以扩展逻辑进行循环计算(效率较低)
|
||||
n = len(close)
|
||||
result = np.ones(n, dtype=bool)
|
||||
for ind in self.indicators:
|
||||
# 获取每个子指标的 condition 满足情况(需自定义辅助方法)
|
||||
# 但原 Indicator 没有提供 per-timestamp condition,所以简化处理:
|
||||
# 我们只关心最新值,因此 get_values 对 Composite 意义不大
|
||||
pass
|
||||
# 保守起见:返回 None 或抛出 NotImplementedError
|
||||
# 但为避免破坏调用链,返回一个 dummy array
|
||||
return np.full(n, np.nan)
|
||||
|
||||
def is_condition_met(self,
|
||||
@@ -75,13 +96,32 @@ class CompositeIndicator(Indicator):
|
||||
open: np.array,
|
||||
high: np.array,
|
||||
low: np.array,
|
||||
volume: np.array):
|
||||
# 关键逻辑:所有子 indicator 的 is_condition_met 必须为 True
|
||||
for indicator in self.indicators:
|
||||
if not indicator.is_condition_met(close, open, high, low, volume):
|
||||
volume: np.array) -> bool:
|
||||
|
||||
# 1. 获取所有子指标的当前状态结果 (List[bool])
|
||||
results = [ind.is_condition_met(close, open, high, low, volume) for ind in self.indicators]
|
||||
|
||||
# 2. 根据模式进行逻辑聚合
|
||||
if self.mode == 'and':
|
||||
# 逻辑与:全为真才为真
|
||||
return all(results)
|
||||
|
||||
elif self.mode == 'or':
|
||||
# 逻辑或:只要有一个为真即为真
|
||||
return any(results)
|
||||
|
||||
elif self.mode == 'xnor':
|
||||
# 逻辑同或 (符号乘法):
|
||||
# 使用 reduce 累积进行 '==' 运算
|
||||
# [True, True] -> True
|
||||
# [True, False] -> False
|
||||
# [False, False] -> True (负负得正)
|
||||
# [True, False, False] -> True (1 * -1 * -1 = 1)
|
||||
return reduce(lambda x, y: x == y, results)
|
||||
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_name(self):
|
||||
return '.'.join([indicator.get_name() for indicator in self.indicators])
|
||||
|
||||
# 让名字体现出组合逻辑,方便日志调试
|
||||
separator = f"_{self.mode.upper()}_"
|
||||
return f"({separator.join([ind.get_name() for ind in self.indicators])})"
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
from src.indicators.indicators import *
|
||||
|
||||
INDICATOR_LIST = [
|
||||
RSI(5),
|
||||
RSI(7),
|
||||
RSI(10),
|
||||
RSI(14),
|
||||
RSI(15),
|
||||
RSI(20),
|
||||
RSI(25),
|
||||
RSI(30),
|
||||
RSI(35),
|
||||
RSI(40),
|
||||
Hurst(23),
|
||||
Hurst(23 * 5),
|
||||
Hurst(23 * 10),
|
||||
HistoricalRange(shift_window=0),
|
||||
HistoricalRange(shift_window=1),
|
||||
HistoricalRange(shift_window=6),
|
||||
@@ -20,7 +18,7 @@ INDICATOR_LIST = [
|
||||
# DifferencedVolumeIndicator(shift_window=6),
|
||||
# DifferencedVolumeIndicator(shift_window=13),
|
||||
# DifferencedVolumeIndicator(shift_window=20),
|
||||
StochasticOscillator(fastk_period=14, slowd_period=3, slowk_period=3),
|
||||
StochasticOscillator(14, 3, 3),
|
||||
StochasticOscillator(5, 3, 3),
|
||||
StochasticOscillator(21, 5, 5),
|
||||
RateOfChange(5),
|
||||
@@ -36,6 +34,9 @@ INDICATOR_LIST = [
|
||||
NormalizedATR(5),
|
||||
NormalizedATR(14),
|
||||
NormalizedATR(21),
|
||||
LogNormalizedATR(5),
|
||||
LogNormalizedATR(14),
|
||||
LogNormalizedATR(21),
|
||||
ADX(7),
|
||||
ADX(14),
|
||||
ADX(30),
|
||||
@@ -59,10 +60,6 @@ INDICATOR_LIST = [
|
||||
ZScoreATR(14, 100),
|
||||
FFTTrendStrength(46, 2, 23),
|
||||
FFTTrendStrength(46, 1, 23),
|
||||
AtrVolatility(7),
|
||||
AtrVolatility(14),
|
||||
AtrVolatility(21),
|
||||
AtrVolatility(230),
|
||||
FFTPhaseShift(),
|
||||
VolatilitySkew(),
|
||||
VolatilityTrendRelationship()
|
||||
|
||||
@@ -68,6 +68,97 @@ class RSI(Indicator):
|
||||
return f"rsi_{self.window}"
|
||||
|
||||
|
||||
class Hurst(Indicator):
|
||||
"""
|
||||
Hurst 指数 (Hurst Exponent) 实现
|
||||
注:TA-Lib 无内置 Hurst,此处使用 Numpy 向量化计算,效率极高。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
window: int = 100, # 建议设置 60 以上,太短的数据计算 Hurst 只有噪声
|
||||
down_bound: float = None,
|
||||
up_bound: float = None,
|
||||
shift_window: int = 0,
|
||||
):
|
||||
super().__init__(down_bound, up_bound)
|
||||
self.window = window
|
||||
self.shift_window = shift_window
|
||||
|
||||
def get_values(
|
||||
self,
|
||||
close: np.array,
|
||||
open: np.array, # 不使用
|
||||
high: np.array, # 不使用
|
||||
low: np.array, # 不使用
|
||||
volume: np.array, # 不使用
|
||||
) -> np.array:
|
||||
"""
|
||||
计算滚动 Hurst 指数。
|
||||
Args:
|
||||
close (np.array): 收盘价数组
|
||||
Returns:
|
||||
np.array: Hurst 值数组
|
||||
"""
|
||||
# 0. 基础检查
|
||||
if len(close) < self.window:
|
||||
return np.full(len(close), np.nan)
|
||||
|
||||
# 1. 计算对数收益率 (Log Returns)
|
||||
# 长度 = N - 1
|
||||
log_ret = np.diff(np.log(close))
|
||||
|
||||
# 2. 准备滚动窗口 (Vectorized Rolling)
|
||||
# 我们需要在 log_ret 上滑动,窗口大小为 window - 1
|
||||
N = self.window - 1
|
||||
|
||||
# sliding_window_view 创建视图,不占用额外内存,速度快
|
||||
# shape: (num_windows, N)
|
||||
try:
|
||||
windows = sliding_window_view(log_ret, window_shape=N)
|
||||
except AttributeError:
|
||||
raise ImportError("请升级 numpy >= 1.20 以使用 sliding_window_view")
|
||||
|
||||
# --- 以下是 R/S 分析的核心步骤 (全部并行计算) ---
|
||||
|
||||
# 3.1 计算每个窗口内的均值
|
||||
# axis=1 表示沿着窗口内部计算
|
||||
means = np.mean(windows, axis=1, keepdims=True)
|
||||
|
||||
# 3.2 离差 (Center data): 减去窗口内的均值
|
||||
centered = windows - means
|
||||
|
||||
# 3.3 累积离差 (Cumulative Deviation)
|
||||
cum_dev = np.cumsum(centered, axis=1)
|
||||
|
||||
# 3.4 极差 R (Range): 最大累积离差 - 最小累积离差
|
||||
R = np.max(cum_dev, axis=1) - np.min(cum_dev, axis=1)
|
||||
|
||||
# 3.5 标准差 S (Standard Deviation)
|
||||
# ddof=1 计算样本标准差
|
||||
S = np.std(windows, axis=1, ddof=1)
|
||||
|
||||
# 防除零处理
|
||||
S[S == 0] = 1e-9
|
||||
|
||||
# 3.6 计算 R/S 比率
|
||||
RS = R / S
|
||||
|
||||
# 4. 计算 Hurst 值
|
||||
# 公式: Hurst = log(R/S) / log(N)
|
||||
hurst_values = np.log(RS) / np.log(N)
|
||||
|
||||
# 5. 填充数据以对齐原始 close 长度
|
||||
# diff 导致少1个,rolling导致少 window-1 个
|
||||
# 总共需要填充 window-1 个 NaN 在前面
|
||||
pad_width = self.window - 1
|
||||
result = np.pad(hurst_values, (pad_width, 0), mode='constant', constant_values=np.nan)
|
||||
|
||||
return result
|
||||
|
||||
def get_name(self):
|
||||
return f"hurst_{self.window}"
|
||||
|
||||
class HistoricalRange(Indicator):
|
||||
"""
|
||||
历史波动幅度指标:计算过去 N 日的 (最高价 - 最低价) 的简单移动平均。
|
||||
@@ -290,6 +381,47 @@ class NormalizedATR(Indicator):
|
||||
def get_name(self):
|
||||
return f"natr_{self.window}"
|
||||
|
||||
class LogNormalizedATR(Indicator):
|
||||
"""
|
||||
归一化平均真实波幅 (NATR),即 ATR / Close * 100。
|
||||
将绝对波动幅度转换为相对波动百分比,使其成为一个更平稳的波动率指标。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
window: int = 14,
|
||||
down_bound: float = None,
|
||||
up_bound: float = None,
|
||||
shift_window: int = 0,
|
||||
):
|
||||
super().__init__(down_bound, up_bound)
|
||||
self.window = window
|
||||
self.shift_window = shift_window
|
||||
|
||||
def get_values(
|
||||
self,
|
||||
close: np.array,
|
||||
open: np.array, # 不使用
|
||||
high: np.array,
|
||||
low: np.array,
|
||||
volume: np.array, # 不使用
|
||||
) -> np.array:
|
||||
"""
|
||||
根据最高价、最低价和收盘价计算 NATR 值。
|
||||
Args:
|
||||
high (np.array): 最高价列表。
|
||||
low (np.array): 最低价列表。
|
||||
close (np.array): 收盘价列表。
|
||||
Returns:
|
||||
np.array: NATR 值列表。
|
||||
"""
|
||||
# 使用 TA-Lib 直接计算 NATR
|
||||
natr_values = talib.NATR(np.log(high), np.log(low), np.log(close), timeperiod=self.window)
|
||||
return natr_values
|
||||
|
||||
def get_name(self):
|
||||
return f"log_natr_{self.window}"
|
||||
|
||||
|
||||
class ADX(Indicator):
|
||||
"""
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -47,14 +47,15 @@ class SpectralTrendStrategy(Strategy):
|
||||
max_hold_days: int = 10, # 最大持仓天数
|
||||
# --- 其他 ---
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicators: Optional[List[Indicator]] = 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(), Empty()] # 保持兼容性
|
||||
indicators = Empty() # 保持兼容性
|
||||
|
||||
# --- 参数赋值 (完全参数化) ---
|
||||
self.trade_volume = trade_volume
|
||||
@@ -88,6 +89,8 @@ class SpectralTrendStrategy(Strategy):
|
||||
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):
|
||||
@@ -107,7 +110,7 @@ class SpectralTrendStrategy(Strategy):
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
# 获取历史价格 (使用完整历史)
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
|
||||
# 【核心】计算频域趋势强度 (显式傅里叶)
|
||||
trend_strength, dominant_freq = self.calculate_trend_strength(closes)
|
||||
@@ -123,6 +126,7 @@ class SpectralTrendStrategy(Strategy):
|
||||
return
|
||||
|
||||
# 核心逻辑:相变入场/退出
|
||||
if self.trading:
|
||||
if position_volume == 0:
|
||||
self.evaluate_entry_signal(open_price, trend_strength, dominant_freq)
|
||||
else:
|
||||
@@ -143,8 +147,9 @@ class SpectralTrendStrategy(Strategy):
|
||||
return 0.0, 0.0
|
||||
|
||||
# 2. 价格归一化 (仅使用窗口内数据)
|
||||
window_data = prices[-self.spectral_window:]
|
||||
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:
|
||||
@@ -201,8 +206,7 @@ class SpectralTrendStrategy(Strategy):
|
||||
# 仅当趋势强度跨越临界点且有明确周期时入场
|
||||
self.log(
|
||||
f"Strength={trend_strength:.2f}")
|
||||
if (trend_strength > self.trend_strength_threshold
|
||||
and self.model_indicator.is_condition_met(*self.get_indicator_tuple())):
|
||||
if trend_strength > self.trend_strength_threshold:
|
||||
direction = None
|
||||
|
||||
indicator = self.model_indicator
|
||||
@@ -215,7 +219,10 @@ class SpectralTrendStrategy(Strategy):
|
||||
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:
|
||||
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"
|
||||
|
||||
1915
src/strategies/Spectral/SpectralTrendStrategy2.ipynb
Normal file
1915
src/strategies/Spectral/SpectralTrendStrategy2.ipynb
Normal file
File diff suppressed because one or more lines are too long
@@ -1,26 +1,24 @@
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pywt
|
||||
import talib
|
||||
from scipy.signal import stft
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Any, List, Dict
|
||||
|
||||
from src.core_data import Order
|
||||
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
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 策略实现 (WaveletDynamicsStrategy - 全新动态分析策略)
|
||||
# =============================================================================
|
||||
|
||||
class WaveletSignalNoiseStrategy(Strategy):
|
||||
class SpectralTrendStrategy(Strategy):
|
||||
"""
|
||||
小波信噪比策略 (最终版)
|
||||
频域能量相变策略 - 极简回归版
|
||||
|
||||
核心哲学:
|
||||
1. 信任小波: 策略完全基于小波变换最独特的“信号/噪音”分离能力。
|
||||
2. 简洁因子: 使用一个核心因子——趋势信噪比(TNR),衡量趋势的质量。
|
||||
3. 可靠逻辑:
|
||||
- 当信噪比高(趋势清晰)时入场。
|
||||
- 当信噪比低(噪音过大)时出场。
|
||||
1. 频域 (STFT): 负责"判势" —— 现在的市场是震荡(噪音主导)还是趋势(低频主导)?
|
||||
2. 时域 (Regression): 负责"定向" —— 这个低频趋势是向上的还是向下的?
|
||||
|
||||
这种组合避免了频域相位计算的复杂性和不稳定性,回归了量化的本质。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -29,172 +27,229 @@ class WaveletSignalNoiseStrategy(Strategy):
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 【核心参数】 ---
|
||||
# --- 市场参数 ---
|
||||
bars_per_day: int = 23,
|
||||
analysis_window_days: float = 2.0, # 窗口长度适中即可
|
||||
wavelet_family: str = 'db4',
|
||||
# --- 【信噪比交易阈值】 ---
|
||||
tnr_entry_threshold: float = 5, # 入场阈值:信号强度至少是噪音的2倍
|
||||
tnr_exit_threshold: float = 5, # 离场阈值:信号强度不再显著高于噪音
|
||||
# --- 【持仓管理】 ---
|
||||
# --- 策略参数 ---
|
||||
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)
|
||||
# ... (参数赋值) ...
|
||||
self.bars_per_day = bars_per_day
|
||||
self.analysis_window_days = analysis_window_days
|
||||
self.wavelet = wavelet_family
|
||||
self.tnr_entry_threshold = tnr_entry_threshold
|
||||
self.tnr_exit_threshold = tnr_exit_threshold
|
||||
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.analysis_window = int(self.analysis_window_days * self.bars_per_day)
|
||||
self.decomposition_level = pywt.dwt_max_level(self.analysis_window, self.wavelet)
|
||||
# 计算窗口大小
|
||||
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.entry_time = None
|
||||
self.order_id_counter = 0
|
||||
self.log("WaveletSignalNoiseStrategy Initialized.")
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
|
||||
def calculate_trend_noise_ratio(self, prices: np.array) -> (float, np.array):
|
||||
"""
|
||||
【最终核心】计算趋势信噪比(TNR)和内在趋势线
|
||||
返回: (tnr_factor, trend_signal)
|
||||
"""
|
||||
if len(prices) < self.analysis_window:
|
||||
return 0.0, None
|
||||
|
||||
window_data = prices[-self.analysis_window:]
|
||||
|
||||
try:
|
||||
coeffs = pywt.wavedec(window_data, self.wavelet, level=self.decomposition_level)
|
||||
|
||||
# 1. 重构内在趋势信号 (Signal)
|
||||
trend_coeffs = [coeffs[0]] + [np.zeros_like(d) for d in coeffs[1:]]
|
||||
trend_signal = pywt.waverec(trend_coeffs, self.wavelet)
|
||||
trend_signal = trend_signal[:len(window_data)]
|
||||
|
||||
# 2. 重构噪音信号 (Noise)
|
||||
noise_coeffs = [np.zeros_like(coeffs[0])] + coeffs[1:]
|
||||
noise_signal = pywt.waverec(noise_coeffs, self.wavelet)
|
||||
noise_signal = noise_signal[:len(window_data)]
|
||||
|
||||
# 3. 计算各自的强度 (标准差)
|
||||
strength_trend = np.std(trend_signal)
|
||||
strength_noise = np.std(noise_signal)
|
||||
|
||||
# 4. 计算信噪比因子
|
||||
if strength_noise < 1e-9: # 避免除以零
|
||||
tnr_factor = np.inf
|
||||
else:
|
||||
tnr_factor = strength_trend / strength_noise
|
||||
|
||||
return tnr_factor, trend_signal
|
||||
|
||||
except Exception as e:
|
||||
self.log(f"TNR calculation error: {e}", "ERROR")
|
||||
return 0.0, 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()
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
current_time = self.get_current_time()
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
if len(bar_history) < self.analysis_window:
|
||||
if len(bar_history) < self.spectral_window + 5:
|
||||
return
|
||||
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
tnr_factor, trend_signal = self.calculate_trend_noise_ratio(closes)
|
||||
# 强制平仓检查
|
||||
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
|
||||
|
||||
if trend_signal is 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, tnr_factor, trend_signal)
|
||||
self.evaluate_entry_signal(open_price, trend_strength, trend_slope)
|
||||
else:
|
||||
self.manage_open_position(position_volume, tnr_factor)
|
||||
self.manage_open_position(position_volume, trend_strength, trend_slope)
|
||||
|
||||
def evaluate_entry_signal(self, open_price: float, tnr_factor: float, trend_signal: np.array):
|
||||
"""入场逻辑:信噪比达标 + 方向确认"""
|
||||
if tnr_factor < self.tnr_entry_threshold:
|
||||
return
|
||||
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
|
||||
# 方向判断:内在趋势线的斜率
|
||||
# if len(trend_signal) < 5: return
|
||||
|
||||
if trend_signal[-1] > trend_signal[-5]:
|
||||
direction = "SELL"
|
||||
elif trend_signal[-1] < trend_signal[-5]:
|
||||
# 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:
|
||||
self.log(f"Entry Signal: {direction} | Trend-Noise Ratio={tnr_factor:.2f}")
|
||||
self.entry_time = self.get_current_time()
|
||||
# 辅助指标过滤
|
||||
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, tnr_factor: float):
|
||||
"""出场逻辑:信噪比低于退出阈值"""
|
||||
if tnr_factor < self.tnr_exit_threshold:
|
||||
direction_str = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
self.log(f"Exit Signal: TNR ({tnr_factor:.2f}) < Threshold ({self.tnr_exit_threshold})")
|
||||
self.close_position(direction_str, abs(volume))
|
||||
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:
|
||||
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")
|
||||
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}_MARKET_{self.order_id_counter}"
|
||||
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
|
||||
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}"
|
||||
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,
|
||||
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
|
||||
193
src/strategies/Spectral/SpectralTrendStrategy3.py
Normal file
193
src/strategies/Spectral/SpectralTrendStrategy3.py
Normal file
@@ -0,0 +1,193 @@
|
||||
import numpy as np
|
||||
from typing import Optional, Any, List
|
||||
from src.core_data import Bar, Order
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class SemiVarianceAsymmetryStrategy(Strategy):
|
||||
"""
|
||||
已实现半方差不对称策略 (RSVA)
|
||||
|
||||
核心原理:
|
||||
放弃"阈值计数",改用"波动能量占比"。
|
||||
因子 = (上行波动能量 - 下行波动能量) / 总波动能量
|
||||
|
||||
优势:
|
||||
1. 自适应:自动适应2021的高波动和2023的低波动,无需调整阈值。
|
||||
2. 灵敏:能捕捉到没有大阳线但持续上涨的"蠕动趋势"。
|
||||
3. 稳健:使用平方项(Variance)而非三次方(Skewness),对异常值更鲁棒。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 窗口参数 ---
|
||||
season_days: int = 20, # 计算日内季节性基准的回溯天数
|
||||
calc_window: int = 120, # 计算不对称因子的窗口 (约5天)
|
||||
cycle_length: int = 23, # 固定周期 (每天23根Bar)
|
||||
|
||||
# --- 信号阈值 ---
|
||||
# RSVA 范围是 [-1, 1]。
|
||||
# 0.2 表示上涨能量比下跌能量多20% (即 60% vs 40%),是一个显著的失衡信号。
|
||||
entry_threshold: float = 0.2,
|
||||
exit_threshold: float = 0.05,
|
||||
|
||||
order_direction: Optional[List[str]] = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None:
|
||||
order_direction = ['BUY', 'SELL']
|
||||
|
||||
self.trade_volume = trade_volume
|
||||
self.season_days = season_days
|
||||
self.calc_window = calc_window
|
||||
self.cycle_length = cycle_length
|
||||
self.entry_threshold = entry_threshold
|
||||
self.exit_threshold = exit_threshold
|
||||
self.order_direction = order_direction
|
||||
|
||||
# 计算最小历史需求
|
||||
# 我们需要: calc_window 个标准化数据
|
||||
# 每个标准化数据需要回溯: season_days * cycle_length
|
||||
self.min_history = self.calc_window + (self.season_days * self.cycle_length)
|
||||
|
||||
# 缓冲区设大一点,避免频繁触发边界检查
|
||||
self.calc_buffer_size = self.min_history + 100
|
||||
|
||||
self.log(f"RSVA Strategy Init: Window={calc_window}, Thresh={entry_threshold}")
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.cancel_all_pending_orders(symbol)
|
||||
|
||||
# 1. 获取历史数据 (切片优化)
|
||||
all_history = self.get_bar_history()
|
||||
total_len = len(all_history)
|
||||
|
||||
if total_len < self.min_history:
|
||||
return
|
||||
|
||||
# 只取计算所需的最后一段数据,保证计算复杂度恒定
|
||||
start_idx = max(0, total_len - self.calc_buffer_size)
|
||||
relevant_bars = all_history[start_idx:]
|
||||
|
||||
# 转为 numpy array
|
||||
closes = np.array([b.close for b in relevant_bars])
|
||||
|
||||
# 2. 计算对数收益率 (Log Returns)
|
||||
# 对数收益率消除了价格水平(Price Level)的影响
|
||||
log_rets = np.diff(np.log(closes))
|
||||
current_idx = len(log_rets) - 1
|
||||
|
||||
# 3. 标准化收益率计算 (De-seasonalization)
|
||||
# 这一步至关重要:剔除日内季节性(早盘波动大、午盘波动小)的干扰
|
||||
std_rets = []
|
||||
|
||||
# 循环计算过去 calc_window 个点的标准化值
|
||||
for i in range(self.calc_window):
|
||||
target_idx = current_idx - i
|
||||
|
||||
# 高效切片:利用 stride=cycle_length 提取同一时间槽的历史
|
||||
# slot_history 包含 [t, t-23, t-46, ...]
|
||||
slot_history = log_rets[target_idx::-self.cycle_length]
|
||||
|
||||
# 截取 season_days
|
||||
if len(slot_history) > self.season_days:
|
||||
slot_history = slot_history[:self.season_days]
|
||||
|
||||
# 计算该时刻的基准波动率
|
||||
if len(slot_history) < 5:
|
||||
# 降级处理:样本不足时用近期全局波动率
|
||||
slot_vol = np.std(log_rets[-self.cycle_length:]) + 1e-9
|
||||
else:
|
||||
slot_vol = np.std(slot_history) + 1e-9
|
||||
|
||||
# 标准化 (Z-Score)
|
||||
std_ret = log_rets[target_idx] / slot_vol
|
||||
std_rets.append(std_ret)
|
||||
|
||||
# 转为数组 (注意:std_rets 是倒序的,但这不影响平方和计算)
|
||||
std_rets_arr = np.array(std_rets)
|
||||
|
||||
# 4. 【核心】计算已实现半方差不对称性 (RSVA)
|
||||
|
||||
# 分离正收益和负收益
|
||||
pos_rets = std_rets_arr[std_rets_arr > 0]
|
||||
neg_rets = std_rets_arr[std_rets_arr < 0]
|
||||
|
||||
# 计算上行能量 (Upside Variance) 和 下行能量 (Downside Variance)
|
||||
rv_pos = np.sum(pos_rets ** 2)
|
||||
rv_neg = np.sum(neg_rets ** 2)
|
||||
total_rv = rv_pos + rv_neg + 1e-9 # 防止除零
|
||||
|
||||
# 计算因子: [-1, 1]
|
||||
# > 0 说明上涨更有力(或更频繁),< 0 说明下跌主导
|
||||
rsva_factor = (rv_pos - rv_neg) / total_rv
|
||||
|
||||
# 5. 交易逻辑
|
||||
current_pos = self.get_current_positions().get(symbol, 0)
|
||||
|
||||
self.log_status(rsva_factor, rv_pos, rv_neg, current_pos)
|
||||
|
||||
if current_pos == 0:
|
||||
self.evaluate_entry(rsva_factor)
|
||||
else:
|
||||
self.evaluate_exit(current_pos, rsva_factor)
|
||||
|
||||
def evaluate_entry(self, factor: float):
|
||||
direction = None
|
||||
|
||||
# 因子 > 0.2: 哪怕没有极端K线,只要累计的上涨能量显著压过下跌能量,就开仓
|
||||
if factor > self.entry_threshold:
|
||||
if "BUY" in self.order_direction:
|
||||
direction = "BUY"
|
||||
|
||||
elif factor < -self.entry_threshold:
|
||||
if "SELL" in self.order_direction:
|
||||
direction = "SELL"
|
||||
|
||||
if direction:
|
||||
self.log(f"ENTRY: {direction} | RSVA={factor:.4f}")
|
||||
self.send_market_order(direction, self.trade_volume, "OPEN")
|
||||
|
||||
def evaluate_exit(self, volume: int, factor: float):
|
||||
do_exit = False
|
||||
reason = ""
|
||||
|
||||
# 当多空能量趋于平衡 (因子回到 0 附近),说明趋势动能耗尽,平仓
|
||||
# 这种离场方式对震荡市非常友好:一旦陷入震荡,rv_pos 和 rv_neg 会迅速接近,因子归零
|
||||
if volume > 0 and factor < self.exit_threshold:
|
||||
do_exit = True
|
||||
reason = f"Bull Energy Fade (RSVA={factor:.4f})"
|
||||
|
||||
elif volume < 0 and factor > -self.exit_threshold:
|
||||
do_exit = True
|
||||
reason = f"Bear Energy Fade (RSVA={factor:.4f})"
|
||||
|
||||
if do_exit:
|
||||
direction = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
self.log(f"EXIT: {reason}")
|
||||
self.send_market_order(direction, abs(volume), "CLOSE")
|
||||
|
||||
def send_market_order(self, direction: str, volume: int, offset: str):
|
||||
# 严格遵守要求:使用 get_current_time()
|
||||
current_time = self.get_current_time()
|
||||
|
||||
order = Order(
|
||||
id=f"{self.main_symbol}_{direction}_{current_time.timestamp()}",
|
||||
symbol=self.symbol,
|
||||
direction=direction,
|
||||
volume=volume,
|
||||
price_type="MARKET",
|
||||
submitted_time=current_time,
|
||||
offset=offset
|
||||
)
|
||||
self.send_order(order)
|
||||
|
||||
def log_status(self, factor: float, pos_e: float, neg_e: float, current_pos: int):
|
||||
if self.enable_log:
|
||||
# 仅在有持仓或信号明显时打印
|
||||
if current_pos != 0 or abs(factor) > self.entry_threshold * 0.8:
|
||||
self.log(f"Status: Pos={current_pos} | RSVA={factor:.4f} | Energy(+/-)={pos_e:.1f}/{neg_e:.1f}")
|
||||
File diff suppressed because one or more lines are too long
@@ -50,6 +50,11 @@ def run_single_backtest(
|
||||
'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)
|
||||
|
||||
# 打印当前进程正在处理的组合信息
|
||||
# 注意:多进程打印会交错显示
|
||||
print(f"--- 正在运行组合: {strategy_parameters} (PID: {multiprocessing.current_process().pid}) ---")
|
||||
|
||||
@@ -19,7 +19,7 @@ CONFIG = {
|
||||
|
||||
"strategy_params": {
|
||||
'main_symbol': 'SA', # <-- 替换为你的交易品种代码,例如 'GC=F' (黄金期货), 'ZC=F' (玉米期货)
|
||||
'trade_volume': 1,
|
||||
'trade_volume': 2,
|
||||
'model_indicator': ZScoreATR(14, 100, 0.5, 3),
|
||||
'spectral_window_days': 8, # STFT窗口大小(天)
|
||||
'low_freq_days': 8, # 低频下限(天)
|
||||
|
||||
34
strategy_manager/strategies/SpectralTrendStrategy/rb.py
Normal file
34
strategy_manager/strategies/SpectralTrendStrategy/rb.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# 策略配置(Python格式)
|
||||
from src.indicators.indicators import ZScoreATR, BollingerBandwidth
|
||||
|
||||
CONFIG = {
|
||||
"name": "傅里叶趋势策略",
|
||||
"version": "1.0",
|
||||
"enabled": True,
|
||||
|
||||
"strategy_class": "futures_trading_strategies.rb.Spectral.SpectralTrendStrategy.SpectralTrendStrategy",
|
||||
|
||||
"engine_params": {
|
||||
"symbol": "KQ.m@SHFE.rb",
|
||||
"duration_seconds": 900,
|
||||
"roll_over_mode": True,
|
||||
"history_length": 1000,
|
||||
# 支持Python对象
|
||||
"close_bar_delta": __import__('datetime').timedelta(minutes=58)
|
||||
},
|
||||
|
||||
"strategy_params": {
|
||||
'main_symbol': 'rb', # <-- 替换为你的交易品种代码,例如 'GC=F' (黄金期货), 'ZC=F' (玉米期货)
|
||||
'trade_volume': 3,
|
||||
# 'order_direction': ['SELL', 'BUY'],
|
||||
# 'indicators': [BollingerBandwidth(20, 2.0, 1.8, 5), BollingerBandwidth(20, 2.0, 1.8, 5),],
|
||||
'model_indicator': BollingerBandwidth(20, 2.0, 0.5, 3.5),
|
||||
'bars_per_day': 23,
|
||||
'spectral_window_days': 4, # STFT窗口大小(天)
|
||||
'low_freq_days': 4, # 低频下限(天)
|
||||
'high_freq_days': 2, # 高频上限(天)
|
||||
'trend_strength_threshold': 0.8, # 相变临界值
|
||||
'exit_threshold': 0.4, # 退出阈值
|
||||
'enable_log': True
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ async def start_scheduler():
|
||||
# 任务 1: 每天 08:55
|
||||
scheduler.add_job(
|
||||
scheduled_restart_task,
|
||||
CronTrigger(hour=8, minute=55),
|
||||
CronTrigger(hour=8, minute=58),
|
||||
id="restart_morning",
|
||||
replace_existing=True
|
||||
)
|
||||
@@ -72,7 +72,7 @@ async def start_scheduler():
|
||||
# 任务 2: 每天 20:55
|
||||
scheduler.add_job(
|
||||
scheduled_restart_task,
|
||||
CronTrigger(hour=20, minute=55),
|
||||
CronTrigger(hour=20, minute=58),
|
||||
id="restart_evening",
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user