更新策略邮件推送

This commit is contained in:
2025-12-16 00:36:36 +08:00
parent 548ea34238
commit 3648f9c790
25 changed files with 10229 additions and 2519 deletions

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View 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)

File diff suppressed because one or more lines are too long

View 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)

File diff suppressed because one or more lines are too long

View 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)

View 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 循环

View 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 循环

View File

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

View File

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

File diff suppressed because one or more lines are too long

View 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)

File diff suppressed because one or more lines are too long

View 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

View File

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

View File

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

View File

@@ -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.3tick_size=1 -> math.floor(100.3) = 100
# 价格100.8tick_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.3tick_size=1 -> math.ceil(100.3) = 101
# 价格100.8tick_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:

View File

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

View File

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