更新策略邮件推送
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -236,12 +236,12 @@ if __name__ == "__main__":
|
||||
|
||||
# 这种方式适合获取相对较短或中等长度的历史K线数据。
|
||||
df_if_backtest_daily = collect_and_save_tqsdk_data_stream(
|
||||
symbol="KQ.m@SHFE.rb",
|
||||
symbol="KQ.m@SHFE.br",
|
||||
# symbol='SHFE.rb2510',
|
||||
# symbol='KQ.i@SHFE.bu',
|
||||
freq="min15",
|
||||
start_date_str="2021-01-01",
|
||||
end_date_str="2025-11-28",
|
||||
end_date_str="2025-12-01",
|
||||
mode="backtest", # 指定为回测模式
|
||||
tq_user=TQ_USER_NAME,
|
||||
tq_pwd=TQ_PASSWORD,
|
||||
|
||||
1153
futures_trading_strategies/rb/Spectral/SpectralTrendStrategy4.ipynb
Normal file
1153
futures_trading_strategies/rb/Spectral/SpectralTrendStrategy4.ipynb
Normal file
File diff suppressed because one or more lines are too long
312
futures_trading_strategies/rb/Spectral/SpectralTrendStrategy4.py
Normal file
312
futures_trading_strategies/rb/Spectral/SpectralTrendStrategy4.py
Normal file
@@ -0,0 +1,312 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
from scipy.signal import stft
|
||||
from datetime import timedelta
|
||||
from typing import Optional, Any, List
|
||||
|
||||
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):
|
||||
"""
|
||||
频域能量相变策略 - 塔勒布宽幅结构版 (Chandelier Exit)
|
||||
|
||||
修改重点:
|
||||
1. 移除 Slope 离场,避免噪音干扰。
|
||||
2. 引入状态变量记录持仓期间的极值 (pos_highest/pos_lowest)。
|
||||
3. 实施“吊灯止损”:以持仓期间极值为锚点,回撤 N * ATR 离场。
|
||||
4. 止损系数建议:4.0 - 6.0 (给予极大的呼吸空间,仅防范趋势崩溃)。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 市场参数 ---
|
||||
bars_per_day: int = 23,
|
||||
# --- STFT 策略参数 ---
|
||||
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, # 仅用于判断方向,不用于离场
|
||||
|
||||
# --- 关键风控参数 (Chandelier Exit) ---
|
||||
stop_loss_atr_multiplier: float = 5.0, # 塔勒布式宽止损,建议 4.0 ~ 6.0
|
||||
stop_loss_atr_period: int = 14,
|
||||
|
||||
# --- 其他 ---
|
||||
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.sl_atr_multiplier = stop_loss_atr_multiplier
|
||||
self.sl_atr_period = stop_loss_atr_period
|
||||
|
||||
self.order_direction = order_direction
|
||||
self.model_indicator = model_indicator or Empty()
|
||||
self.indicators = indicators or Empty()
|
||||
self.reverse = reverse
|
||||
|
||||
# 计算 STFT 窗口大小
|
||||
self.spectral_window = int(self.spectral_window_days * self.bars_per_day)
|
||||
if self.spectral_window % 2 != 0:
|
||||
self.spectral_window += 1
|
||||
|
||||
self.low_freq_bound = 1.0 / self.low_freq_days if self.low_freq_days > 0 else float('inf')
|
||||
self.high_freq_bound = 1.0 / self.high_freq_days if self.high_freq_days > 0 else 0.0
|
||||
|
||||
self.order_id_counter = 0
|
||||
|
||||
# --- 持仓状态追踪变量 ---
|
||||
self.entry_price = 0.0
|
||||
self.pos_highest = 0.0 # 持有多单期间的最高价
|
||||
self.pos_lowest = 0.0 # 持有空单期间的最低价
|
||||
|
||||
self.log(
|
||||
f"SpectralTrend Strategy Initialized. Window: {self.spectral_window}, "
|
||||
f"Chandelier Stop: {self.sl_atr_multiplier}x ATR"
|
||||
)
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
# 1. 数据长度检查
|
||||
required_len = max(self.spectral_window, self.sl_atr_period + 5)
|
||||
if len(bar_history) < required_len:
|
||||
return
|
||||
|
||||
# 2. 计算 ATR (用于止损)
|
||||
atr_window = self.sl_atr_period + 10
|
||||
highs = np.array([b.high for b in bar_history[-atr_window:]], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history[-atr_window:]], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history[-atr_window:]], dtype=float)
|
||||
|
||||
try:
|
||||
atr_values = talib.ATR(highs, lows, closes, timeperiod=self.sl_atr_period)
|
||||
current_atr = atr_values[-1]
|
||||
if np.isnan(current_atr): current_atr = 0.0
|
||||
except Exception as e:
|
||||
self.log(f"ATR Calc Error: {e}")
|
||||
current_atr = 0.0
|
||||
|
||||
# 3. 计算 STFT 核心指标
|
||||
stft_closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
trend_strength, trend_slope = self.calculate_market_state(stft_closes)
|
||||
|
||||
# 4. 交易逻辑
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
# 获取当前Bar的最高/最低价用于更新极值(如果使用Bar内更新,更加灵敏)
|
||||
# 这里为了稳健,使用上一根Bar的High/Low来更新,或者使用开盘价近似
|
||||
current_high = bar_history[-1].high
|
||||
current_low = bar_history[-1].low
|
||||
|
||||
if self.trading:
|
||||
if position_volume == 0:
|
||||
# 重置状态
|
||||
self.pos_highest = 0.0
|
||||
self.pos_lowest = 0.0
|
||||
self.entry_price = 0.0
|
||||
|
||||
self.evaluate_entry_signal(open_price, trend_strength, trend_slope)
|
||||
else:
|
||||
# 传入 current_high/low 用于更新追踪止损的锚点
|
||||
self.manage_open_position(
|
||||
position_volume,
|
||||
trend_strength,
|
||||
open_price,
|
||||
current_atr,
|
||||
current_high,
|
||||
current_low
|
||||
)
|
||||
|
||||
def calculate_market_state(self, prices: np.array) -> (float, float):
|
||||
"""
|
||||
计算频域能量占比和线性回归斜率(仅用于方向)
|
||||
"""
|
||||
if len(prices) < self.spectral_window:
|
||||
return 0.0, 0.0
|
||||
|
||||
# 标准化处理,消除绝对价格影响
|
||||
window_data = prices[-self.spectral_window:]
|
||||
mean_val = np.mean(window_data)
|
||||
std_val = np.std(window_data)
|
||||
if std_val == 0: std_val = 1.0
|
||||
|
||||
normalized = (window_data - mean_val) / (std_val + 1e-8)
|
||||
|
||||
# STFT 计算
|
||||
try:
|
||||
f, t, Zxx = stft(
|
||||
normalized,
|
||||
fs=self.bars_per_day,
|
||||
nperseg=self.spectral_window,
|
||||
noverlap=max(0, self.spectral_window // 2),
|
||||
boundary=None,
|
||||
padded=False
|
||||
)
|
||||
except Exception:
|
||||
return 0.0, 0.0
|
||||
|
||||
# 提取有效频率
|
||||
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
|
||||
f = f[valid_mask]
|
||||
Zxx = Zxx[valid_mask, :]
|
||||
|
||||
if Zxx.size == 0: return 0.0, 0.0
|
||||
|
||||
# 计算能量
|
||||
current_energy = np.abs(Zxx[:, -1]) ** 2
|
||||
|
||||
low_mask = f < self.low_freq_bound
|
||||
high_mask = f > self.high_freq_bound
|
||||
|
||||
low_energy = np.sum(current_energy[low_mask]) if np.any(low_mask) else 0.0
|
||||
high_energy = np.sum(current_energy[high_mask]) if np.any(high_mask) else 0.0
|
||||
total_energy = low_energy + high_energy + 1e-8
|
||||
|
||||
trend_strength = low_energy / total_energy
|
||||
|
||||
# 计算简单线性斜率用于判断方向
|
||||
x = np.arange(len(normalized))
|
||||
slope, _ = np.polyfit(x, normalized, 1)
|
||||
|
||||
return trend_strength, slope
|
||||
|
||||
def evaluate_entry_signal(self, open_price: float, trend_strength: float, trend_slope: float):
|
||||
"""
|
||||
入场逻辑:高低频能量比 > 阈值 + 斜率确认方向
|
||||
"""
|
||||
# 必须有足够的趋势强度
|
||||
if trend_strength > self.trend_strength_threshold:
|
||||
direction = None
|
||||
|
||||
# 仅使用 slope 的正负号和基本阈值来决定方向,不作为离场依据
|
||||
if "BUY" in self.order_direction and trend_slope > self.slope_threshold:
|
||||
direction = "BUY"
|
||||
elif "SELL" in self.order_direction and trend_slope < -self.slope_threshold:
|
||||
direction = "SELL"
|
||||
|
||||
if direction:
|
||||
# 外部过滤条件
|
||||
if not self.indicators.is_condition_met(*self.get_indicator_tuple()):
|
||||
return
|
||||
if not self.model_indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
if self.reverse:
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
|
||||
self.log(f"Entry: {direction} | Strength={trend_strength:.2f} | DirSlope={trend_slope:.4f}")
|
||||
|
||||
self.send_limit_order(direction, open_price, self.trade_volume, "OPEN")
|
||||
|
||||
# 初始化持仓状态
|
||||
self.entry_price = open_price
|
||||
# 初始极值设为当前价格
|
||||
self.pos_highest = open_price
|
||||
self.pos_lowest = open_price
|
||||
|
||||
def manage_open_position(self, volume: int, trend_strength: float, current_price: float,
|
||||
current_atr: float, high_price: float, low_price: float):
|
||||
"""
|
||||
离场逻辑核心:
|
||||
1. 信号离场:能量自然衰竭 (Trend Strength < Threshold)
|
||||
2. 结构离场:Chandelier Exit (吊灯止损) - 宽幅 ATR 追踪
|
||||
"""
|
||||
|
||||
exit_dir = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
|
||||
# --- 更新持仓期间的极值 ---
|
||||
if volume > 0: # 多头
|
||||
if high_price > self.pos_highest or self.pos_highest == 0:
|
||||
self.pos_highest = high_price
|
||||
else: # 空头
|
||||
if (low_price < self.pos_lowest or self.pos_lowest == 0) and low_price > 0:
|
||||
self.pos_lowest = low_price
|
||||
|
||||
# --- 1. 计算宽幅吊灯止损 (Structural Stop) ---
|
||||
is_stop_triggered = False
|
||||
stop_line = 0.0
|
||||
|
||||
# 如果 ATR 无效,暂时不触发止损,或者使用百分比兜底(此处略)
|
||||
if current_atr > 0:
|
||||
stop_distance = current_atr * self.sl_atr_multiplier
|
||||
|
||||
if volume > 0:
|
||||
# 多头止损线 = 最高价 - N * ATR
|
||||
# 随着价格创新高,止损线不断上移;价格下跌,止损线不变
|
||||
stop_line = self.pos_highest - stop_distance
|
||||
if current_price <= stop_line:
|
||||
is_stop_triggered = True
|
||||
self.log(
|
||||
f"STOP (Long): Price {current_price} <= Highest {self.pos_highest} - {self.sl_atr_multiplier}xATR")
|
||||
|
||||
else:
|
||||
# 空头止损线 = 最低价 + N * ATR
|
||||
stop_line = self.pos_lowest + stop_distance
|
||||
if current_price >= stop_line:
|
||||
is_stop_triggered = True
|
||||
self.log(
|
||||
f"STOP (Short): Price {current_price} >= Lowest {self.pos_lowest} + {self.sl_atr_multiplier}xATR")
|
||||
|
||||
if is_stop_triggered:
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
return # 止损优先
|
||||
|
||||
# --- 2. 信号自然离场 (Signal Exit) ---
|
||||
# 当 STFT 检测到低频能量消散,说明市场进入混沌或震荡,此时平仓
|
||||
# 这是一个 "慢" 离场,通常在趋势走完后
|
||||
if trend_strength < self.exit_threshold:
|
||||
self.log(f"Exit (Signal): Strength {trend_strength:.2f} < {self.exit_threshold}")
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
return
|
||||
|
||||
# --- 交易执行辅助函数 ---
|
||||
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)
|
||||
1131
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy4.ipynb
Normal file
1131
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy4.ipynb
Normal file
File diff suppressed because one or more lines are too long
312
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy4.py
Normal file
312
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy4.py
Normal file
@@ -0,0 +1,312 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
from scipy.signal import stft
|
||||
from datetime import timedelta
|
||||
from typing import Optional, Any, List
|
||||
|
||||
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):
|
||||
"""
|
||||
频域能量相变策略 - 塔勒布宽幅结构版 (Chandelier Exit)
|
||||
|
||||
修改重点:
|
||||
1. 移除 Slope 离场,避免噪音干扰。
|
||||
2. 引入状态变量记录持仓期间的极值 (pos_highest/pos_lowest)。
|
||||
3. 实施“吊灯止损”:以持仓期间极值为锚点,回撤 N * ATR 离场。
|
||||
4. 止损系数建议:4.0 - 6.0 (给予极大的呼吸空间,仅防范趋势崩溃)。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 市场参数 ---
|
||||
bars_per_day: int = 23,
|
||||
# --- STFT 策略参数 ---
|
||||
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, # 仅用于判断方向,不用于离场
|
||||
|
||||
# --- 关键风控参数 (Chandelier Exit) ---
|
||||
stop_loss_atr_multiplier: float = 5.0, # 塔勒布式宽止损,建议 4.0 ~ 6.0
|
||||
stop_loss_atr_period: int = 14,
|
||||
|
||||
# --- 其他 ---
|
||||
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.sl_atr_multiplier = stop_loss_atr_multiplier
|
||||
self.sl_atr_period = stop_loss_atr_period
|
||||
|
||||
self.order_direction = order_direction
|
||||
self.model_indicator = model_indicator or Empty()
|
||||
self.indicators = indicators or Empty()
|
||||
self.reverse = reverse
|
||||
|
||||
# 计算 STFT 窗口大小
|
||||
self.spectral_window = int(self.spectral_window_days * self.bars_per_day)
|
||||
if self.spectral_window % 2 != 0:
|
||||
self.spectral_window += 1
|
||||
|
||||
self.low_freq_bound = 1.0 / self.low_freq_days if self.low_freq_days > 0 else float('inf')
|
||||
self.high_freq_bound = 1.0 / self.high_freq_days if self.high_freq_days > 0 else 0.0
|
||||
|
||||
self.order_id_counter = 0
|
||||
|
||||
# --- 持仓状态追踪变量 ---
|
||||
self.entry_price = 0.0
|
||||
self.pos_highest = 0.0 # 持有多单期间的最高价
|
||||
self.pos_lowest = 0.0 # 持有空单期间的最低价
|
||||
|
||||
self.log(
|
||||
f"SpectralTrend Strategy Initialized. Window: {self.spectral_window}, "
|
||||
f"Chandelier Stop: {self.sl_atr_multiplier}x ATR"
|
||||
)
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
# 1. 数据长度检查
|
||||
required_len = max(self.spectral_window, self.sl_atr_period + 5)
|
||||
if len(bar_history) < required_len:
|
||||
return
|
||||
|
||||
# 2. 计算 ATR (用于止损)
|
||||
atr_window = self.sl_atr_period + 10
|
||||
highs = np.array([b.high for b in bar_history[-atr_window:]], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history[-atr_window:]], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history[-atr_window:]], dtype=float)
|
||||
|
||||
try:
|
||||
atr_values = talib.ATR(highs, lows, closes, timeperiod=self.sl_atr_period)
|
||||
current_atr = atr_values[-1]
|
||||
if np.isnan(current_atr): current_atr = 0.0
|
||||
except Exception as e:
|
||||
self.log(f"ATR Calc Error: {e}")
|
||||
current_atr = 0.0
|
||||
|
||||
# 3. 计算 STFT 核心指标
|
||||
stft_closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
trend_strength, trend_slope = self.calculate_market_state(stft_closes)
|
||||
|
||||
# 4. 交易逻辑
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
# 获取当前Bar的最高/最低价用于更新极值(如果使用Bar内更新,更加灵敏)
|
||||
# 这里为了稳健,使用上一根Bar的High/Low来更新,或者使用开盘价近似
|
||||
current_high = bar_history[-1].high
|
||||
current_low = bar_history[-1].low
|
||||
|
||||
if self.trading:
|
||||
if position_volume == 0:
|
||||
# 重置状态
|
||||
self.pos_highest = 0.0
|
||||
self.pos_lowest = 0.0
|
||||
self.entry_price = 0.0
|
||||
|
||||
self.evaluate_entry_signal(open_price, trend_strength, trend_slope)
|
||||
else:
|
||||
# 传入 current_high/low 用于更新追踪止损的锚点
|
||||
self.manage_open_position(
|
||||
position_volume,
|
||||
trend_strength,
|
||||
open_price,
|
||||
current_atr,
|
||||
current_high,
|
||||
current_low
|
||||
)
|
||||
|
||||
def calculate_market_state(self, prices: np.array) -> (float, float):
|
||||
"""
|
||||
计算频域能量占比和线性回归斜率(仅用于方向)
|
||||
"""
|
||||
if len(prices) < self.spectral_window:
|
||||
return 0.0, 0.0
|
||||
|
||||
# 标准化处理,消除绝对价格影响
|
||||
window_data = prices[-self.spectral_window:]
|
||||
mean_val = np.mean(window_data)
|
||||
std_val = np.std(window_data)
|
||||
if std_val == 0: std_val = 1.0
|
||||
|
||||
normalized = (window_data - mean_val) / (std_val + 1e-8)
|
||||
|
||||
# STFT 计算
|
||||
try:
|
||||
f, t, Zxx = stft(
|
||||
normalized,
|
||||
fs=self.bars_per_day,
|
||||
nperseg=self.spectral_window,
|
||||
noverlap=max(0, self.spectral_window // 2),
|
||||
boundary=None,
|
||||
padded=False
|
||||
)
|
||||
except Exception:
|
||||
return 0.0, 0.0
|
||||
|
||||
# 提取有效频率
|
||||
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
|
||||
f = f[valid_mask]
|
||||
Zxx = Zxx[valid_mask, :]
|
||||
|
||||
if Zxx.size == 0: return 0.0, 0.0
|
||||
|
||||
# 计算能量
|
||||
current_energy = np.abs(Zxx[:, -1]) ** 2
|
||||
|
||||
low_mask = f < self.low_freq_bound
|
||||
high_mask = f > self.high_freq_bound
|
||||
|
||||
low_energy = np.sum(current_energy[low_mask]) if np.any(low_mask) else 0.0
|
||||
high_energy = np.sum(current_energy[high_mask]) if np.any(high_mask) else 0.0
|
||||
total_energy = low_energy + high_energy + 1e-8
|
||||
|
||||
trend_strength = low_energy / total_energy
|
||||
|
||||
# 计算简单线性斜率用于判断方向
|
||||
x = np.arange(len(normalized))
|
||||
slope, _ = np.polyfit(x, normalized, 1)
|
||||
|
||||
return trend_strength, slope
|
||||
|
||||
def evaluate_entry_signal(self, open_price: float, trend_strength: float, trend_slope: float):
|
||||
"""
|
||||
入场逻辑:高低频能量比 > 阈值 + 斜率确认方向
|
||||
"""
|
||||
# 必须有足够的趋势强度
|
||||
if trend_strength > self.trend_strength_threshold:
|
||||
direction = None
|
||||
|
||||
# 仅使用 slope 的正负号和基本阈值来决定方向,不作为离场依据
|
||||
if "BUY" in self.order_direction and trend_slope > self.slope_threshold:
|
||||
direction = "BUY"
|
||||
elif "SELL" in self.order_direction and trend_slope < -self.slope_threshold:
|
||||
direction = "SELL"
|
||||
|
||||
if direction:
|
||||
# 外部过滤条件
|
||||
if not self.indicators.is_condition_met(*self.get_indicator_tuple()):
|
||||
return
|
||||
if not self.model_indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
if self.reverse:
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
|
||||
self.log(f"Entry: {direction} | Strength={trend_strength:.2f} | DirSlope={trend_slope:.4f}")
|
||||
|
||||
self.send_limit_order(direction, open_price, self.trade_volume, "OPEN")
|
||||
|
||||
# 初始化持仓状态
|
||||
self.entry_price = open_price
|
||||
# 初始极值设为当前价格
|
||||
self.pos_highest = open_price
|
||||
self.pos_lowest = open_price
|
||||
|
||||
def manage_open_position(self, volume: int, trend_strength: float, current_price: float,
|
||||
current_atr: float, high_price: float, low_price: float):
|
||||
"""
|
||||
离场逻辑核心:
|
||||
1. 信号离场:能量自然衰竭 (Trend Strength < Threshold)
|
||||
2. 结构离场:Chandelier Exit (吊灯止损) - 宽幅 ATR 追踪
|
||||
"""
|
||||
|
||||
exit_dir = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
|
||||
# --- 更新持仓期间的极值 ---
|
||||
if volume > 0: # 多头
|
||||
if high_price > self.pos_highest or self.pos_highest == 0:
|
||||
self.pos_highest = high_price
|
||||
else: # 空头
|
||||
if (low_price < self.pos_lowest or self.pos_lowest == 0) and low_price > 0:
|
||||
self.pos_lowest = low_price
|
||||
|
||||
# --- 1. 计算宽幅吊灯止损 (Structural Stop) ---
|
||||
is_stop_triggered = False
|
||||
stop_line = 0.0
|
||||
|
||||
# 如果 ATR 无效,暂时不触发止损,或者使用百分比兜底(此处略)
|
||||
if current_atr > 0:
|
||||
stop_distance = current_atr * self.sl_atr_multiplier
|
||||
|
||||
if volume > 0:
|
||||
# 多头止损线 = 最高价 - N * ATR
|
||||
# 随着价格创新高,止损线不断上移;价格下跌,止损线不变
|
||||
stop_line = self.pos_highest - stop_distance
|
||||
if current_price <= stop_line:
|
||||
is_stop_triggered = True
|
||||
self.log(
|
||||
f"STOP (Long): Price {current_price} <= Highest {self.pos_highest} - {self.sl_atr_multiplier}xATR")
|
||||
|
||||
else:
|
||||
# 空头止损线 = 最低价 + N * ATR
|
||||
stop_line = self.pos_lowest + stop_distance
|
||||
if current_price >= stop_line:
|
||||
is_stop_triggered = True
|
||||
self.log(
|
||||
f"STOP (Short): Price {current_price} >= Lowest {self.pos_lowest} + {self.sl_atr_multiplier}xATR")
|
||||
|
||||
if is_stop_triggered:
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
return # 止损优先
|
||||
|
||||
# --- 2. 信号自然离场 (Signal Exit) ---
|
||||
# 当 STFT 检测到低频能量消散,说明市场进入混沌或震荡,此时平仓
|
||||
# 这是一个 "慢" 离场,通常在趋势走完后
|
||||
if trend_strength < self.exit_threshold:
|
||||
self.log(f"Exit (Signal): Strength {trend_strength:.2f} < {self.exit_threshold}")
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
return
|
||||
|
||||
# --- 交易执行辅助函数 ---
|
||||
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)
|
||||
1179
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy5.ipynb
Normal file
1179
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy5.ipynb
Normal file
File diff suppressed because one or more lines are too long
335
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy5.py
Normal file
335
futures_trading_strategies/ru/Spectral/SpectralTrendStrategy5.py
Normal file
@@ -0,0 +1,335 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
from scipy.signal import stft
|
||||
from datetime import timedelta
|
||||
from typing import Optional, Any, List
|
||||
|
||||
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):
|
||||
"""
|
||||
频域能量相变策略 - 双因子自适应版 (Dual-Factor Adaptive)
|
||||
|
||||
优化逻辑:
|
||||
不再通过静态参数 reverse 控制方向,而是由两个 Indicator 动态决策:
|
||||
1. 计算 STFT 基础方向 (Base Direction)。
|
||||
2. 检查 indicator_primary:若满足,则采用 Base Direction (顺势/正向)。
|
||||
3. 若不满足,检查 indicator_secondary:若满足,则采用 Reverse Direction (逆势/反转)。
|
||||
4. 若都不满足,保持空仓。
|
||||
|
||||
状态追踪:
|
||||
self.entry_signal_source 会记录当前持仓是 'PRIMARY' 还是 'SECONDARY'。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 市场参数 ---
|
||||
bars_per_day: int = 23,
|
||||
# --- STFT 策略参数 ---
|
||||
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,
|
||||
|
||||
# --- 关键风控参数 (Chandelier Exit) ---
|
||||
stop_loss_atr_multiplier: float = 5.0,
|
||||
stop_loss_atr_period: int = 14,
|
||||
|
||||
# --- 信号控制指标 (核心优化) ---
|
||||
indicator_primary: Indicator = None, # 满足此条件 -> 正向开仓 (reverse=False)
|
||||
indicator_secondary: Indicator = None, # 满足此条件 -> 反向开仓 (reverse=True)
|
||||
model_indicator: Indicator = None, # 可选:额外的AI模型过滤器
|
||||
|
||||
# --- 其他 ---
|
||||
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.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.sl_atr_multiplier = stop_loss_atr_multiplier
|
||||
self.sl_atr_period = stop_loss_atr_period
|
||||
|
||||
self.order_direction = order_direction
|
||||
|
||||
# 初始指标容器 (默认为Empty,即永远返回True或False,视Empty具体实现而定,建议传入具体指标)
|
||||
self.indicator_primary = indicator_primary or Empty()
|
||||
self.indicator_secondary = indicator_secondary or Empty()
|
||||
self.model_indicator = model_indicator or Empty()
|
||||
|
||||
# 计算 STFT 窗口大小
|
||||
self.spectral_window = int(self.spectral_window_days * self.bars_per_day)
|
||||
if self.spectral_window % 2 != 0:
|
||||
self.spectral_window += 1
|
||||
|
||||
self.low_freq_bound = 1.0 / self.low_freq_days if self.low_freq_days > 0 else float('inf')
|
||||
self.high_freq_bound = 1.0 / self.high_freq_days if self.high_freq_days > 0 else 0.0
|
||||
|
||||
self.order_id_counter = 0
|
||||
|
||||
# --- 持仓状态追踪变量 ---
|
||||
self.entry_price = 0.0
|
||||
self.pos_highest = 0.0
|
||||
self.pos_lowest = 0.0
|
||||
|
||||
# 新增:记录开仓信号来源 ('PRIMARY' or 'SECONDARY')
|
||||
self.entry_signal_source = None
|
||||
|
||||
self.log(
|
||||
f"SpectralTrend Dual-Adaptive Strategy Initialized.\n"
|
||||
f"Window: {self.spectral_window}, ATR Stop: {self.sl_atr_multiplier}x\n"
|
||||
f"Primary Ind: {type(self.indicator_primary).__name__}, "
|
||||
f"Secondary Ind: {type(self.indicator_secondary).__name__}"
|
||||
)
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
# 1. 数据长度检查
|
||||
required_len = max(self.spectral_window, self.sl_atr_period + 5)
|
||||
if len(bar_history) < required_len:
|
||||
return
|
||||
|
||||
# 2. 计算 ATR
|
||||
atr_window = self.sl_atr_period + 10
|
||||
highs = np.array([b.high for b in bar_history[-atr_window:]], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history[-atr_window:]], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history[-atr_window:]], dtype=float)
|
||||
|
||||
try:
|
||||
atr_values = talib.ATR(highs, lows, closes, timeperiod=self.sl_atr_period)
|
||||
current_atr = atr_values[-1]
|
||||
if np.isnan(current_atr): current_atr = 0.0
|
||||
except Exception as e:
|
||||
self.log(f"ATR Calc Error: {e}")
|
||||
current_atr = 0.0
|
||||
|
||||
# 3. 计算 STFT 核心指标
|
||||
stft_closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
trend_strength, trend_slope = self.calculate_market_state(stft_closes)
|
||||
|
||||
# 4. 交易逻辑
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
current_high = bar_history[-1].high
|
||||
current_low = bar_history[-1].low
|
||||
|
||||
if self.trading:
|
||||
if position_volume == 0:
|
||||
# 重置所有状态
|
||||
self.pos_highest = 0.0
|
||||
self.pos_lowest = 0.0
|
||||
self.entry_price = 0.0
|
||||
self.entry_signal_source = None # 重置信号来源
|
||||
|
||||
self.evaluate_entry_signal(open_price, trend_strength, trend_slope)
|
||||
else:
|
||||
self.manage_open_position(
|
||||
position_volume,
|
||||
trend_strength,
|
||||
open_price,
|
||||
current_atr,
|
||||
current_high,
|
||||
current_low
|
||||
)
|
||||
|
||||
def calculate_market_state(self, prices: np.array) -> (float, float):
|
||||
"""
|
||||
计算频域能量占比和线性回归斜率
|
||||
"""
|
||||
if len(prices) < self.spectral_window:
|
||||
return 0.0, 0.0
|
||||
|
||||
window_data = prices[-self.spectral_window:]
|
||||
mean_val = np.mean(window_data)
|
||||
std_val = np.std(window_data)
|
||||
if std_val == 0: std_val = 1.0
|
||||
|
||||
normalized = (window_data - mean_val) / (std_val + 1e-8)
|
||||
|
||||
try:
|
||||
f, t, Zxx = stft(
|
||||
normalized,
|
||||
fs=self.bars_per_day,
|
||||
nperseg=self.spectral_window,
|
||||
noverlap=max(0, self.spectral_window // 2),
|
||||
boundary=None,
|
||||
padded=False
|
||||
)
|
||||
except Exception:
|
||||
return 0.0, 0.0
|
||||
|
||||
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
|
||||
f = f[valid_mask]
|
||||
Zxx = Zxx[valid_mask, :]
|
||||
|
||||
if Zxx.size == 0: return 0.0, 0.0
|
||||
|
||||
current_energy = np.abs(Zxx[:, -1]) ** 2
|
||||
low_mask = f < self.low_freq_bound
|
||||
high_mask = f > self.high_freq_bound
|
||||
|
||||
low_energy = np.sum(current_energy[low_mask]) if np.any(low_mask) else 0.0
|
||||
high_energy = np.sum(current_energy[high_mask]) if np.any(high_mask) else 0.0
|
||||
total_energy = low_energy + high_energy + 1e-8
|
||||
|
||||
trend_strength = low_energy / total_energy
|
||||
|
||||
x = np.arange(len(normalized))
|
||||
slope, _ = np.polyfit(x, normalized, 1)
|
||||
|
||||
return trend_strength, slope
|
||||
|
||||
def evaluate_entry_signal(self, open_price: float, trend_strength: float, trend_slope: float):
|
||||
"""
|
||||
入场逻辑优化:双因子控制
|
||||
"""
|
||||
# 1. 基础能量阈值检查
|
||||
if trend_strength <= self.trend_strength_threshold:
|
||||
return
|
||||
|
||||
# 2. 确定 STFT 原始方向 (Raw Direction)
|
||||
raw_direction = None
|
||||
if trend_slope > self.slope_threshold:
|
||||
raw_direction = "BUY"
|
||||
elif trend_slope < -self.slope_threshold:
|
||||
raw_direction = "SELL"
|
||||
|
||||
if not raw_direction:
|
||||
return
|
||||
|
||||
# 3. 双指标分支逻辑 (Dual-Indicator Branching)
|
||||
# 获取指标计算所需的参数 (通常是 bar_history 等,依赖基类实现)
|
||||
indicator_args = self.get_indicator_tuple()
|
||||
|
||||
final_direction = None
|
||||
source_tag = None
|
||||
|
||||
# --- 分支 1: Primary Indicator (优先) ---
|
||||
# 如果满足主条件 -> 使用原始方向 (Equivalent to reverse=False)
|
||||
if self.indicator_primary.is_condition_met(*indicator_args):
|
||||
final_direction = raw_direction
|
||||
source_tag = "PRIMARY"
|
||||
|
||||
# --- 分支 2: Secondary Indicator (备选/Else) ---
|
||||
# 如果不满足主条件,但满足备选条件 -> 使用反转方向 (Equivalent to reverse=True)
|
||||
elif self.indicator_secondary.is_condition_met(*indicator_args):
|
||||
final_direction = "SELL" if raw_direction == "BUY" else "BUY"
|
||||
source_tag = "SECONDARY"
|
||||
|
||||
# --- 分支 3: 都不满足 ---
|
||||
else:
|
||||
return # 放弃交易
|
||||
|
||||
# 4. 全局模型过滤 (可选)
|
||||
if not self.model_indicator.is_condition_met(*indicator_args):
|
||||
return
|
||||
|
||||
# 5. 最终方向检查
|
||||
if final_direction not in self.order_direction:
|
||||
return
|
||||
|
||||
# 6. 执行开仓
|
||||
self.log(
|
||||
f"Entry Triggered [{source_tag}]: "
|
||||
f"Raw={raw_direction} -> Final={final_direction} | "
|
||||
f"Strength={trend_strength:.2f} | Slope={trend_slope:.4f}"
|
||||
)
|
||||
|
||||
self.send_limit_order(final_direction, open_price, self.trade_volume, "OPEN")
|
||||
|
||||
# 更新状态
|
||||
self.entry_price = open_price
|
||||
self.pos_highest = open_price
|
||||
self.pos_lowest = open_price
|
||||
self.entry_signal_source = source_tag # 保存是由哪一个条件控制的
|
||||
|
||||
def manage_open_position(self, volume: int, trend_strength: float, current_price: float,
|
||||
current_atr: float, high_price: float, low_price: float):
|
||||
"""
|
||||
离场逻辑 (保持不变,但日志中可以体现来源)
|
||||
"""
|
||||
exit_dir = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
|
||||
# 更新极值
|
||||
if volume > 0:
|
||||
if high_price > self.pos_highest or self.pos_highest == 0:
|
||||
self.pos_highest = high_price
|
||||
else:
|
||||
if (low_price < self.pos_lowest or self.pos_lowest == 0) and low_price > 0:
|
||||
self.pos_lowest = low_price
|
||||
|
||||
# 1. 结构性止损 (Chandelier Exit)
|
||||
is_stop_triggered = False
|
||||
stop_line = 0.0
|
||||
|
||||
if current_atr > 0:
|
||||
stop_distance = current_atr * self.sl_atr_multiplier
|
||||
if volume > 0:
|
||||
stop_line = self.pos_highest - stop_distance
|
||||
if current_price <= stop_line:
|
||||
is_stop_triggered = True
|
||||
else:
|
||||
stop_line = self.pos_lowest + stop_distance
|
||||
if current_price >= stop_line:
|
||||
is_stop_triggered = True
|
||||
|
||||
if is_stop_triggered:
|
||||
self.log(
|
||||
f"STOP Loss ({self.entry_signal_source}): Price {current_price} hit Chandelier line {stop_line:.2f}")
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
return
|
||||
|
||||
# 2. 信号衰竭离场
|
||||
if trend_strength < self.exit_threshold:
|
||||
self.log(
|
||||
f"Exit Signal ({self.entry_signal_source}): Energy Faded {trend_strength:.2f} < {self.exit_threshold}")
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
return
|
||||
|
||||
# --- 交易辅助 ---
|
||||
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)
|
||||
47
real_trading/Spectral/FG.py
Normal file
47
real_trading/Spectral/FG.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from tqsdk import TqApi, TqAuth, TqAccount, TqKq
|
||||
|
||||
from futures_trading_strategies.FG.Spectral.SpectralTrendStrategy3 import SpectralTrendStrategy
|
||||
# 导入 TqsdkEngine,而不是原来的 BacktestEngine
|
||||
from src.indicators.indicators import PriceRangeToVolatilityRatio, ZScoreATR, Hurst
|
||||
from src.tqsdk_real_engine import TqsdkEngine
|
||||
|
||||
# 导入你的策略类
|
||||
|
||||
# --- 配置参数 ---
|
||||
# Tqsdk 的本地数据文件路径,注意 Tqsdk 要求文件名为 KQ_m@交易所_品种名_周期.csv
|
||||
|
||||
# 主力合约的 symbol
|
||||
symbol = "KQ.m@SHFE.ag"
|
||||
strategy_parameters = {
|
||||
'main_symbol': 'ag', # <-- 替换为你的交易品种代码,例如 'GC=F' (黄金期货), 'ZC=F' (玉米期货)
|
||||
'trade_volume': 1,
|
||||
# 'order_direction': ['SELL', 'BUY'],
|
||||
'indicators': Hurst(115, 0, 0.5),
|
||||
# 'model_indicator': ADX(7, 0, 30),
|
||||
'bars_per_day': 23,
|
||||
'spectral_window_days': 9, # STFT窗口大小(天)
|
||||
'low_freq_days': 9, # 低频下限(天)
|
||||
'high_freq_days': 4, # 高频上限(天)
|
||||
'trend_strength_threshold': 0.6, # 相变临界值
|
||||
'exit_threshold': 0.3, # 退出阈值
|
||||
'enable_log': True,
|
||||
'reverse': False,
|
||||
}
|
||||
api = TqApi(TqKq(), auth=TqAuth("emanresu", "dfgvfgdfgg"))
|
||||
# api = TqApi(TqAccount('H宏源期货', '903308830', 'lzr520102'), auth=TqAuth("emanresu", "dfgvfgdfgg"))
|
||||
|
||||
# --- 1. 初始化回测引擎并运行 ---
|
||||
print("\n初始化 Tqsdk 回测引擎...")
|
||||
engine = TqsdkEngine(
|
||||
strategy_class=SpectralTrendStrategy,
|
||||
strategy_params=strategy_parameters,
|
||||
api=api,
|
||||
symbol=symbol,
|
||||
duration_seconds=60 * 15,
|
||||
roll_over_mode=True, # 启用换月模式检测
|
||||
history_length=1000,
|
||||
close_bar_delta=timedelta(minutes=58)
|
||||
)
|
||||
engine.run() # 这是一个同步方法,内部会运行 asyncio 循环
|
||||
36
real_trading/TestStrategy/test.py
Normal file
36
real_trading/TestStrategy/test.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from tqsdk import TqApi, TqAuth, TqAccount, TqKq
|
||||
|
||||
from src.strategies.SimpleLimitBuyStrategy import SimpleLimitBuyStrategyLong
|
||||
# 导入 TqsdkEngine,而不是原来的 BacktestEngine
|
||||
from src.indicators.indicators import PriceRangeToVolatilityRatio, ZScoreATR, Hurst
|
||||
from src.tqsdk_real_engine import TqsdkEngine
|
||||
|
||||
# 导入你的策略类
|
||||
|
||||
# --- 配置参数 ---
|
||||
# Tqsdk 的本地数据文件路径,注意 Tqsdk 要求文件名为 KQ_m@交易所_品种名_周期.csv
|
||||
|
||||
# 主力合约的 symbol
|
||||
symbol = "KQ.m@SHFE.ag"
|
||||
strategy_parameters = {
|
||||
'symbol': symbol,
|
||||
'trade_volume': 1,
|
||||
'enable_log': True,
|
||||
}
|
||||
api = TqApi(TqKq(), auth=TqAuth("emanresu", "dfgvfgdfgg"))
|
||||
|
||||
# --- 1. 初始化回测引擎并运行 ---
|
||||
print("\n初始化 Tqsdk 回测引擎...")
|
||||
engine = TqsdkEngine(
|
||||
strategy_class=SimpleLimitBuyStrategyLong,
|
||||
strategy_params=strategy_parameters,
|
||||
api=api,
|
||||
symbol=symbol,
|
||||
duration_seconds=60 * 15,
|
||||
roll_over_mode=True, # 启用换月模式检测
|
||||
history_length=1000,
|
||||
close_bar_delta=timedelta(minutes=58)
|
||||
)
|
||||
engine.run() # 这是一个同步方法,内部会运行 asyncio 循环
|
||||
@@ -21,11 +21,6 @@ class SimpleLimitBuyStrategyLong(Strategy):
|
||||
symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
open_range_factor_1_ago: float,
|
||||
open_range_factor_7_ago: float,
|
||||
max_position: int,
|
||||
stop_loss_points: float = 10, # 新增:止损点数
|
||||
take_profit_points: float = 10,
|
||||
): # 新增:止盈点数
|
||||
"""
|
||||
初始化策略。
|
||||
@@ -41,716 +36,26 @@ class SimpleLimitBuyStrategyLong(Strategy):
|
||||
"""
|
||||
super().__init__(context, symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
self.open_range_factor_1_ago = open_range_factor_1_ago
|
||||
self.open_range_factor_7_ago = open_range_factor_7_ago
|
||||
self.max_position = max_position # 理论上这里应为1
|
||||
self.stop_loss_points = stop_loss_points
|
||||
self.take_profit_points = take_profit_points
|
||||
|
||||
self.order_id_counter = 0
|
||||
|
||||
self._last_order_id: Optional[str] = None # 用于跟踪上一根K线发出的订单ID
|
||||
|
||||
self.log(
|
||||
f"策略初始化: symbol={self.symbol}, trade_volume={self.trade_volume}, "
|
||||
f"open_range_factor_1_ago={self.open_range_factor_1_ago}, "
|
||||
f"open_range_factor_7_ago={self.open_range_factor_7_ago}, "
|
||||
f"max_position={self.max_position}, "
|
||||
f"止损点={self.stop_loss_points}, 止盈点={self.take_profit_points}"
|
||||
)
|
||||
|
||||
def on_open_bar(self, bar: Bar, next_bar_open: Optional[float] = None):
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
"""
|
||||
每当新的K线数据到来时调用。
|
||||
Args:
|
||||
bar (Bar): 当前的K线数据对象。
|
||||
next_bar_open (Optional[float]): 下一根K线的开盘价,此处策略未使用。
|
||||
"""
|
||||
current_datetime = bar.datetime # 获取当前K线时间
|
||||
self.symbol = bar.symbol
|
||||
self.symbol = symbol
|
||||
|
||||
# --- 1. 撤销上一根K线未成交的订单 ---
|
||||
# 检查是否记录了上一笔订单ID,并且该订单仍然在待处理列表中
|
||||
if self._last_order_id:
|
||||
pending_orders = self.get_pending_orders()
|
||||
if self._last_order_id in pending_orders:
|
||||
success = self.cancel_order(
|
||||
self._last_order_id
|
||||
) # 直接调用基类的取消方法
|
||||
if success:
|
||||
self.log(
|
||||
f"[{current_datetime}] 策略: 成功撤销上一根K线未成交订单 {self._last_order_id}"
|
||||
)
|
||||
else:
|
||||
self.log(
|
||||
f"[{current_datetime}] 策略: 尝试撤销订单 {self._last_order_id} 失败(可能已成交或不存在)"
|
||||
)
|
||||
# 无论撤销成功与否,既然我们尝试了撤销,就清除记录
|
||||
self._last_order_id = None
|
||||
# else:
|
||||
# self.log(f"[{current_datetime}] 策略: 无上一根K线未成交订单需要撤销。")
|
||||
|
||||
# 2. 更新K线历史
|
||||
trade_volume = self.trade_volume
|
||||
|
||||
# 获取当前持仓和未决订单(在取消之后获取,确保是最新的状态)
|
||||
current_positions = self.get_current_positions()
|
||||
current_pos_volume = current_positions.get(self.symbol, 0)
|
||||
pending_orders_after_cancel = (
|
||||
self.get_pending_orders()
|
||||
) # 再次获取,此时应已取消旧订单
|
||||
|
||||
# --- 3. 平仓逻辑 (止损/止盈) ---
|
||||
# 只有当有持仓时才考虑平仓
|
||||
if current_pos_volume > 0: # 假设只做多,所以持仓量 > 0
|
||||
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||
if avg_entry_price is not None:
|
||||
pnl_per_unit = (
|
||||
bar.open - avg_entry_price
|
||||
) # 当前浮动盈亏(以收盘价计算)
|
||||
|
||||
# 止盈条件
|
||||
if pnl_per_unit >= self.take_profit_points:
|
||||
self.log(
|
||||
f"[{current_datetime}] 止盈信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {self.take_profit_points:.2f}"
|
||||
)
|
||||
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction="CLOSE_LONG",
|
||||
volume=trade_volume,
|
||||
price_type="MARKET",
|
||||
# limit_price=limit_price,
|
||||
submitted_time=bar.datetime,
|
||||
offset="CLOSE",
|
||||
)
|
||||
trade = self.send_order(order)
|
||||
return # 平仓后本K线不再进行开仓判断
|
||||
|
||||
# 止损条件
|
||||
elif pnl_per_unit <= -self.stop_loss_points:
|
||||
self.log(
|
||||
f"[{current_datetime}] 止损信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {-self.stop_loss_points:.2f}"
|
||||
)
|
||||
# 发送市价卖出订单平仓,确保立即成交
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction="CLOSE_LONG",
|
||||
volume=trade_volume,
|
||||
price_type="MARKET",
|
||||
# limit_price=limit_price,
|
||||
submitted_time=bar.datetime,
|
||||
offset="CLOSE",
|
||||
)
|
||||
trade = self.send_order(order)
|
||||
return # 平仓后本K线不再进行开仓判断
|
||||
|
||||
# --- 4. 开仓逻辑 (只考虑做多 BUY 方向) ---
|
||||
# 只有在没有持仓 (current_pos_volume == 0) 且没有待处理订单 (not pending_orders_after_cancel)
|
||||
# 且K线历史足够长时才考虑开仓
|
||||
bar_history = self.get_bar_history()
|
||||
if current_pos_volume == 0 and len(bar_history) > 16:
|
||||
|
||||
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
||||
bar_1_ago = bar_history[-8]
|
||||
bar_7_ago = bar_history[-15]
|
||||
|
||||
# 计算历史 K 线的 Range
|
||||
range_1_ago = bar_1_ago.high - bar_1_ago.low
|
||||
range_7_ago = bar_7_ago.high - bar_7_ago.low
|
||||
|
||||
# 根据策略逻辑计算目标买入价格
|
||||
# 目标买入价 = 当前K线Open - (前1根Range * 因子1 + 前7根Range * 因子2)
|
||||
self.log(
|
||||
bar.open,
|
||||
range_1_ago * self.open_range_factor_1_ago,
|
||||
range_7_ago * self.open_range_factor_7_ago,
|
||||
)
|
||||
target_buy_price = bar.open - (
|
||||
range_1_ago * self.open_range_factor_1_ago
|
||||
+ range_7_ago * self.open_range_factor_7_ago
|
||||
)
|
||||
|
||||
# 确保目标买入价格有效,例如不能是负数
|
||||
target_buy_price = max(0.01, target_buy_price)
|
||||
|
||||
self.log(
|
||||
f"[{current_datetime}] 开多仓信号 - 当前Open={bar.open:.2f}, "
|
||||
f"前1Range={range_1_ago:.2f}, 前7Range={range_7_ago:.2f}, "
|
||||
f"计算目标买入价={target_buy_price:.2f}"
|
||||
)
|
||||
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction="BUY",
|
||||
volume=trade_volume,
|
||||
price_type="LIMIT",
|
||||
limit_price=target_buy_price,
|
||||
submitted_time=bar.datetime,
|
||||
)
|
||||
new_order = self.send_order(order)
|
||||
# 记录下这个订单的ID,以便在下一根K线开始时进行撤销
|
||||
if new_order:
|
||||
self._last_order_id = new_order.id
|
||||
self.log(
|
||||
f"[{current_datetime}] 策略: 发送限价买入订单 {self._last_order_id} @ {target_buy_price:.2f}"
|
||||
if current_pos_volume == 0:
|
||||
self.send_order(
|
||||
Order(
|
||||
id=0, symbol=self.symbol, direction='BUY', volume=1,
|
||||
price_type="MARKET", submitted_time=self.get_current_time(), offset='OPEN',
|
||||
)
|
||||
else:
|
||||
self.log(f"[{current_datetime}] 策略: 发送订单失败。")
|
||||
|
||||
# else:
|
||||
# self.log(f"[{current_datetime}] 不满足开仓条件:持仓={current_pos_volume}, 待处理订单={len(pending_orders_after_cancel)}, K线历史长度={len(bar_history)}")
|
||||
|
||||
def on_close_bar(self, bar: Bar, next_bar_open: Optional[float] = None):
|
||||
self.cancel_all_pending_orders()
|
||||
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
"""
|
||||
在合约换月时清空历史K线数据和上次订单ID,避免使用旧合约数据进行计算。
|
||||
"""
|
||||
super().on_rollover(old_symbol, new_symbol) # 调用基类方法打印日志
|
||||
self._last_order_id = None # 清空上次订单ID,因为旧合约订单已取消
|
||||
|
||||
self.log(f"换月完成,清空历史K线数据和上次订单ID,准备新合约交易。")
|
||||
|
||||
|
||||
class SimpleLimitBuyStrategyShort(Strategy):
|
||||
"""
|
||||
一个基于当前K线Open、前1根和前7根K线Range计算优势价格进行限价买入的策略。
|
||||
具备以下特点:
|
||||
- 每根K线开始时取消上一根K线未成交的订单。
|
||||
- 最多只能有一个开仓挂单和一个持仓。
|
||||
- 包含简单的止损和止盈逻辑。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
open_range_factor_1_ago: float,
|
||||
open_range_factor_7_ago: float,
|
||||
max_position: int,
|
||||
stop_loss_points: float = 10, # 新增:止损点数
|
||||
take_profit_points: float = 10,
|
||||
): # 新增:止盈点数
|
||||
"""
|
||||
初始化策略。
|
||||
Args:
|
||||
context: 模拟器实例。
|
||||
symbol (str): 交易合约代码。
|
||||
trade_volume (int): 单笔交易量。
|
||||
open_range_factor_1_ago (float): 前1根K线Range的权重因子,用于从Open价向下偏移。
|
||||
open_range_factor_7_ago (float): 前7根K线Range的权重因子,用于从Open价向下偏移。
|
||||
max_position (int): 最大持仓量(此处为1,因为只允许一个持仓)。
|
||||
stop_loss_points (float): 止损点数(例如,亏损达到此点数则止损)。
|
||||
take_profit_points (float): 止盈点数(例如,盈利达到此点数则止盈)。
|
||||
"""
|
||||
super().__init__(context, symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
self.open_range_factor_1_ago = open_range_factor_1_ago
|
||||
self.open_range_factor_7_ago = open_range_factor_7_ago
|
||||
self.max_position = max_position # 理论上这里应为1
|
||||
self.stop_loss_points = stop_loss_points
|
||||
self.take_profit_points = take_profit_points
|
||||
|
||||
self.order_id_counter = 0
|
||||
self.last_buy_price = 0
|
||||
self.last_sell_price = 0
|
||||
|
||||
bar_history: deque[Bar] = deque(maxlen=10)
|
||||
self._last_order_id: Optional[str] = None # 用于跟踪上一根K线发出的订单ID
|
||||
|
||||
self.log(
|
||||
f"策略初始化: symbol={self.symbol}, trade_volume={self.trade_volume}, "
|
||||
f"open_range_factor_1_ago={self.open_range_factor_1_ago}, "
|
||||
f"open_range_factor_7_ago={self.open_range_factor_7_ago}, "
|
||||
f"max_position={self.max_position}, "
|
||||
f"止损点={self.stop_loss_points}, 止盈点={self.take_profit_points}"
|
||||
)
|
||||
|
||||
def on_open_bar(self, bar: Bar, next_bar_open: Optional[float] = None):
|
||||
"""
|
||||
每当新的K线数据到来时调用。
|
||||
Args:
|
||||
bar (Bar): 当前的K线数据对象。
|
||||
next_bar_open (Optional[float]): 下一根K线的开盘价,此处策略未使用。
|
||||
"""
|
||||
current_datetime = bar.datetime # 获取当前K线时间
|
||||
self.symbol = bar.symbol
|
||||
|
||||
# --- 1. 撤销上一根K线未成交的订单 ---
|
||||
# 检查是否记录了上一笔订单ID,并且该订单仍然在待处理列表中
|
||||
if self._last_order_id:
|
||||
pending_orders = self.get_pending_orders()
|
||||
if self._last_order_id in pending_orders:
|
||||
success = self.cancel_order(
|
||||
self._last_order_id
|
||||
) # 直接调用基类的取消方法
|
||||
if success:
|
||||
self.log(
|
||||
f"[{current_datetime}] 策略: 成功撤销上一根K线未成交订单 {self._last_order_id}"
|
||||
)
|
||||
else:
|
||||
self.log(
|
||||
f"[{current_datetime}] 策略: 尝试撤销订单 {self._last_order_id} 失败(可能已成交或不存在)"
|
||||
)
|
||||
# 无论撤销成功与否,既然我们尝试了撤销,就清除记录
|
||||
self._last_order_id = None
|
||||
# else:
|
||||
# self.log(f"[{current_datetime}] 策略: 无上一根K线未成交订单需要撤销。")
|
||||
|
||||
# 2. 更新K线历史
|
||||
trade_volume = self.trade_volume
|
||||
|
||||
# 获取当前持仓和未决订单(在取消之后获取,确保是最新的状态)
|
||||
current_positions = self.get_current_positions()
|
||||
current_pos_volume = current_positions.get(self.symbol, 0)
|
||||
pending_orders_after_cancel = (
|
||||
self.get_pending_orders()
|
||||
) # 再次获取,此时应已取消旧订单
|
||||
|
||||
# --- 3. 平仓逻辑 (止损/止盈) ---
|
||||
# 只有当有持仓时才考虑平仓
|
||||
if current_pos_volume < 0: # 假设只做多,所以持仓量 > 0
|
||||
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||
self.log(f'avg_entry_price {avg_entry_price}, bar.open {bar.open}')
|
||||
|
||||
if avg_entry_price is not None:
|
||||
pnl_per_unit = (
|
||||
avg_entry_price - bar.open
|
||||
) # 当前浮动盈亏(以收盘价计算)
|
||||
|
||||
# 止盈条件
|
||||
if pnl_per_unit >= self.take_profit_points:
|
||||
self.log(
|
||||
f"[{current_datetime}] 止盈信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {self.take_profit_points:.2f}"
|
||||
)
|
||||
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction="CLOSE_SHORT",
|
||||
volume=trade_volume,
|
||||
price_type="MARKET",
|
||||
# limit_price=limit_price,
|
||||
submitted_time=bar.datetime,
|
||||
offset="CLOSE",
|
||||
)
|
||||
trade = self.send_order(order)
|
||||
return # 平仓后本K线不再进行开仓判断
|
||||
|
||||
# 止损条件
|
||||
elif pnl_per_unit <= -self.stop_loss_points:
|
||||
self.log(
|
||||
f"[{current_datetime}] 止损信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {-self.stop_loss_points:.2f}"
|
||||
)
|
||||
# 发送市价卖出订单平仓,确保立即成交
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction="CLOSE_SHORT",
|
||||
volume=trade_volume,
|
||||
price_type="MARKET",
|
||||
# limit_price=limit_price,
|
||||
submitted_time=bar.datetime,
|
||||
offset="CLOSE",
|
||||
)
|
||||
trade = self.send_order(order)
|
||||
return # 平仓后本K线不再进行开仓判断
|
||||
|
||||
# --- 4. 开仓逻辑 (只考虑做多 BUY 方向) ---
|
||||
# 只有在没有持仓 (current_pos_volume == 0) 且没有待处理订单 (not pending_orders_after_cancel)
|
||||
# 且K线历史足够长时才考虑开仓
|
||||
bar_history = self.get_bar_history()
|
||||
if current_pos_volume == 0 and len(bar_history) > 16:
|
||||
|
||||
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
||||
bar_1_ago = bar_history[-8]
|
||||
bar_7_ago = bar_history[-15]
|
||||
|
||||
# 计算历史 K 线的 Range
|
||||
range_1_ago = bar_1_ago.high - bar_1_ago.low
|
||||
range_7_ago = bar_7_ago.high - bar_7_ago.low
|
||||
|
||||
# 根据策略逻辑计算目标买入价格
|
||||
# 目标买入价 = 当前K线Open - (前1根Range * 因子1 + 前7根Range * 因子2)
|
||||
self.log(
|
||||
bar.open,
|
||||
range_1_ago * self.open_range_factor_1_ago,
|
||||
range_7_ago * self.open_range_factor_7_ago,
|
||||
)
|
||||
target_buy_price = bar.open + (
|
||||
range_1_ago * self.open_range_factor_1_ago
|
||||
+ range_7_ago * self.open_range_factor_7_ago
|
||||
)
|
||||
|
||||
# 确保目标买入价格有效,例如不能是负数
|
||||
target_buy_price = max(0.01, target_buy_price)
|
||||
|
||||
self.log(
|
||||
f"[{current_datetime}] 开多仓信号 - 当前Open={bar.open:.2f}, "
|
||||
f"前1Range={range_1_ago:.2f}, 前7Range={range_7_ago:.2f}, "
|
||||
f"计算目标买入价={target_buy_price:.2f}"
|
||||
)
|
||||
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction="SELL",
|
||||
volume=trade_volume,
|
||||
price_type="LIMIT",
|
||||
limit_price=target_buy_price,
|
||||
submitted_time=bar.datetime,
|
||||
)
|
||||
new_order = self.send_order(order)
|
||||
# 记录下这个订单的ID,以便在下一根K线开始时进行撤销
|
||||
if new_order:
|
||||
self._last_order_id = new_order.id
|
||||
self.log(
|
||||
f"[{current_datetime}] 策略: 发送限价买入订单 {self._last_order_id} @ {target_buy_price:.2f}"
|
||||
)
|
||||
else:
|
||||
self.log(f"[{current_datetime}] 策略: 发送订单失败。")
|
||||
|
||||
# else:
|
||||
# self.log(f"[{current_datetime}] 不满足开仓条件:持仓={current_pos_volume}, 待处理订单={len(pending_orders_after_cancel)}, K线历史长度={len(bar_history)}")
|
||||
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
"""
|
||||
在合约换月时清空历史K线数据和上次订单ID,避免使用旧合约数据进行计算。
|
||||
"""
|
||||
super().on_rollover(old_symbol, new_symbol) # 调用基类方法打印日志
|
||||
# bar_history.clear() # 清空历史K线
|
||||
self._last_order_id = None # 清空上次订单ID,因为旧合约订单已取消
|
||||
|
||||
self.log(f"换月完成,清空历史K线数据和上次订单ID,准备新合约交易。")
|
||||
|
||||
|
||||
class SimpleLimitBuyStrategy(Strategy):
|
||||
"""
|
||||
一个基于当前K线Open、前1根和前7根K线Range计算优势价格进行限价买入的策略。
|
||||
具备以下特点:
|
||||
- 每根K线开始时取消上一根K线未成交的订单。
|
||||
- 最多只能有一个开仓挂单和一个持仓。
|
||||
- 包含简单的止损和止盈逻辑。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
open_range_factor_1_long: float,
|
||||
open_range_factor_7_long: float,
|
||||
open_range_factor_1_short: float,
|
||||
open_range_factor_7_short: float,
|
||||
max_position: int,
|
||||
stop_loss_points: float = 10, # 新增:止损点数
|
||||
take_profit_points: float = 10,
|
||||
): # 新增:止盈点数
|
||||
"""
|
||||
初始化策略。
|
||||
Args:
|
||||
context: 模拟器实例。
|
||||
symbol (str): 交易合约代码。
|
||||
trade_volume (int): 单笔交易量。
|
||||
open_range_factor_1_ago (float): 前1根K线Range的权重因子,用于从Open价向下偏移。
|
||||
open_range_factor_7_ago (float): 前7根K线Range的权重因子,用于从Open价向下偏移。
|
||||
max_position (int): 最大持仓量(此处为1,因为只允许一个持仓)。
|
||||
stop_loss_points (float): 止损点数(例如,亏损达到此点数则止损)。
|
||||
take_profit_points (float): 止盈点数(例如,盈利达到此点数则止盈)。
|
||||
"""
|
||||
super().__init__(context, symbol, enable_log)
|
||||
self.last_buy_price = 0
|
||||
self.last_sell_price = 0
|
||||
self.trade_volume = trade_volume
|
||||
self.open_range_factor_1_long = open_range_factor_1_long
|
||||
self.open_range_factor_7_long = open_range_factor_7_long
|
||||
self.open_range_factor_1_short = open_range_factor_1_short
|
||||
self.open_range_factor_7_short = open_range_factor_7_short
|
||||
self.max_position = max_position # 理论上这里应为1
|
||||
self.stop_loss_points = stop_loss_points
|
||||
self.take_profit_points = take_profit_points
|
||||
|
||||
self.order_id_counter = 0
|
||||
|
||||
self._last_order_id: Optional[str] = None # 用于跟踪上一根K线发出的订单ID
|
||||
|
||||
self.log(
|
||||
f"策略初始化: symbol={self.symbol}, trade_volume={self.trade_volume}, "
|
||||
f"max_position={self.max_position}, "
|
||||
f"止损点={self.stop_loss_points}, 止盈点={self.take_profit_points}"
|
||||
)
|
||||
|
||||
def on_open_bar(self, bar: Bar, next_bar_open: Optional[float] = None):
|
||||
"""
|
||||
每当新的K线数据到来时调用。
|
||||
Args:
|
||||
bar (Bar): 当前的K线数据对象。
|
||||
next_bar_open (Optional[float]): 下一根K线的开盘价,此处策略未使用。
|
||||
"""
|
||||
current_datetime = bar.datetime # 获取当前K线时间
|
||||
self.symbol = bar.symbol
|
||||
|
||||
# --- 1. 撤销上一根K线未成交的订单 ---
|
||||
# 检查是否记录了上一笔订单ID,并且该订单仍然在待处理列表中
|
||||
if self._last_order_id:
|
||||
pending_orders = self.get_pending_orders()
|
||||
# if self._last_order_id in pending_orders:
|
||||
# success = self.cancel_order(self._last_order_id) # 直接调用基类的取消方法
|
||||
# if success:
|
||||
# self.log(f"[{current_datetime}] 策略: 成功撤销上一根K线未成交订单 {self._last_order_id}")
|
||||
# else:
|
||||
# self.log(f"[{current_datetime}] 策略: 尝试撤销订单 {self._last_order_id} 失败(可能已成交或不存在)")
|
||||
# # 无论撤销成功与否,既然我们尝试了撤销,就清除记录
|
||||
# self._last_order_id = None
|
||||
self.cancel_all_pending_orders()
|
||||
# else:
|
||||
# self.log(f"[{current_datetime}] 策略: 无上一根K线未成交订单需要撤销。")
|
||||
|
||||
# 2. 更新K线历史
|
||||
trade_volume = self.trade_volume
|
||||
|
||||
# 获取当前持仓和未决订单(在取消之后获取,确保是最新的状态)
|
||||
current_positions = self.get_current_positions()
|
||||
current_pos_volume = current_positions.get(self.symbol, 0)
|
||||
pending_orders_after_cancel = (
|
||||
self.get_pending_orders()
|
||||
) # 再次获取,此时应已取消旧订单
|
||||
|
||||
# --- 3. 平仓逻辑 (止损/止盈) ---
|
||||
# 只有当有持仓时才考虑平仓
|
||||
self.log(current_positions, self.symbol)
|
||||
if current_pos_volume < 0: # 假设只做多,所以持仓量 > 0
|
||||
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||
avg_entry_price = self.last_sell_price
|
||||
if avg_entry_price is not None:
|
||||
pnl_per_unit = (
|
||||
avg_entry_price - bar.open
|
||||
) # 当前浮动盈亏(以收盘价计算)
|
||||
|
||||
# 止盈条件
|
||||
if pnl_per_unit >= self.take_profit_points:
|
||||
self.log(
|
||||
f"[{current_datetime}] 止盈信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {self.take_profit_points:.2f}"
|
||||
)
|
||||
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction="CLOSE_SHORT",
|
||||
volume=trade_volume,
|
||||
price_type="MARKET",
|
||||
# limit_price=limit_price,
|
||||
submitted_time=bar.datetime,
|
||||
offset="CLOSE",
|
||||
)
|
||||
trade = self.send_order(order)
|
||||
return # 平仓后本K线不再进行开仓判断
|
||||
|
||||
# 止损条件
|
||||
elif pnl_per_unit <= -self.stop_loss_points:
|
||||
self.log(
|
||||
f"[{current_datetime}] 止损信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {-self.stop_loss_points:.2f}"
|
||||
)
|
||||
# 发送市价卖出订单平仓,确保立即成交
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction="CLOSE_SHORT",
|
||||
volume=trade_volume,
|
||||
price_type="MARKET",
|
||||
# limit_price=limit_price,
|
||||
submitted_time=bar.datetime,
|
||||
offset="CLOSE",
|
||||
)
|
||||
trade = self.send_order(order)
|
||||
return # 平仓后本K线不再进行开仓判断
|
||||
|
||||
if current_pos_volume > 0: # 假设只做多,所以持仓量 > 0
|
||||
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||
avg_entry_price = self.last_buy_price
|
||||
if avg_entry_price is not None:
|
||||
pnl_per_unit = (
|
||||
bar.open - avg_entry_price
|
||||
) # 当前浮动盈亏(以收盘价计算)
|
||||
|
||||
# 止盈条件
|
||||
if pnl_per_unit >= self.take_profit_points:
|
||||
self.log(
|
||||
f"[{current_datetime}] 止盈信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {self.take_profit_points:.2f}"
|
||||
)
|
||||
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction="CLOSE_LONG",
|
||||
volume=trade_volume,
|
||||
price_type="MARKET",
|
||||
# limit_price=limit_price,
|
||||
submitted_time=bar.datetime,
|
||||
offset="CLOSE",
|
||||
)
|
||||
trade = self.send_order(order)
|
||||
return # 平仓后本K线不再进行开仓判断
|
||||
|
||||
# 止损条件
|
||||
elif pnl_per_unit <= -self.stop_loss_points:
|
||||
self.log(
|
||||
f"[{current_datetime}] 止损信号 - PnL per unit: {pnl_per_unit:.2f}, 目标: {-self.stop_loss_points:.2f}"
|
||||
)
|
||||
# 发送市价卖出订单平仓,确保立即成交
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=self.symbol,
|
||||
direction="CLOSE_LONG",
|
||||
volume=trade_volume,
|
||||
price_type="MARKET",
|
||||
# limit_price=limit_price,
|
||||
submitted_time=bar.datetime,
|
||||
offset="CLOSE",
|
||||
)
|
||||
trade = self.send_order(order)
|
||||
return # 平仓后本K线不再进行开仓判断
|
||||
|
||||
bar_history = self.get_bar_history()
|
||||
if current_pos_volume == 0 and len(bar_history) > 16:
|
||||
|
||||
# 获取前1根K线 (倒数第二根) 和前7根K线 (队列中最老的一根)
|
||||
bar_1_ago = bar_history[-8]
|
||||
bar_7_ago = bar_history[-15]
|
||||
|
||||
print(bar_1_ago, bar_7_ago)
|
||||
|
||||
# 计算历史 K 线的 Range
|
||||
range_1_ago = bar_1_ago.high - bar_1_ago.low
|
||||
range_7_ago = bar_7_ago.high - bar_7_ago.low
|
||||
|
||||
# 根据策略逻辑计算目标买入价格
|
||||
# 目标买入价 = 当前K线Open - (前1根Range * 因子1 + 前7根Range * 因子2)
|
||||
target_buy_price = bar.open + (
|
||||
range_1_ago * self.open_range_factor_1_short
|
||||
+ range_7_ago * self.open_range_factor_7_short
|
||||
)
|
||||
|
||||
# 确保目标买入价格有效,例如不能是负数
|
||||
target_buy_price = max(0.01, target_buy_price)
|
||||
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=bar.symbol,
|
||||
direction="SELL",
|
||||
volume=trade_volume,
|
||||
price_type="LIMIT",
|
||||
limit_price=target_buy_price,
|
||||
submitted_time=bar.datetime,
|
||||
)
|
||||
self.last_sell_price = target_buy_price
|
||||
new_order = self.send_order(order)
|
||||
# 记录下这个订单的ID,以便在下一根K线开始时进行撤销
|
||||
if new_order:
|
||||
self._last_order_id = new_order.id
|
||||
self.log(
|
||||
f"[{current_datetime}] 策略: 发送限价SELL订单 {self._last_order_id} @ {target_buy_price:.2f}"
|
||||
)
|
||||
else:
|
||||
self.log(f"[{current_datetime}] 策略: 发送订单失败。")
|
||||
|
||||
target_buy_price = bar.open - (
|
||||
range_1_ago * self.open_range_factor_1_long
|
||||
+ range_7_ago * self.open_range_factor_7_long
|
||||
)
|
||||
# 确保目标买入价格有效,例如不能是负数
|
||||
target_buy_price = max(0.01, target_buy_price)
|
||||
|
||||
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
# 创建一个限价多单
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=bar.symbol,
|
||||
direction="BUY",
|
||||
volume=trade_volume,
|
||||
price_type="LIMIT",
|
||||
limit_price=target_buy_price,
|
||||
submitted_time=bar.datetime,
|
||||
)
|
||||
new_order = self.send_order(order)
|
||||
self.last_buy_price = target_buy_price
|
||||
# 记录下这个订单的ID,以便在下一根K线开始时进行撤销
|
||||
if new_order:
|
||||
self._last_order_id = new_order.id
|
||||
self.log(
|
||||
f"[{current_datetime}] 策略: 发送限价BUY订单 {self._last_order_id} @ {target_buy_price:.2f}"
|
||||
)
|
||||
else:
|
||||
self.log(f"[{current_datetime}] 策略: 发送订单失败。")
|
||||
|
||||
# else:
|
||||
# self.log(f"[{current_datetime}] 不满足开仓条件:持仓={current_pos_volume}, 待处理订单={len(pending_orders_after_cancel)}, K线历史长度={len(bar_history)}")
|
||||
|
||||
|
||||
def on_close_bar(self, bar):
|
||||
self.log('on close bar!')
|
||||
self.log(self.get_pending_orders())
|
||||
self.cancel_all_pending_orders()
|
||||
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
"""
|
||||
在合约换月时清空历史K线数据和上次订单ID,避免使用旧合约数据进行计算。
|
||||
"""
|
||||
super().on_rollover(old_symbol, new_symbol) # 调用基类方法打印日志
|
||||
self._last_order_id = None # 清空上次订单ID,因为旧合约订单已取消
|
||||
|
||||
self.log(f"换月完成,清空历史K线数据和上次订单ID,准备新合约交易。")
|
||||
)
|
||||
File diff suppressed because one or more lines are too long
@@ -1,21 +1,18 @@
|
||||
import numpy as np
|
||||
from typing import Optional, Any, List
|
||||
import talib
|
||||
from scipy.signal import stft
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Any, List, Dict
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.indicators.indicators import Empty, ADX
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class SemiVarianceAsymmetryStrategy(Strategy):
|
||||
class SpectralTrendStrategy(Strategy):
|
||||
"""
|
||||
已实现半方差不对称策略 (RSVA)
|
||||
|
||||
核心原理:
|
||||
放弃"阈值计数",改用"波动能量占比"。
|
||||
因子 = (上行波动能量 - 下行波动能量) / 总波动能量
|
||||
|
||||
优势:
|
||||
1. 自适应:自动适应2021的高波动和2023的低波动,无需调整阈值。
|
||||
2. 灵敏:能捕捉到没有大阳线但持续上涨的"蠕动趋势"。
|
||||
3. 稳健:使用平方项(Variance)而非三次方(Skewness),对异常值更鲁棒。
|
||||
频域能量相变策略 - 极简回归版 (动态ATR止损)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -24,170 +21,258 @@ class SemiVarianceAsymmetryStrategy(Strategy):
|
||||
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,
|
||||
|
||||
# --- 市场参数 ---
|
||||
bars_per_day: int = 23,
|
||||
# --- 策略参数 ---
|
||||
spectral_window_days: float = 2.0,
|
||||
low_freq_days: float = 2.0,
|
||||
high_freq_days: float = 1.0,
|
||||
trend_strength_threshold: float = 0.2,
|
||||
exit_threshold: float = 0.1,
|
||||
slope_threshold: float = 0.0,
|
||||
max_hold_days: int = 10,
|
||||
# --- 风控参数 ---
|
||||
stop_loss_atr_multiplier: float = 2.0, # 止损距离是当前ATR的几倍
|
||||
stop_loss_atr_period: int = 14, # ATR计算周期
|
||||
# --- 其他 ---
|
||||
order_direction: Optional[List[str]] = None,
|
||||
indicators: Indicator = None,
|
||||
model_indicator: Indicator = None,
|
||||
reverse: bool = False,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
if order_direction is None:
|
||||
order_direction = ['BUY', 'SELL']
|
||||
|
||||
self.trade_volume = trade_volume
|
||||
self.season_days = season_days
|
||||
self.calc_window = calc_window
|
||||
self.cycle_length = cycle_length
|
||||
self.entry_threshold = entry_threshold
|
||||
self.bars_per_day = bars_per_day
|
||||
self.spectral_window_days = spectral_window_days
|
||||
self.low_freq_days = low_freq_days
|
||||
self.high_freq_days = high_freq_days
|
||||
self.trend_strength_threshold = trend_strength_threshold
|
||||
self.exit_threshold = exit_threshold
|
||||
self.slope_threshold = slope_threshold
|
||||
self.max_hold_days = max_hold_days
|
||||
|
||||
# --- 风控参数 ---
|
||||
self.sl_atr_multiplier = stop_loss_atr_multiplier
|
||||
self.sl_atr_period = stop_loss_atr_period
|
||||
# 注意:移除了 self.stop_loss_price 状态变量,改为实时计算
|
||||
|
||||
self.order_direction = order_direction
|
||||
self.model_indicator = model_indicator or Empty()
|
||||
self.indicators = indicators or Empty()
|
||||
self.reverse = reverse
|
||||
|
||||
# 计算最小历史需求
|
||||
# 我们需要: calc_window 个标准化数据
|
||||
# 每个标准化数据需要回溯: season_days * cycle_length
|
||||
self.min_history = self.calc_window + (self.season_days * self.cycle_length)
|
||||
# 计算窗口大小
|
||||
self.spectral_window = int(self.spectral_window_days * self.bars_per_day)
|
||||
if self.spectral_window % 2 != 0:
|
||||
self.spectral_window += 1
|
||||
|
||||
# 缓冲区设大一点,避免频繁触发边界检查
|
||||
self.calc_buffer_size = self.min_history + 100
|
||||
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.log(f"RSVA Strategy Init: Window={calc_window}, Thresh={entry_threshold}")
|
||||
self.order_id_counter = 0
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
|
||||
self.log(
|
||||
f"SpectralTrendStrategy Init. Window: {self.spectral_window}, Dynamic ATR SL: {self.sl_atr_multiplier}x")
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.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()
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
current_time = self.get_current_time()
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
# 确保数据长度足够计算 STFT 和 ATR
|
||||
required_len = max(self.spectral_window, self.sl_atr_period + 5)
|
||||
if len(bar_history) < required_len:
|
||||
return
|
||||
|
||||
# 强制平仓检查 (时间)
|
||||
# if self.entry_time and (current_time - self.entry_time) >= timedelta(days=self.max_hold_days):
|
||||
# self.close_all_positions(reason="MaxHoldDays")
|
||||
# return
|
||||
|
||||
# 获取数据用于 STFT
|
||||
closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
|
||||
# --- 计算 ATR (每一根Bar都计算最新的ATR) ---
|
||||
atr_window = self.sl_atr_period + 10
|
||||
highs_atr = np.array([b.high for b in bar_history[-atr_window:]], dtype=float)
|
||||
lows_atr = np.array([b.low for b in bar_history[-atr_window:]], dtype=float)
|
||||
closes_atr = np.array([b.close for b in bar_history[-atr_window:]], dtype=float)
|
||||
|
||||
try:
|
||||
atr_values = talib.ATR(highs_atr, lows_atr, closes_atr, timeperiod=self.sl_atr_period)
|
||||
current_atr = atr_values[-1]
|
||||
except Exception as e:
|
||||
self.log(f"ATR Calculation Error: {e}")
|
||||
current_atr = 0.0
|
||||
|
||||
|
||||
|
||||
# 计算核心指标
|
||||
trend_strength, trend_slope = self.calculate_market_state(closes)
|
||||
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
if self.trading:
|
||||
if position_volume == 0:
|
||||
self.evaluate_entry_signal(open_price, trend_strength, trend_slope)
|
||||
else:
|
||||
# 传入 current_atr 用于动态止损计算
|
||||
self.manage_open_position(position_volume, trend_strength, trend_slope, open_price, current_atr)
|
||||
|
||||
def calculate_market_state(self, prices: np.array) -> (float, float):
|
||||
# ... (此处逻辑保持不变) ...
|
||||
if len(prices) < self.spectral_window:
|
||||
return 0.0, 0.0
|
||||
|
||||
window_data = prices[-self.spectral_window:]
|
||||
normalized = (window_data - np.mean(window_data)) / (np.std(window_data) + 1e-8)
|
||||
normalized = normalized[-self.spectral_window:]
|
||||
|
||||
try:
|
||||
f, t, Zxx = stft(
|
||||
normalized,
|
||||
fs=self.bars_per_day,
|
||||
nperseg=self.spectral_window,
|
||||
noverlap=max(0, self.spectral_window // 2),
|
||||
boundary=None,
|
||||
padded=False
|
||||
)
|
||||
except Exception as e:
|
||||
return 0.0, 0.0
|
||||
|
||||
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
|
||||
f = f[valid_mask]
|
||||
Zxx = Zxx[valid_mask, :]
|
||||
|
||||
if Zxx.size == 0 or Zxx.shape[1] == 0:
|
||||
return 0.0, 0.0
|
||||
|
||||
current_energy = np.abs(Zxx[:, -1]) ** 2
|
||||
|
||||
low_freq_mask = f < self.low_freq_bound
|
||||
high_freq_mask = f > self.high_freq_bound
|
||||
|
||||
low_energy = np.sum(current_energy[low_freq_mask]) if np.any(low_freq_mask) else 0.0
|
||||
high_energy = np.sum(current_energy[high_freq_mask]) if np.any(high_freq_mask) else 0.0
|
||||
total_energy = low_energy + high_energy + 1e-8
|
||||
|
||||
trend_strength = low_energy / total_energy
|
||||
|
||||
x = np.arange(len(normalized))
|
||||
slope, intercept = np.polyfit(x, normalized, 1)
|
||||
|
||||
return trend_strength, slope
|
||||
|
||||
def evaluate_entry_signal(self, open_price: float, trend_strength: float, trend_slope: float):
|
||||
"""
|
||||
入场逻辑:不再计算止损价,只负责开仓
|
||||
"""
|
||||
if trend_strength > self.trend_strength_threshold:
|
||||
direction = None
|
||||
|
||||
if "BUY" in self.order_direction and trend_slope > self.slope_threshold:
|
||||
direction = "BUY"
|
||||
elif "SELL" in self.order_direction and trend_slope < -self.slope_threshold:
|
||||
direction = "SELL"
|
||||
|
||||
if direction:
|
||||
if not self.indicators.is_condition_met(*self.get_indicator_tuple()):
|
||||
return
|
||||
|
||||
if not self.model_indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
if self.reverse:
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
|
||||
self.log(f"Signal: {direction} | Strength={trend_strength:.2f} | Slope={trend_slope:.4f}")
|
||||
|
||||
self.send_limit_order(direction, open_price, self.trade_volume, "OPEN")
|
||||
self.entry_time = self.get_current_time()
|
||||
self.position_direction = "LONG" if direction == "BUY" else "SHORT"
|
||||
|
||||
def manage_open_position(self, volume: int, trend_strength: float, trend_slope: float, current_price: float,
|
||||
current_atr: float):
|
||||
"""
|
||||
离场逻辑:实时计算均价止损
|
||||
"""
|
||||
# --- 1. 动态ATR止损检查 ---
|
||||
# 获取持仓均价
|
||||
avg_entry_price = self.get_average_position_price(self.symbol)
|
||||
|
||||
# 确保 ATR 和 均价 有效
|
||||
if current_atr > 0 and avg_entry_price > 0:
|
||||
is_stop_loss = False
|
||||
exit_dir = ""
|
||||
stop_price = 0.0
|
||||
|
||||
sl_distance = current_atr * self.sl_atr_multiplier
|
||||
|
||||
# 多头持仓:止损价 = 均价 - N * ATR
|
||||
if volume > 0:
|
||||
stop_price = avg_entry_price - sl_distance
|
||||
if current_price <= stop_price:
|
||||
is_stop_loss = True
|
||||
exit_dir = "CLOSE_LONG"
|
||||
|
||||
# 空头持仓:止损价 = 均价 + N * ATR
|
||||
elif volume < 0:
|
||||
stop_price = avg_entry_price + sl_distance
|
||||
if current_price >= stop_price:
|
||||
is_stop_loss = True
|
||||
exit_dir = "CLOSE_SHORT"
|
||||
|
||||
if is_stop_loss:
|
||||
self.log(
|
||||
f"ATR STOP LOSS: {exit_dir} | Current={current_price:.2f} | AvgEntry={avg_entry_price:.2f} | ATR={current_atr:.2f} | StopPrice={stop_price:.2f}")
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
return # 止损触发后直接返回
|
||||
|
||||
# --- 2. 信号离场 (原能量逻辑) ---
|
||||
if trend_strength < self.exit_threshold:
|
||||
direction = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
self.log(f"Exit (Signal): {direction} | Strength={trend_strength:.2f} < {self.exit_threshold}")
|
||||
self.close_position(direction, abs(volume))
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
|
||||
# --- 交易辅助 ---
|
||||
def close_all_positions(self, reason=""):
|
||||
positions = self.get_current_positions()
|
||||
if self.symbol in positions and positions[self.symbol] != 0:
|
||||
dir = "CLOSE_LONG" if positions[self.symbol] > 0 else "CLOSE_SHORT"
|
||||
self.log(f"Close All ({reason}): {dir}")
|
||||
self.close_position(dir, abs(positions[self.symbol]))
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
|
||||
def close_position(self, direction: str, volume: int):
|
||||
self.send_market_order(direction, volume, offset="CLOSE")
|
||||
|
||||
def send_market_order(self, direction: str, volume: int, offset: str):
|
||||
order_id = f"{self.symbol}_{direction}_MKT_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(
|
||||
id=f"{self.main_symbol}_{direction}_{current_time.timestamp()}",
|
||||
symbol=self.symbol,
|
||||
direction=direction,
|
||||
volume=volume,
|
||||
price_type="MARKET",
|
||||
submitted_time=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 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}")
|
||||
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)
|
||||
|
||||
1069
src/strategies/Spectral/SpectralTrendStrategy4.ipynb
Normal file
1069
src/strategies/Spectral/SpectralTrendStrategy4.ipynb
Normal file
File diff suppressed because one or more lines are too long
334
src/strategies/Spectral/SpectralTrendStrategy4.py
Normal file
334
src/strategies/Spectral/SpectralTrendStrategy4.py
Normal file
@@ -0,0 +1,334 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
from scipy.signal import stft
|
||||
from datetime import timedelta
|
||||
from typing import Optional, Any, List
|
||||
|
||||
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):
|
||||
"""
|
||||
频域能量相变策略 - 塔勒布宽幅结构版 (Chandelier Exit)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 市场参数 ---
|
||||
bars_per_day: int = 23,
|
||||
# --- STFT 策略参数 ---
|
||||
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,
|
||||
|
||||
# --- 关键风控参数 (Chandelier Exit) ---
|
||||
stop_loss_atr_multiplier: float = 5.0,
|
||||
stop_loss_atr_period: int = 14,
|
||||
|
||||
# --- 其他 ---
|
||||
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.sl_atr_multiplier = stop_loss_atr_multiplier
|
||||
self.sl_atr_period = stop_loss_atr_period
|
||||
|
||||
self.order_direction = order_direction
|
||||
self.model_indicator = model_indicator or Empty()
|
||||
self.indicators = indicators or Empty()
|
||||
self.reverse = reverse
|
||||
|
||||
# 计算 STFT 窗口大小
|
||||
self.spectral_window = int(self.spectral_window_days * self.bars_per_day)
|
||||
if self.spectral_window % 2 != 0:
|
||||
self.spectral_window += 1
|
||||
|
||||
self.low_freq_bound = 1.0 / self.low_freq_days if self.low_freq_days > 0 else float('inf')
|
||||
self.high_freq_bound = 1.0 / self.high_freq_days if self.high_freq_days > 0 else 0.0
|
||||
|
||||
self.order_id_counter = 0
|
||||
|
||||
# --- 持仓状态追踪变量 ---
|
||||
self.entry_price = 0.0
|
||||
self.pos_highest = 0.0
|
||||
self.pos_lowest = 0.0
|
||||
|
||||
self.log(
|
||||
f"SpectralTrend Strategy Initialized. Window: {self.spectral_window}, "
|
||||
f"Chandelier Stop: {self.sl_atr_multiplier}x ATR"
|
||||
)
|
||||
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
"""
|
||||
当回测的合约发生换月时调用此方法。
|
||||
|
||||
功能:
|
||||
1. 切换策略绑定的合约代码。
|
||||
2. 彻底重置所有持仓相关的状态变量(极值、入场价),防止旧合约价格干扰新合约逻辑。
|
||||
3. 确保后续 on_open_bar 中 len(history) 检查生效,从而强制策略重新积累新合约数据,
|
||||
避免 STFT 计算混合了新旧合约的数据(那样会产生极大噪音)。
|
||||
"""
|
||||
self.log(f"合约换月事件触发: {old_symbol} -> {new_symbol}")
|
||||
|
||||
# 1. 更新当前主力合约代码
|
||||
self.symbol = new_symbol
|
||||
self.main_symbol = new_symbol
|
||||
|
||||
# 2. 撤销所有挂单 (虽然引擎通常会做,但策略层再次确保安全)
|
||||
self.cancel_all_pending_orders(old_symbol)
|
||||
self.cancel_all_pending_orders(new_symbol)
|
||||
|
||||
# 3. 重置持仓状态关键变量
|
||||
# 换月后,原有的持仓已被引擎强平,我们需要重置逻辑状态
|
||||
self.entry_price = 0.0
|
||||
self.pos_highest = 0.0 # 必须归零,否则新合约价格可能直接触发基于旧合约高点的止损
|
||||
self.pos_lowest = 0.0
|
||||
|
||||
# 4. (隐式逻辑) 数据重置
|
||||
# 下一次 on_open_bar 调用 get_bar_history() 时,
|
||||
# 如果引擎行为正确,应该返回新合约的数据列表。
|
||||
# 由于 on_open_bar 中有 `if len(bar_history) < required_len: return`
|
||||
# 策略会自动进入"冷冻期",直到新合约积累了足够 spectral_window 长度的K线。
|
||||
# 这完美避免了"引入老合约数据"的问题。
|
||||
|
||||
self.log(f"状态已重置,等待新合约数据积累 ({self.spectral_window} bars)...")
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
# 1. 数据长度检查
|
||||
# (换月后,这里会因为新合约数据不足而直接返回,从而保护了STFT计算)
|
||||
required_len = max(self.spectral_window, self.sl_atr_period + 5)
|
||||
if len(bar_history) < required_len:
|
||||
return
|
||||
|
||||
# 2. 计算 ATR (用于止损)
|
||||
atr_window = self.sl_atr_period + 10
|
||||
highs = np.array([b.high for b in bar_history[-atr_window:]], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history[-atr_window:]], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history[-atr_window:]], dtype=float)
|
||||
|
||||
try:
|
||||
atr_values = talib.ATR(highs, lows, closes, timeperiod=self.sl_atr_period)
|
||||
current_atr = atr_values[-1]
|
||||
if np.isnan(current_atr): current_atr = 0.0
|
||||
except Exception as e:
|
||||
self.log(f"ATR Calc Error: {e}")
|
||||
current_atr = 0.0
|
||||
|
||||
# 3. 计算 STFT 核心指标
|
||||
stft_closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
trend_strength, trend_slope = self.calculate_market_state(stft_closes)
|
||||
|
||||
# 4. 交易逻辑
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
# 获取当前Bar的最高/最低价
|
||||
# 这里假设 bar_history[-1] 是最近一根也就是当前的Bar(根据具体回测引擎定义可能略有不同)
|
||||
current_high = bar_history[-1].high
|
||||
current_low = bar_history[-1].low
|
||||
|
||||
if self.trading:
|
||||
if position_volume == 0:
|
||||
# 重置状态 (双重保险,防止手动平仓后状态未清)
|
||||
if self.pos_highest != 0.0 or self.pos_lowest != 0.0:
|
||||
self.pos_highest = 0.0
|
||||
self.pos_lowest = 0.0
|
||||
self.entry_price = 0.0
|
||||
|
||||
self.evaluate_entry_signal(open_price, trend_strength, trend_slope)
|
||||
else:
|
||||
self.manage_open_position(
|
||||
position_volume,
|
||||
trend_strength,
|
||||
open_price,
|
||||
current_atr,
|
||||
current_high,
|
||||
current_low
|
||||
)
|
||||
|
||||
def calculate_market_state(self, prices: np.array) -> (float, float):
|
||||
"""
|
||||
计算频域能量占比和线性回归斜率(仅用于方向)
|
||||
"""
|
||||
if len(prices) < self.spectral_window:
|
||||
return 0.0, 0.0
|
||||
|
||||
# 标准化处理,消除绝对价格影响
|
||||
window_data = prices[-self.spectral_window:]
|
||||
mean_val = np.mean(window_data)
|
||||
std_val = np.std(window_data)
|
||||
if std_val == 0: std_val = 1.0
|
||||
|
||||
normalized = (window_data - mean_val) / (std_val + 1e-8)
|
||||
|
||||
# STFT 计算
|
||||
try:
|
||||
f, t, Zxx = stft(
|
||||
normalized,
|
||||
fs=self.bars_per_day,
|
||||
nperseg=self.spectral_window,
|
||||
noverlap=max(0, self.spectral_window // 2),
|
||||
boundary=None,
|
||||
padded=False
|
||||
)
|
||||
except Exception:
|
||||
return 0.0, 0.0
|
||||
|
||||
# 提取有效频率
|
||||
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
|
||||
f = f[valid_mask]
|
||||
Zxx = Zxx[valid_mask, :]
|
||||
|
||||
if Zxx.size == 0: return 0.0, 0.0
|
||||
|
||||
# 计算能量
|
||||
current_energy = np.abs(Zxx[:, -1]) ** 2
|
||||
|
||||
low_mask = f < self.low_freq_bound
|
||||
high_mask = f > self.high_freq_bound
|
||||
|
||||
low_energy = np.sum(current_energy[low_mask]) if np.any(low_mask) else 0.0
|
||||
high_energy = np.sum(current_energy[high_mask]) if np.any(high_mask) else 0.0
|
||||
total_energy = low_energy + high_energy + 1e-8
|
||||
|
||||
trend_strength = low_energy / total_energy
|
||||
|
||||
# 计算简单线性斜率用于判断方向
|
||||
x = np.arange(len(normalized))
|
||||
slope, _ = np.polyfit(x, normalized, 1)
|
||||
|
||||
return trend_strength, slope
|
||||
|
||||
def evaluate_entry_signal(self, open_price: float, trend_strength: float, trend_slope: float):
|
||||
"""
|
||||
入场逻辑:高低频能量比 > 阈值 + 斜率确认方向
|
||||
"""
|
||||
if trend_strength > self.trend_strength_threshold:
|
||||
direction = None
|
||||
|
||||
if "BUY" in self.order_direction and trend_slope > self.slope_threshold:
|
||||
direction = "BUY"
|
||||
elif "SELL" in self.order_direction and trend_slope < -self.slope_threshold:
|
||||
direction = "SELL"
|
||||
|
||||
if direction:
|
||||
if not self.indicators.is_condition_met(*self.get_indicator_tuple()):
|
||||
return
|
||||
if not self.model_indicator.is_condition_met(*self.get_indicator_tuple()):
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
if self.reverse:
|
||||
direction = "SELL" if direction == "BUY" else "BUY"
|
||||
|
||||
self.log(f"Entry: {direction} | Strength={trend_strength:.2f} | DirSlope={trend_slope:.4f}")
|
||||
|
||||
self.send_limit_order(direction, open_price, self.trade_volume, "OPEN")
|
||||
|
||||
# 初始化持仓状态
|
||||
self.entry_price = open_price
|
||||
self.pos_highest = open_price
|
||||
self.pos_lowest = open_price
|
||||
|
||||
def manage_open_position(self, volume: int, trend_strength: float, current_price: float,
|
||||
current_atr: float, high_price: float, low_price: float):
|
||||
"""
|
||||
离场逻辑核心:
|
||||
1. 信号离场:能量自然衰竭 (Trend Strength < Threshold)
|
||||
2. 结构离场:Chandelier Exit (吊灯止损) - 宽幅 ATR 追踪
|
||||
"""
|
||||
|
||||
exit_dir = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
|
||||
# --- 更新持仓期间的极值 ---
|
||||
if volume > 0: # 多头
|
||||
if high_price > self.pos_highest or self.pos_highest == 0:
|
||||
self.pos_highest = high_price
|
||||
else: # 空头
|
||||
if (low_price < self.pos_lowest or self.pos_lowest == 0) and low_price > 0:
|
||||
self.pos_lowest = low_price
|
||||
|
||||
# --- 1. 计算宽幅吊灯止损 (Structural Stop) ---
|
||||
is_stop_triggered = False
|
||||
|
||||
# 保护性检查:如果ATR为0或NaN,使用默认逻辑(如价格变动百分比)或跳过止损计算
|
||||
if current_atr > 0 and self.sl_atr_multiplier > 0:
|
||||
stop_distance = current_atr * self.sl_atr_multiplier
|
||||
|
||||
if volume > 0:
|
||||
# 多头止损线 = 最高价 - N * ATR
|
||||
stop_line = self.pos_highest - stop_distance
|
||||
if current_price <= stop_line:
|
||||
is_stop_triggered = True
|
||||
self.log(
|
||||
f"STOP (Long): Price {current_price} <= Highest {self.pos_highest} - {self.sl_atr_multiplier}xATR")
|
||||
|
||||
else:
|
||||
# 空头止损线 = 最低价 + N * ATR
|
||||
stop_line = self.pos_lowest + stop_distance
|
||||
if current_price >= stop_line:
|
||||
is_stop_triggered = True
|
||||
self.log(
|
||||
f"STOP (Short): Price {current_price} >= Lowest {self.pos_lowest} + {self.sl_atr_multiplier}xATR")
|
||||
|
||||
if is_stop_triggered:
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
return # 止损优先
|
||||
|
||||
# --- 2. 信号自然离场 (Signal Exit) ---
|
||||
if trend_strength < self.exit_threshold:
|
||||
self.log(f"Exit (Signal): Strength {trend_strength:.2f} < {self.exit_threshold}")
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
return
|
||||
|
||||
# --- 交易执行辅助函数 ---
|
||||
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)
|
||||
1179
src/strategies/Spectral/SpectralTrendStrategy5.ipynb
Normal file
1179
src/strategies/Spectral/SpectralTrendStrategy5.ipynb
Normal file
File diff suppressed because one or more lines are too long
335
src/strategies/Spectral/SpectralTrendStrategy5.py
Normal file
335
src/strategies/Spectral/SpectralTrendStrategy5.py
Normal file
@@ -0,0 +1,335 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
from scipy.signal import stft
|
||||
from datetime import timedelta
|
||||
from typing import Optional, Any, List
|
||||
|
||||
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):
|
||||
"""
|
||||
频域能量相变策略 - 双因子自适应版 (Dual-Factor Adaptive)
|
||||
|
||||
优化逻辑:
|
||||
不再通过静态参数 reverse 控制方向,而是由两个 Indicator 动态决策:
|
||||
1. 计算 STFT 基础方向 (Base Direction)。
|
||||
2. 检查 indicator_primary:若满足,则采用 Base Direction (顺势/正向)。
|
||||
3. 若不满足,检查 indicator_secondary:若满足,则采用 Reverse Direction (逆势/反转)。
|
||||
4. 若都不满足,保持空仓。
|
||||
|
||||
状态追踪:
|
||||
self.entry_signal_source 会记录当前持仓是 'PRIMARY' 还是 'SECONDARY'。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 市场参数 ---
|
||||
bars_per_day: int = 23,
|
||||
# --- STFT 策略参数 ---
|
||||
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,
|
||||
|
||||
# --- 关键风控参数 (Chandelier Exit) ---
|
||||
stop_loss_atr_multiplier: float = 5.0,
|
||||
stop_loss_atr_period: int = 14,
|
||||
|
||||
# --- 信号控制指标 (核心优化) ---
|
||||
indicator_primary: Indicator = None, # 满足此条件 -> 正向开仓 (reverse=False)
|
||||
indicator_secondary: Indicator = None, # 满足此条件 -> 反向开仓 (reverse=True)
|
||||
model_indicator: Indicator = None, # 可选:额外的AI模型过滤器
|
||||
|
||||
# --- 其他 ---
|
||||
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.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.sl_atr_multiplier = stop_loss_atr_multiplier
|
||||
self.sl_atr_period = stop_loss_atr_period
|
||||
|
||||
self.order_direction = order_direction
|
||||
|
||||
# 初始指标容器 (默认为Empty,即永远返回True或False,视Empty具体实现而定,建议传入具体指标)
|
||||
self.indicator_primary = indicator_primary or Empty()
|
||||
self.indicator_secondary = indicator_secondary or Empty()
|
||||
self.model_indicator = model_indicator or Empty()
|
||||
|
||||
# 计算 STFT 窗口大小
|
||||
self.spectral_window = int(self.spectral_window_days * self.bars_per_day)
|
||||
if self.spectral_window % 2 != 0:
|
||||
self.spectral_window += 1
|
||||
|
||||
self.low_freq_bound = 1.0 / self.low_freq_days if self.low_freq_days > 0 else float('inf')
|
||||
self.high_freq_bound = 1.0 / self.high_freq_days if self.high_freq_days > 0 else 0.0
|
||||
|
||||
self.order_id_counter = 0
|
||||
|
||||
# --- 持仓状态追踪变量 ---
|
||||
self.entry_price = 0.0
|
||||
self.pos_highest = 0.0
|
||||
self.pos_lowest = 0.0
|
||||
|
||||
# 新增:记录开仓信号来源 ('PRIMARY' or 'SECONDARY')
|
||||
self.entry_signal_source = None
|
||||
|
||||
self.log(
|
||||
f"SpectralTrend Dual-Adaptive Strategy Initialized.\n"
|
||||
f"Window: {self.spectral_window}, ATR Stop: {self.sl_atr_multiplier}x\n"
|
||||
f"Primary Ind: {type(self.indicator_primary).__name__}, "
|
||||
f"Secondary Ind: {type(self.indicator_secondary).__name__}"
|
||||
)
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
self.symbol = symbol
|
||||
bar_history = self.get_bar_history()
|
||||
|
||||
self.cancel_all_pending_orders(self.main_symbol)
|
||||
|
||||
# 1. 数据长度检查
|
||||
required_len = max(self.spectral_window, self.sl_atr_period + 5)
|
||||
if len(bar_history) < required_len:
|
||||
return
|
||||
|
||||
# 2. 计算 ATR
|
||||
atr_window = self.sl_atr_period + 10
|
||||
highs = np.array([b.high for b in bar_history[-atr_window:]], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history[-atr_window:]], dtype=float)
|
||||
closes = np.array([b.close for b in bar_history[-atr_window:]], dtype=float)
|
||||
|
||||
try:
|
||||
atr_values = talib.ATR(highs, lows, closes, timeperiod=self.sl_atr_period)
|
||||
current_atr = atr_values[-1]
|
||||
if np.isnan(current_atr): current_atr = 0.0
|
||||
except Exception as e:
|
||||
self.log(f"ATR Calc Error: {e}")
|
||||
current_atr = 0.0
|
||||
|
||||
# 3. 计算 STFT 核心指标
|
||||
stft_closes = np.array([b.close for b in bar_history[-self.spectral_window:]], dtype=float)
|
||||
trend_strength, trend_slope = self.calculate_market_state(stft_closes)
|
||||
|
||||
# 4. 交易逻辑
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
current_high = bar_history[-1].high
|
||||
current_low = bar_history[-1].low
|
||||
|
||||
if self.trading:
|
||||
if position_volume == 0:
|
||||
# 重置所有状态
|
||||
self.pos_highest = 0.0
|
||||
self.pos_lowest = 0.0
|
||||
self.entry_price = 0.0
|
||||
self.entry_signal_source = None # 重置信号来源
|
||||
|
||||
self.evaluate_entry_signal(open_price, trend_strength, trend_slope)
|
||||
else:
|
||||
self.manage_open_position(
|
||||
position_volume,
|
||||
trend_strength,
|
||||
open_price,
|
||||
current_atr,
|
||||
current_high,
|
||||
current_low
|
||||
)
|
||||
|
||||
def calculate_market_state(self, prices: np.array) -> (float, float):
|
||||
"""
|
||||
计算频域能量占比和线性回归斜率
|
||||
"""
|
||||
if len(prices) < self.spectral_window:
|
||||
return 0.0, 0.0
|
||||
|
||||
window_data = prices[-self.spectral_window:]
|
||||
mean_val = np.mean(window_data)
|
||||
std_val = np.std(window_data)
|
||||
if std_val == 0: std_val = 1.0
|
||||
|
||||
normalized = (window_data - mean_val) / (std_val + 1e-8)
|
||||
|
||||
try:
|
||||
f, t, Zxx = stft(
|
||||
normalized,
|
||||
fs=self.bars_per_day,
|
||||
nperseg=self.spectral_window,
|
||||
noverlap=max(0, self.spectral_window // 2),
|
||||
boundary=None,
|
||||
padded=False
|
||||
)
|
||||
except Exception:
|
||||
return 0.0, 0.0
|
||||
|
||||
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
|
||||
f = f[valid_mask]
|
||||
Zxx = Zxx[valid_mask, :]
|
||||
|
||||
if Zxx.size == 0: return 0.0, 0.0
|
||||
|
||||
current_energy = np.abs(Zxx[:, -1]) ** 2
|
||||
low_mask = f < self.low_freq_bound
|
||||
high_mask = f > self.high_freq_bound
|
||||
|
||||
low_energy = np.sum(current_energy[low_mask]) if np.any(low_mask) else 0.0
|
||||
high_energy = np.sum(current_energy[high_mask]) if np.any(high_mask) else 0.0
|
||||
total_energy = low_energy + high_energy + 1e-8
|
||||
|
||||
trend_strength = low_energy / total_energy
|
||||
|
||||
x = np.arange(len(normalized))
|
||||
slope, _ = np.polyfit(x, normalized, 1)
|
||||
|
||||
return trend_strength, slope
|
||||
|
||||
def evaluate_entry_signal(self, open_price: float, trend_strength: float, trend_slope: float):
|
||||
"""
|
||||
入场逻辑优化:双因子控制
|
||||
"""
|
||||
# 1. 基础能量阈值检查
|
||||
if trend_strength <= self.trend_strength_threshold:
|
||||
return
|
||||
|
||||
# 2. 确定 STFT 原始方向 (Raw Direction)
|
||||
raw_direction = None
|
||||
if trend_slope > self.slope_threshold:
|
||||
raw_direction = "BUY"
|
||||
elif trend_slope < -self.slope_threshold:
|
||||
raw_direction = "SELL"
|
||||
|
||||
if not raw_direction:
|
||||
return
|
||||
|
||||
# 3. 双指标分支逻辑 (Dual-Indicator Branching)
|
||||
# 获取指标计算所需的参数 (通常是 bar_history 等,依赖基类实现)
|
||||
indicator_args = self.get_indicator_tuple()
|
||||
|
||||
final_direction = None
|
||||
source_tag = None
|
||||
|
||||
# --- 分支 1: Primary Indicator (优先) ---
|
||||
# 如果满足主条件 -> 使用原始方向 (Equivalent to reverse=False)
|
||||
if self.indicator_primary.is_condition_met(*indicator_args):
|
||||
final_direction = raw_direction
|
||||
source_tag = "PRIMARY"
|
||||
|
||||
# --- 分支 2: Secondary Indicator (备选/Else) ---
|
||||
# 如果不满足主条件,但满足备选条件 -> 使用反转方向 (Equivalent to reverse=True)
|
||||
elif self.indicator_secondary.is_condition_met(*indicator_args):
|
||||
final_direction = "SELL" if raw_direction == "BUY" else "BUY"
|
||||
source_tag = "SECONDARY"
|
||||
|
||||
# --- 分支 3: 都不满足 ---
|
||||
else:
|
||||
return # 放弃交易
|
||||
|
||||
# 4. 全局模型过滤 (可选)
|
||||
if not self.model_indicator.is_condition_met(*indicator_args):
|
||||
return
|
||||
|
||||
# 5. 最终方向检查
|
||||
if final_direction not in self.order_direction:
|
||||
return
|
||||
|
||||
# 6. 执行开仓
|
||||
self.log(
|
||||
f"Entry Triggered [{source_tag}]: "
|
||||
f"Raw={raw_direction} -> Final={final_direction} | "
|
||||
f"Strength={trend_strength:.2f} | Slope={trend_slope:.4f}"
|
||||
)
|
||||
|
||||
self.send_limit_order(final_direction, open_price, self.trade_volume, "OPEN")
|
||||
|
||||
# 更新状态
|
||||
self.entry_price = open_price
|
||||
self.pos_highest = open_price
|
||||
self.pos_lowest = open_price
|
||||
self.entry_signal_source = source_tag # 保存是由哪一个条件控制的
|
||||
|
||||
def manage_open_position(self, volume: int, trend_strength: float, current_price: float,
|
||||
current_atr: float, high_price: float, low_price: float):
|
||||
"""
|
||||
离场逻辑 (保持不变,但日志中可以体现来源)
|
||||
"""
|
||||
exit_dir = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
|
||||
# 更新极值
|
||||
if volume > 0:
|
||||
if high_price > self.pos_highest or self.pos_highest == 0:
|
||||
self.pos_highest = high_price
|
||||
else:
|
||||
if (low_price < self.pos_lowest or self.pos_lowest == 0) and low_price > 0:
|
||||
self.pos_lowest = low_price
|
||||
|
||||
# 1. 结构性止损 (Chandelier Exit)
|
||||
is_stop_triggered = False
|
||||
stop_line = 0.0
|
||||
|
||||
if current_atr > 0:
|
||||
stop_distance = current_atr * self.sl_atr_multiplier
|
||||
if volume > 0:
|
||||
stop_line = self.pos_highest - stop_distance
|
||||
if current_price <= stop_line:
|
||||
is_stop_triggered = True
|
||||
else:
|
||||
stop_line = self.pos_lowest + stop_distance
|
||||
if current_price >= stop_line:
|
||||
is_stop_triggered = True
|
||||
|
||||
if is_stop_triggered:
|
||||
self.log(
|
||||
f"STOP Loss ({self.entry_signal_source}): Price {current_price} hit Chandelier line {stop_line:.2f}")
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
return
|
||||
|
||||
# 2. 信号衰竭离场
|
||||
if trend_strength < self.exit_threshold:
|
||||
self.log(
|
||||
f"Exit Signal ({self.entry_signal_source}): Energy Faded {trend_strength:.2f} < {self.exit_threshold}")
|
||||
self.close_position(exit_dir, abs(volume))
|
||||
return
|
||||
|
||||
# --- 交易辅助 ---
|
||||
def close_position(self, direction: str, volume: int):
|
||||
self.send_market_order(direction, volume, offset="CLOSE")
|
||||
|
||||
def send_market_order(self, direction: str, volume: int, offset: str):
|
||||
order_id = f"{self.symbol}_{direction}_MKT_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(
|
||||
id=order_id, symbol=self.symbol, direction=direction, volume=volume,
|
||||
price_type="MARKET", submitted_time=self.get_current_time(), offset=offset
|
||||
)
|
||||
self.send_order(order)
|
||||
|
||||
def send_limit_order(self, direction: str, limit_price: float, volume: int, offset: str):
|
||||
order_id = f"{self.symbol}_{direction}_LMT_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(
|
||||
id=order_id, symbol=self.symbol, direction=direction, volume=volume,
|
||||
price_type="LIMIT", submitted_time=self.get_current_time(), offset=offset,
|
||||
limit_price=limit_price
|
||||
)
|
||||
self.send_order(order)
|
||||
File diff suppressed because one or more lines are too long
@@ -52,8 +52,12 @@ def run_single_backtest(
|
||||
}
|
||||
# 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['high_freq_days'] = 1
|
||||
strategy_parameters['exit_threshold'] = max(strategy_parameters['trend_strength_threshold'] - 0.3, 0)
|
||||
if 'exit_threshold' in common_config:
|
||||
strategy_parameters['exit_threshold'] = common_config['exit_threshold']
|
||||
if 'reverse' in common_config:
|
||||
strategy_parameters['reverse'] = common_config['reverse']
|
||||
|
||||
# 打印当前进程正在处理的组合信息
|
||||
# 注意:多进程打印会交错显示
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,32 +1,37 @@
|
||||
import numpy as np
|
||||
from scipy.signal import stft
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Any, List, Dict
|
||||
import talib
|
||||
from typing import Optional, Any, List
|
||||
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.indicators.indicators import Empty, NormalizedATR, AtrVolatility
|
||||
from src.indicators.indicators import Empty
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 策略实现 (SpectralTrendStrategy)
|
||||
# =============================================================================
|
||||
|
||||
class SpectralTrendStrategy(Strategy):
|
||||
class AdaptiveRobustSpectralStrategy(Strategy):
|
||||
"""
|
||||
频域能量相变策略 - 捕获肥尾趋势
|
||||
自适应鲁棒谱策略 (Adaptive Robust Spectral Strategy)
|
||||
|
||||
核心哲学:
|
||||
1. 显式傅里叶变换: 直接分离低频(趋势)、高频(噪音)能量
|
||||
2. 相变临界点: 仅当低频能量占比 > 阈值时入场
|
||||
3. 低频交易: 每月仅2-5次信号,持仓数日捕获肥尾
|
||||
4. 完全参数化: 无硬编码,适配任何市场时间结构
|
||||
--- 核心逻辑重构 ---
|
||||
1. 对数空间处理 (Log Space):
|
||||
所有计算基于 ln(Price)。这彻底解决了价格水平变化(如3000点到1000点)带来的量纲问题。
|
||||
因子本质变为处理“收益率”,而非“价格差”。
|
||||
|
||||
参数说明:
|
||||
- bars_per_day: 市场每日K线数量 (e.g., 23 for 15min US markets)
|
||||
- low_freq_days: 低频定义下限 (天), 默认2.0
|
||||
- high_freq_days: 高频定义上限 (天), 默认1.0
|
||||
2. 基于信噪比的自适应 (SNR-based Adaptation):
|
||||
弃用波动率自适应。改用考夫曼效率比 (Efficiency Ratio, ER) 作为信噪比代理。
|
||||
- 趋势效率高 (ER -> 1) => 滤波器周期变短 (减少滞后,捕捉快趋势)。
|
||||
- 趋势效率低 (ER -> 0) => 滤波器周期变长 (增强平滑,过滤震荡)。
|
||||
这符合信号处理原理:在低信噪比环境下,必须降低带宽以提取信号。
|
||||
|
||||
3. 风险调整动量因子 (Risk-Adjusted Momentum Factor):
|
||||
因子 = (滤波后的对数趋势速度) / (对数收益率波动率)。
|
||||
物理含义:【瞬时夏普比率】。
|
||||
这是一种经济学意义上最平稳的归一化方式:衡量单位风险下的趋势收益。
|
||||
|
||||
--- 关键因子 (Grid Search Targets) ---
|
||||
1. min_period: 最快响应周期 (对应 ER=1.0 时的极速状态)
|
||||
2. max_period: 最慢平滑周期 (对应 ER=0.0 时的混沌状态)
|
||||
3. entry_threshold: 入场阈值 (建议 0.5 ~ 1.5,即趋势强度需达到 0.5~1.5 倍标准差)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -35,247 +40,229 @@ class SpectralTrendStrategy(Strategy):
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 【市场结构参数】 ---
|
||||
bars_per_day: int = 23, # 关键: 适配23根/天的市场
|
||||
# --- 【频域核心参数】 ---
|
||||
spectral_window_days: float = 2.0, # STFT窗口大小(天)
|
||||
low_freq_days: float = 2.0, # 低频下限(天)
|
||||
high_freq_days: float = 1.0, # 高频上限(天)
|
||||
trend_strength_threshold: float = 0.8, # 相变临界值
|
||||
exit_threshold: float = 0.4, # 退出阈值
|
||||
# --- 【持仓管理】 ---
|
||||
max_hold_days: int = 10, # 最大持仓天数
|
||||
# --- 市场参数 ---
|
||||
bars_per_day: int = 23,
|
||||
|
||||
# --- [关键因子 - 滤波器频带] ---
|
||||
min_period: float = 10.0, # 高信噪比时的最短周期
|
||||
max_period: float = 60.0, # 低信噪比时的最长周期
|
||||
|
||||
# --- [关键因子 - 信号阈值] ---
|
||||
entry_threshold: float = 0.4, # 风险调整后的动量阈值 (单位: Sigma)
|
||||
|
||||
# --- [辅助参数] ---
|
||||
er_window: int = 30, # 计算效率比的回看窗口
|
||||
vol_window: int = 60, # 计算波动率(分母)的窗口
|
||||
exit_threshold: float = 0.4, # 因子回落到此值以下平仓 (Alpha Decay)
|
||||
|
||||
# --- 其他 ---
|
||||
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()] # 保持兼容性
|
||||
|
||||
# --- 参数赋值 (完全参数化) ---
|
||||
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.min_period = min_period
|
||||
self.max_period = max_period
|
||||
self.entry_threshold = entry_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
|
||||
self.er_window = er_window
|
||||
self.vol_window = vol_window
|
||||
|
||||
# 频率边界 (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.order_direction = order_direction or ['BUY', 'SELL']
|
||||
self.model_indicator = model_indicator or Empty()
|
||||
self.indicators = indicators or Empty()
|
||||
self.reverse = reverse
|
||||
|
||||
# --- 内部状态变量 ---
|
||||
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)")
|
||||
# --- 滤波器状态 (基于 Log Price) ---
|
||||
self.filt_1 = 0.0
|
||||
self.filt_2 = 0.0
|
||||
self.prev_log_price_1 = 0.0
|
||||
self.prev_log_price_2 = 0.0
|
||||
self.init_flag = False
|
||||
|
||||
self.log(
|
||||
f"RobustSpectral Init: PeriodRange=[{self.min_period}, {self.max_period}], "
|
||||
f"EntrySigma={self.entry_threshold}"
|
||||
)
|
||||
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
self.log(f"Rollover: {old_symbol} -> {new_symbol}")
|
||||
self.symbol = new_symbol
|
||||
self.main_symbol = new_symbol
|
||||
self.cancel_all_pending_orders(old_symbol)
|
||||
|
||||
# 重置滤波器状态
|
||||
self.filt_1 = 0.0
|
||||
self.filt_2 = 0.0
|
||||
self.prev_log_price_1 = 0.0
|
||||
self.prev_log_price_2 = 0.0
|
||||
self.init_flag = False
|
||||
|
||||
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")
|
||||
# 1. 数据准备
|
||||
required_len = max(self.er_window, self.vol_window) + 5
|
||||
if len(bar_history) < required_len:
|
||||
return
|
||||
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
# 获取历史价格 (使用完整历史)
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
|
||||
# 【核心】计算频域趋势强度 (显式傅里叶)
|
||||
trend_strength, dominant_freq = self.calculate_trend_strength(closes)
|
||||
self.last_trend_strength = trend_strength
|
||||
self.last_dominant_freq = dominant_freq
|
||||
# --- 核心步骤 A: 进入对数空间 (Log Space Transformation) ---
|
||||
# 所有的处理都基于 log_price,彻底消除绝对价格影响
|
||||
log_closes = np.log(closes)
|
||||
current_log_price = log_closes[-1]
|
||||
|
||||
# 检查最大持仓时间 (防止极端事件)
|
||||
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
|
||||
# --- 核心步骤 B: 计算信噪比 (Efficiency Ratio) ---
|
||||
# ER = Net_Change / Total_Path
|
||||
# 我们计算 log_price 的 ER
|
||||
window_log = log_closes[-self.er_window:]
|
||||
net_change = abs(window_log[-1] - window_log[0])
|
||||
path_sum = np.sum(np.abs(np.diff(window_log)))
|
||||
|
||||
efficiency = 0.0
|
||||
if path_sum > 1e-9:
|
||||
efficiency = net_change / path_sum
|
||||
|
||||
# --- 核心步骤 C: 自适应周期映射 (Adaptive Mapping) ---
|
||||
# 逻辑:Efficiency 越高 (Trend),Period 越短 (Fast)。
|
||||
# 这是一个线性插值映射:
|
||||
# ER = 1.0 -> min_period
|
||||
# ER = 0.0 -> max_period
|
||||
# Formula: Period = Max - ER * (Max - Min)
|
||||
dynamic_period = self.max_period - efficiency * (self.max_period - self.min_period)
|
||||
|
||||
# --- 核心步骤 D: 滤波 (Filtering) ---
|
||||
# 暂存 t-1 时刻的平滑趋势
|
||||
prev_smooth_trend = self.filt_1
|
||||
|
||||
# 更新滤波器
|
||||
current_smooth_trend = self.update_super_smoother(current_log_price, dynamic_period)
|
||||
|
||||
if not self.init_flag or prev_smooth_trend == 0:
|
||||
return
|
||||
|
||||
# 核心逻辑:相变入场/退出
|
||||
if position_volume == 0:
|
||||
self.evaluate_entry_signal(open_price, trend_strength, dominant_freq)
|
||||
# --- 核心步骤 E: 构建风险调整因子 (Risk-Adjusted Factor) ---
|
||||
|
||||
# 1. 分子:趋势速度 (Trend Velocity)
|
||||
# 代表平滑后的对数收益率 (Smoothed Log Return)
|
||||
trend_velocity = current_smooth_trend - prev_smooth_trend
|
||||
|
||||
# 2. 分母:噪音波动率 (Noise Volatility)
|
||||
# 计算原始对数收益率的滚动标准差
|
||||
raw_log_returns = np.diff(log_closes[-self.vol_window - 1:])
|
||||
noise_vol = np.std(raw_log_returns)
|
||||
|
||||
if noise_vol < 1e-9: noise_vol = 1e-9
|
||||
|
||||
# 3. 最终因子:瞬时夏普比率 (Instantaneous Sharpe)
|
||||
# 物理意义:当前趋势带来的收益 是 市场噪音波动率 的多少倍
|
||||
risk_adjusted_signal = trend_velocity / noise_vol
|
||||
|
||||
# --- 信号反转处理 ---
|
||||
is_trend_mode = self.model_indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
if not is_trend_mode:
|
||||
risk_adjusted_signal = -risk_adjusted_signal
|
||||
|
||||
if self.reverse:
|
||||
final_signal = -risk_adjusted_signal
|
||||
else:
|
||||
self.manage_open_position(position_volume, trend_strength, dominant_freq)
|
||||
final_signal = risk_adjusted_signal
|
||||
|
||||
def calculate_trend_strength(self, prices: np.array) -> (float, float):
|
||||
# --- 交易执行 (Reflexivity Exit) ---
|
||||
position_volume = self.get_current_positions().get(self.symbol, 0)
|
||||
|
||||
# 灾难风控 (基于 ATR 的硬止损,防止黑天鹅)
|
||||
# 此处省略具体实现,实盘中应始终保留一个极宽的止损单
|
||||
|
||||
if position_volume == 0:
|
||||
# 入场:因子强度显著 (大于 N 倍标准差)
|
||||
if final_signal > self.entry_threshold:
|
||||
self.execute_signal("BUY", open_price)
|
||||
elif final_signal < -self.entry_threshold:
|
||||
self.execute_signal("SELL", open_price)
|
||||
|
||||
elif position_volume > 0:
|
||||
# 离场:Alpha 衰竭 (因子回落到 0 附近)
|
||||
# 这意味着趋势不再提供超额的风险调整收益
|
||||
if final_signal < self.exit_threshold:
|
||||
self.log(f"Alpha Decay (Long): {final_signal:.3f} < {self.exit_threshold}")
|
||||
self.close_position("CLOSE_LONG", abs(position_volume))
|
||||
|
||||
elif position_volume < 0:
|
||||
# 离场
|
||||
if final_signal > -self.exit_threshold:
|
||||
self.log(f"Alpha Decay (Short): {final_signal:.3f} > {-self.exit_threshold}")
|
||||
self.close_position("CLOSE_SHORT", abs(position_volume))
|
||||
|
||||
def update_super_smoother(self, log_price: float, period: float) -> float:
|
||||
"""
|
||||
【显式傅里叶】计算低频能量占比 (完全参数化)
|
||||
|
||||
步骤:
|
||||
1. 价格归一化 (窗口内)
|
||||
2. 短时傅里叶变换 (STFT) - 采样率=bars_per_day
|
||||
3. 动态计算频段边界 (基于bars_per_day)
|
||||
4. 趋势强度 = 低频能量 / (低频+高频能量)
|
||||
Ehlers Super Smoother Filter (针对 Log Price)
|
||||
"""
|
||||
# 1. 验证数据长度
|
||||
if len(prices) < self.spectral_window:
|
||||
return 0.0, 0.0
|
||||
if self.filt_1 == 0 and self.filt_2 == 0:
|
||||
self.filt_1 = log_price
|
||||
self.filt_2 = log_price
|
||||
self.prev_log_price_1 = log_price
|
||||
self.prev_log_price_2 = log_price
|
||||
return log_price
|
||||
|
||||
# 2. 价格归一化 (仅使用窗口内数据)
|
||||
window_data = prices[-self.spectral_window:]
|
||||
normalized = (window_data - np.mean(window_data)) / (np.std(window_data) + 1e-8)
|
||||
if period < 2: period = 2
|
||||
|
||||
# 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
|
||||
# 弧度转换
|
||||
sqrt2 = 1.41421356
|
||||
pi = 3.14159265359
|
||||
lambda_val = (sqrt2 * pi) / period
|
||||
a1 = np.exp(-lambda_val)
|
||||
|
||||
# 4. 过滤无效频率 (STFT返回频率范围: 0 到 fs/2)
|
||||
valid_mask = (f >= 0) & (f <= self.bars_per_day / 2)
|
||||
f = f[valid_mask]
|
||||
Zxx = Zxx[valid_mask, :]
|
||||
c2 = 2 * a1 * np.cos(lambda_val)
|
||||
c3 = - (a1 * a1)
|
||||
c1 = 1 - c2 - c3
|
||||
|
||||
if Zxx.size == 0 or Zxx.shape[1] == 0:
|
||||
return 0.0, 0.0
|
||||
# 递归
|
||||
filt = c1 * (log_price + self.prev_log_price_1) / 2 + c2 * self.filt_1 + c3 * self.filt_2
|
||||
|
||||
# 5. 计算最新时间点的能量
|
||||
current_energy = np.abs(Zxx[:, -1]) ** 2
|
||||
# 状态更新
|
||||
self.filt_2 = self.filt_1
|
||||
self.filt_1 = filt
|
||||
self.prev_log_price_2 = self.prev_log_price_1
|
||||
self.prev_log_price_1 = log_price
|
||||
|
||||
# 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
|
||||
self.init_flag = True
|
||||
return filt
|
||||
|
||||
# 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):
|
||||
"""评估相变入场信号"""
|
||||
# 仅当趋势强度跨越临界点且有明确周期时入场
|
||||
if trend_strength > self.trend_strength_threshold and dominant_freq > self.low_freq_days:
|
||||
direction = None
|
||||
|
||||
indicator = self.model_indicator
|
||||
|
||||
# 做多信号: 价格在窗口均值上方
|
||||
closes = np.array([b.close for b in self.get_bar_history()[-self.spectral_window:]], dtype=float)
|
||||
if "BUY" in self.order_direction and np.mean(closes[-5:]) > np.mean(closes):
|
||||
direction = "BUY" if indicator.is_condition_met(*self.get_indicator_tuple()) else "SELL"
|
||||
# 做空信号: 价格在窗口均值下方
|
||||
elif "SELL" in self.order_direction and np.mean(closes[-5:]) < np.mean(closes):
|
||||
direction = "SELL" if indicator.is_condition_met(*self.get_indicator_tuple()) else "BUY"
|
||||
|
||||
if direction:
|
||||
self.log(
|
||||
f"Phase Transition Entry: {direction} | Strength={trend_strength:.2f} | Dominant Period={dominant_freq:.1f}d")
|
||||
self.send_limit_order(direction, open_price, self.trade_volume, "OPEN")
|
||||
self.entry_time = self.get_current_time()
|
||||
self.position_direction = "LONG" if direction == "BUY" else "SHORT"
|
||||
|
||||
def manage_open_position(self, volume: int, trend_strength: float, dominant_freq: float):
|
||||
"""管理持仓:仅当相变逆转时退出"""
|
||||
# 相变逆转条件: 趋势强度 < 退出阈值
|
||||
if trend_strength < self.exit_threshold:
|
||||
direction = "CLOSE_LONG" if volume > 0 else "CLOSE_SHORT"
|
||||
self.log(f"Phase Transition Exit: {direction} | Strength={trend_strength:.2f} < {self.exit_threshold}")
|
||||
self.close_position(direction, abs(volume))
|
||||
self.entry_time = None
|
||||
self.position_direction = None
|
||||
|
||||
# --- 辅助函数区 ---
|
||||
def close_all_positions(self):
|
||||
"""强制平仓所有头寸"""
|
||||
positions = self.get_current_positions()
|
||||
if self.symbol in positions and positions[self.symbol] != 0:
|
||||
direction = "CLOSE_LONG" if positions[self.symbol] > 0 else "CLOSE_SHORT"
|
||||
self.close_position(direction, abs(positions[self.symbol]))
|
||||
self.log(f"Forced exit of {abs(positions[self.symbol])} contracts")
|
||||
def execute_signal(self, direction: str, price: float):
|
||||
if not self.indicators.is_condition_met(*self.get_indicator_tuple()): return
|
||||
if direction not in self.order_direction: return
|
||||
self.log(f"Entry: {direction} @ {price}")
|
||||
self.send_limit_order(direction, price, self.trade_volume, "OPEN")
|
||||
|
||||
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}_{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}_{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
|
||||
self.send_order(order)
|
||||
File diff suppressed because one or more lines are too long
@@ -3,13 +3,20 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
import math
|
||||
from email.utils import formataddr
|
||||
from typing import Dict, Any, Optional, List, TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
# --- 新增:邮件功能所需的库 ---
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.header import Header
|
||||
# -----------------------------
|
||||
|
||||
# 使用 TYPE_CHECKING 避免循环导入,但保留类型提示
|
||||
from ..backtest_context import BacktestContext # 转发引用 BacktestEngine
|
||||
from ..core_data import Bar, Order, Trade # 导入必要的类型
|
||||
from ..backtest_context import BacktestContext
|
||||
from ..core_data import Bar, Order, Trade
|
||||
|
||||
|
||||
class Strategy(ABC):
|
||||
@@ -23,115 +30,143 @@ class Strategy(ABC):
|
||||
context: "BacktestContext",
|
||||
symbol: str,
|
||||
enable_log: bool = True,
|
||||
real_trading: bool = False, # <-- 新增属性
|
||||
**params: Any,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
context (BacktestEngine): 回测引擎实例,作为策略的上下文,提供与模拟器等的交互接口。
|
||||
context (BacktestEngine): 回测引擎实例。
|
||||
symbol (str): 策略操作的合约Symbol。
|
||||
enable_log (bool): 是否开启日志。
|
||||
real_trading (bool): 是否为实盘模式。如果为True,将触发邮件提醒等实盘特定操作。
|
||||
**params (Any): 其他策略特定参数。
|
||||
"""
|
||||
self.context = context # 存储 context 对象
|
||||
self.symbol = symbol # 策略操作的合约Symbol
|
||||
self.context = context
|
||||
self.symbol = symbol
|
||||
self.main_symbol = symbol
|
||||
self.params = params
|
||||
self.enable_log = enable_log
|
||||
self.trading = False
|
||||
self.real_trading = real_trading # <-- 保存新增的属性
|
||||
|
||||
# 缓存指标用
|
||||
self._indicator_cache = None # type: Optional[Tuple[np.ndarray, ...]]
|
||||
self._cache_length = 0 # 上次缓存时数据长度
|
||||
self._indicator_cache = None
|
||||
self._cache_length = 0
|
||||
|
||||
if self.real_trading:
|
||||
self.log("策略已启用实盘模式,订单信号将会发送邮件提醒。")
|
||||
|
||||
# --- 新增:发送邮件的私有方法 ---
|
||||
def _send_email_notification(self, order: "Order"):
|
||||
"""
|
||||
[修复版] 使用QQ邮箱发送开仓/平仓信号邮件。
|
||||
使用 formataddr 确保邮件头格式正确。
|
||||
"""
|
||||
# --- 邮件配置 (请务必修改为你的信息) ---
|
||||
mail_host = "smtp.qq.com"
|
||||
mail_port = 465
|
||||
mail_pass = "oxrhuqpyxaxcbahd"
|
||||
sender_email = "1300336796@qq.com"
|
||||
receiver_email = "1300336796@qq.com"
|
||||
|
||||
# --- 邮件内容构建 ---
|
||||
order_details = (
|
||||
f"合约代码 (Symbol): {order.symbol}\n"
|
||||
f"订单方向 (Direction): {order.direction}\n"
|
||||
f"下单手数 (Volume): {order.volume}\n"
|
||||
f"价格类型 (Price Type): {order.price_type}\n"
|
||||
)
|
||||
if order.price_type == 'LIMIT':
|
||||
order_details += f"限定价格 (Limit Price): {order.limit_price}\n"
|
||||
|
||||
email_subject = f"量化交易信号 - {order.direction} {order.symbol}"
|
||||
email_body = f"策略发出了一个新的交易指令:\n\n{order_details}\n请关注账户状态。\n"
|
||||
|
||||
# --- 发送邮件 ---
|
||||
msg = MIMEText(email_body, 'plain', 'utf-8')
|
||||
|
||||
# --- [核心修改] 使用 formataddr 来格式化发件人和收件人 ---
|
||||
msg['From'] = formataddr(("量化策略提醒", sender_email))
|
||||
msg['To'] = formataddr(("管理员", receiver_email))
|
||||
# -----------------------------------------------------------
|
||||
|
||||
# 主题仍然可以使用 Header 来确保非ASCII字符正确编码
|
||||
msg['Subject'] = Header(email_subject, 'utf-8')
|
||||
|
||||
try:
|
||||
self.log(f"正在尝试发送订单邮件提醒至 {receiver_email}...")
|
||||
server = smtplib.SMTP_SSL(mail_host, mail_port)
|
||||
server.login(sender_email, mail_pass)
|
||||
server.sendmail(sender_email, [receiver_email], msg.as_string())
|
||||
server.quit()
|
||||
self.log("✅ 邮件提醒发送成功。")
|
||||
except Exception as e:
|
||||
# 打印更详细的错误信息
|
||||
self.log(f"❌ 邮件提醒发送失败! 错误详情: {e}")
|
||||
|
||||
def on_init(self):
|
||||
"""
|
||||
策略初始化时调用(在回测开始前)。
|
||||
可用于设置初始状态或打印信息。
|
||||
"""
|
||||
"""策略初始化时调用。"""
|
||||
print(f"{self.__class__.__name__} 策略初始化回调被调用。")
|
||||
|
||||
def on_trade(self, trade: "Trade"):
|
||||
"""
|
||||
当模拟器成功执行一笔交易时调用。
|
||||
可用于更新策略内部持仓状态或记录交易。
|
||||
|
||||
Args:
|
||||
trade (Trade): 已完成的交易记录。
|
||||
"""
|
||||
# print(f"策略接收到交易: {trade.direction} {trade.volume} {trade.symbol} @ {trade.price:.2f}")
|
||||
pass # 默认不执行任何操作,具体策略可覆盖
|
||||
"""当模拟器成功执行一笔交易时调用。"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def on_open_bar(self, open: float, symbol: str):
|
||||
"""
|
||||
每当新的K线数据到来时调用此方法。
|
||||
Args:
|
||||
bar (Bar): 当前的K线数据对象。
|
||||
next_bar_open (Optional[float]): 下一根K线的开盘价,如果存在的话。
|
||||
"""
|
||||
"""每当新的K线 Open 时调用此方法。"""
|
||||
pass
|
||||
|
||||
def on_close_bar(self, bar: "Bar"):
|
||||
"""
|
||||
每当新的K线数据到来时调用此方法。
|
||||
Args:
|
||||
bar (Bar): 当前的K线数据对象。
|
||||
next_bar_close (Optional[float]): 下一根K线的开盘价,如果存在的话。
|
||||
"""
|
||||
"""每当新的K线 Close 时调用此方法。"""
|
||||
pass
|
||||
|
||||
def on_start_trading(self):
|
||||
pass
|
||||
|
||||
# --- 新增/修改的辅助方法 ---
|
||||
|
||||
# --- 修改 send_order 方法 ---
|
||||
def send_order(self, order: "Order") -> Optional[Order]:
|
||||
"""
|
||||
发送订单的辅助方法。
|
||||
会在 BaseStrategy 内部构建 Order 对象,并通过 context 转发给模拟器。
|
||||
- 在实盘模式下 (real_trading=True),会发送邮件提醒。
|
||||
- 会自动处理限价单价格的取整。
|
||||
"""
|
||||
if not self.trading:
|
||||
return None
|
||||
if self.context.is_rollover_bar:
|
||||
|
||||
# 在换月期间,通常禁止开新仓,但允许平仓
|
||||
if self.context.is_rollover_bar and "OPEN" in order.offset:
|
||||
self.log(f"当前是换月K线,禁止开仓订单")
|
||||
return None
|
||||
|
||||
|
||||
# --- 核心修改:在发送订单前,检查是否需要发送邮件 ---
|
||||
if self.real_trading:
|
||||
# 调用新方法来发送邮件
|
||||
self._send_email_notification(order)
|
||||
|
||||
# (原有的价格处理逻辑保持不变)
|
||||
if order.price_type == 'LIMIT':
|
||||
limit_price = order.limit_price
|
||||
if order.direction in ["BUY", "CLOSE_SHORT"]:
|
||||
# 买入限价单(或平空),希望以更低或相等的价格成交,
|
||||
# 所以向下取整,确保挂单价格不高于预期。
|
||||
# 例如:价格100.3,tick_size=1 -> math.floor(100.3) = 100
|
||||
# 价格100.8,tick_size=1 -> math.floor(100.8) = 100
|
||||
order.limit_price = math.floor(limit_price)
|
||||
order.limit_price = math.floor(limit_price)
|
||||
elif order.direction in ["SELL", "CLOSE_LONG"]:
|
||||
# 卖出限价单(或平多),希望以更高或相等的价格成交,
|
||||
# 所以向上取整,确保挂单价格不低于预期。
|
||||
# 例如:价格100.3,tick_size=1 -> math.ceil(100.3) = 101
|
||||
# 价格100.8,tick_size=1 -> math.ceil(100.8) = 101
|
||||
order.limit_price = math.ceil(limit_price)
|
||||
order.limit_price = math.ceil(limit_price)
|
||||
|
||||
return self.context.send_order(order)
|
||||
|
||||
# (后续的其他方法保持不变)
|
||||
def cancel_order(self, order_id: str) -> bool:
|
||||
"""
|
||||
取消指定ID的订单。
|
||||
通过 context 调用模拟器的 cancel_order 方法。
|
||||
"""
|
||||
"""取消指定ID的订单。"""
|
||||
if not self.trading:
|
||||
return False
|
||||
|
||||
return self.context.cancel_order(order_id)
|
||||
|
||||
|
||||
def cancel_all_pending_orders(self, main_symbol = None) -> int:
|
||||
"""取消当前策略的未决订单,仅限于当前策略关注的Symbol。"""
|
||||
# 注意:在换月模式下,引擎会自动取消旧合约的挂单,这里是策略主动取消
|
||||
def cancel_all_pending_orders(self, main_symbol=None) -> int:
|
||||
"""取消当前策略的未决订单。"""
|
||||
if not self.trading:
|
||||
return 0
|
||||
|
||||
pending_orders = self.get_pending_orders()
|
||||
cancelled_count = 0
|
||||
# orders_to_cancel = [
|
||||
# order.id for order in pending_orders.values() if order.symbol == self.symbol
|
||||
# ]
|
||||
if main_symbol is not None:
|
||||
orders_to_cancel = [
|
||||
order.id for order in pending_orders.values() if main_symbol in order.symbol
|
||||
@@ -146,11 +181,11 @@ class Strategy(ABC):
|
||||
return cancelled_count
|
||||
|
||||
def get_current_positions(self) -> Dict[str, int]:
|
||||
"""获取所有当前持仓 (可能包含多个合约)。"""
|
||||
"""获取所有当前持仓。"""
|
||||
return self.context.get_current_positions()
|
||||
|
||||
def get_pending_orders(self) -> Dict[str, "Order"]:
|
||||
"""获取所有当前待处理订单的副本 (可能包含多个合约)。"""
|
||||
"""获取所有当前待处理订单。"""
|
||||
return self.context.get_pending_orders()
|
||||
|
||||
def get_average_position_price(self, symbol: str) -> Optional[float]:
|
||||
@@ -166,46 +201,25 @@ class Strategy(ABC):
|
||||
return self.context.get_current_time()
|
||||
|
||||
def log(self, *args: Any, **kwargs: Any):
|
||||
"""
|
||||
统一的日志打印方法。
|
||||
如果 enable_log 为 True,则打印消息到控制台,并包含当前模拟时间。
|
||||
支持传入多个参数,像 print() 函数一样使用。
|
||||
"""
|
||||
"""统一的日志打印方法。"""
|
||||
if self.enable_log:
|
||||
# 尝试获取当前模拟时间,如果模拟器或时间不可用,则跳过时间前缀
|
||||
try:
|
||||
current_time_str = self.context.get_current_time().strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
time_prefix = f"[{current_time_str}] "
|
||||
except AttributeError:
|
||||
# 如果获取不到时间(例如在策略初始化时,模拟器时间还未设置),则不加时间前缀
|
||||
time_prefix = ""
|
||||
|
||||
# 使用 f-string 结合 *args 来构建消息
|
||||
# print() 函数会将 *args 自动用空格分隔,这里我们模仿这个行为
|
||||
message = " ".join(map(str, args))
|
||||
|
||||
# 你可以将其他 kwargs (如 sep, end, file, flush) 传递给 print,
|
||||
# 但通常日志方法不会频繁使用这些。这里只支持最基础的打印。
|
||||
print(f"{time_prefix}策略 ({self.symbol}): {message}")
|
||||
|
||||
def on_rollover(self, old_symbol: str, new_symbol: str):
|
||||
"""
|
||||
当回测的合约发生换月时调用此方法。
|
||||
子类可以重写此方法来执行换月相关的逻辑(例如,调整目标仓位,清空历史数据)。
|
||||
注意:在调用此方法前,引擎已强制平仓旧合约的所有仓位并取消所有挂单。
|
||||
Args:
|
||||
old_symbol (str): 旧的合约代码。
|
||||
new_symbol (str): 新的合约代码。
|
||||
"""
|
||||
"""当合约发生换月时调用此方法。"""
|
||||
self.log(f"合约换月事件: 从 {old_symbol} 切换到 {new_symbol}")
|
||||
# 默认实现可以为空,子类根据需要重写
|
||||
pass
|
||||
|
||||
def get_bar_history(self):
|
||||
return self.context.get_bar_history()
|
||||
|
||||
|
||||
def get_price_history(self, key: str):
|
||||
return self.context.get_price_history(key)
|
||||
@@ -214,21 +228,15 @@ class Strategy(ABC):
|
||||
"""获取价格数据的 numpy 数组元组,带缓存功能。"""
|
||||
close_data = self.get_price_history("close")
|
||||
current_length = len(close_data)
|
||||
|
||||
# 如果长度没有变化,直接返回缓存
|
||||
if self._indicator_cache is not None and current_length == self._cache_length:
|
||||
return self._indicator_cache
|
||||
|
||||
# 数据有变化,重新创建数组并更新缓存
|
||||
close = np.array(close_data[-1000:])
|
||||
open_price = np.array(self.get_price_history("open")[-1000:])
|
||||
high = np.array(self.get_price_history("high")[-1000:])
|
||||
low = np.array(self.get_price_history("low")[-1000:])
|
||||
volume = np.array(self.get_price_history("volume")[-1000:])
|
||||
|
||||
self._indicator_cache = (close, open_price, high, low, volume)
|
||||
self._cache_length = current_length
|
||||
|
||||
return self._indicator_cache
|
||||
|
||||
def save_state(self, state: Any) -> None:
|
||||
|
||||
@@ -403,6 +403,7 @@ class TqsdkEngine:
|
||||
and self.last_processed_bar is not None
|
||||
and self._last_underlying_symbol != self.last_processed_bar.symbol
|
||||
):
|
||||
|
||||
self._is_rollover_bar = True
|
||||
print(
|
||||
f"TqsdkEngine: 检测到换月信号!从 {self._last_underlying_symbol} 切换到 {self.quote.underlying_symbol}"
|
||||
|
||||
@@ -325,6 +325,7 @@ class TqsdkEngine:
|
||||
print(f"TqsdkEngine: 开始加载历史数据,加载k线数量{self.history_length}")
|
||||
|
||||
self._strategy.trading = False
|
||||
self._strategy.real_trading = True
|
||||
|
||||
is_trading_time = is_futures_trading_time()
|
||||
|
||||
@@ -352,7 +353,7 @@ class TqsdkEngine:
|
||||
|
||||
new_bar = False
|
||||
|
||||
if is_trading_time:
|
||||
if True:
|
||||
if not self.is_checked_rollover:
|
||||
self._check_roll_over()
|
||||
self.is_checked_rollover = True
|
||||
|
||||
Reference in New Issue
Block a user