新增实盘策略:ITrendStrategy(SA)
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,160 @@
|
||||
import numpy as np
|
||||
import talib
|
||||
from typing import Optional, Any, List, Dict
|
||||
from src.core_data import Bar, Order
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class KalmanMeanReversion(Strategy):
|
||||
"""
|
||||
改进版卡尔曼均值回归策略
|
||||
1. 以 Slow Line 为止盈目标 (真正的均值回归)
|
||||
2. 严格的 ATR 止损 (防止趋势爆发)
|
||||
3. 时间衰减退出机制
|
||||
4. 趋势斜率过滤
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
fast_sensitivity: float = 0.05, # 快线灵敏度
|
||||
slow_sensitivity: float = 0.01, # 慢线灵敏度 (更平滑作为均值)
|
||||
lookback_variance: int = 60,
|
||||
atr_period: int = 20,
|
||||
entry_threshold: float = 1.2, # 偏离多少个ATR入场
|
||||
stop_loss_multiplier: float = 2.0, # 止损倍数
|
||||
max_hold_bars: int = 30, # 最大持仓时间
|
||||
indicator: Any = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
self.fast_sensitivity = fast_sensitivity
|
||||
self.slow_sensitivity = slow_sensitivity
|
||||
self.lookback_variance = lookback_variance
|
||||
self.atr_period = atr_period
|
||||
self.entry_threshold = entry_threshold
|
||||
self.stop_loss_multiplier = stop_loss_multiplier
|
||||
self.max_hold_bars = max_hold_bars
|
||||
|
||||
self.indicator = indicator
|
||||
|
||||
# 状态变量
|
||||
self.kf_fast = {'x': 0.0, 'P': 1.0}
|
||||
self.kf_slow = {'x': 0.0, 'P': 1.0}
|
||||
self.kalman_initialized = False
|
||||
|
||||
# 记录持仓信息
|
||||
self.entry_price = 0.0
|
||||
self.entry_bar_count = 0
|
||||
self.order_id_counter = 0
|
||||
|
||||
def _update_kalman(self, state: dict, measurement: float, Q: float, R: float) -> float:
|
||||
"""递归卡尔曼滤波更新"""
|
||||
p_minus = state['P'] + Q
|
||||
k_gain = p_minus / (p_minus + R)
|
||||
state['x'] = state['x'] + k_gain * (measurement - state['x'])
|
||||
state['P'] = (1 - k_gain) * p_minus
|
||||
return state['x']
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
bar_history = self.get_bar_history()
|
||||
if len(bar_history) < 100:
|
||||
return
|
||||
|
||||
self.cancel_all_pending_orders()
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
last_price = closes[-1]
|
||||
|
||||
# 1. 计算自适应噪声 (基于滚动方差)
|
||||
rolling_var = np.var(closes[-self.lookback_variance:])
|
||||
r_base = rolling_var if rolling_var > 0 else 1.0
|
||||
|
||||
# 2. 初始化或更新卡尔曼滤波器
|
||||
if not self.kalman_initialized:
|
||||
self.kf_fast['x'] = self.kf_slow['x'] = last_price
|
||||
self.kalman_initialized = True
|
||||
return
|
||||
|
||||
# 快线追踪价格,慢线代表平衡位置
|
||||
fast_line = self._update_kalman(self.kf_fast, last_price, r_base * self.fast_sensitivity, r_base)
|
||||
slow_line = self._update_kalman(self.kf_slow, last_price, r_base * self.slow_sensitivity, r_base * 10.0)
|
||||
|
||||
# 3. 计算 ATR 和 偏离度
|
||||
highs = np.array([b.high for b in bar_history], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history], dtype=float)
|
||||
atr = talib.ATR(highs, lows, closes, self.atr_period)[-1]
|
||||
|
||||
if atr <= 0: return
|
||||
|
||||
# 计算价格偏离慢线的程度 (Z-Score 的变体)
|
||||
diff_in_atr = (last_price - slow_line) / atr
|
||||
|
||||
# 4. 仓位逻辑
|
||||
pos = self.get_current_positions().get(symbol, 0)
|
||||
|
||||
if pos == 0:
|
||||
# --- 入场逻辑 ---
|
||||
# 只有在外部指标允许且偏离度足够大时入场
|
||||
can_trade = self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())
|
||||
|
||||
if can_trade:
|
||||
if diff_in_atr > self.entry_threshold:
|
||||
# 超买,做空
|
||||
self.execute_order(symbol, "SELL", "OPEN", last_price)
|
||||
elif diff_in_atr < -self.entry_threshold:
|
||||
# 超卖,做多
|
||||
self.execute_order(symbol, "BUY", "OPEN", last_price)
|
||||
else:
|
||||
# --- 出场逻辑 ---
|
||||
self.entry_bar_count += 1
|
||||
is_long = pos > 0
|
||||
should_close = False
|
||||
exit_reason = ""
|
||||
|
||||
# A. 均值回归止盈:触碰或穿过慢线
|
||||
if is_long and last_price >= slow_line:
|
||||
should_close = True
|
||||
exit_reason = "Take Profit: Reached Mean"
|
||||
elif not is_long and last_price <= slow_line:
|
||||
should_close = True
|
||||
exit_reason = "Take Profit: Reached Mean"
|
||||
|
||||
# B. 严谨止损:背离程度进一步扩大
|
||||
elif is_long and last_price < self.entry_price - self.stop_loss_multiplier * atr:
|
||||
should_close = True
|
||||
exit_reason = "Stop Loss: Deviation Too Large"
|
||||
elif not is_long and last_price > self.entry_price + self.stop_loss_multiplier * atr:
|
||||
should_close = True
|
||||
exit_reason = "Stop Loss: Deviation Too Large"
|
||||
|
||||
# C. 时间退出:久攻不下,由于均值回归的时效性,超时即走
|
||||
elif self.entry_bar_count >= self.max_hold_bars:
|
||||
should_close = True
|
||||
exit_reason = "Time Exit: Holding Too Long"
|
||||
|
||||
if should_close:
|
||||
direction = "CLOSE_LONG" if is_long else "CLOSE_SHORT"
|
||||
self.log(f"EXIT {direction}: Price={last_price}, Reason={exit_reason}")
|
||||
self.execute_order(symbol, direction, "CLOSE", last_price)
|
||||
|
||||
def execute_order(self, symbol, direction, offset, price):
|
||||
"""执行订单并更新状态"""
|
||||
if offset == "OPEN":
|
||||
self.entry_price = price
|
||||
self.entry_bar_count = 0
|
||||
|
||||
order_id = f"{symbol}_{direction}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
|
||||
order = Order(
|
||||
id=order_id,
|
||||
symbol=symbol,
|
||||
direction=direction,
|
||||
volume=self.trade_volume,
|
||||
price_type="MARKET",
|
||||
offset=offset
|
||||
)
|
||||
self.send_order(order)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,190 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import talib
|
||||
from collections import deque
|
||||
from typing import Optional, Any, List, Dict
|
||||
from src.core_data import Bar, Order
|
||||
from src.indicators.base_indicators import Indicator
|
||||
from src.strategies.base_strategy import Strategy
|
||||
import numpy as np
|
||||
import talib
|
||||
from typing import Optional, Any, List, Dict
|
||||
from src.core_data import Bar, Order
|
||||
from src.strategies.base_strategy import Strategy
|
||||
|
||||
|
||||
class KalmanTrendFollower(Strategy):
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Any,
|
||||
main_symbol: str,
|
||||
enable_log: bool,
|
||||
trade_volume: int,
|
||||
# --- 显著降低灵敏度,过滤日内杂波 ---
|
||||
fast_sensitivity: float = 0.05, # 调低:让快线也变得稳重
|
||||
slow_sensitivity: float = 0.005, # 极低:慢线只代表大趋势方向
|
||||
lookback_variance: int = 60, # 增加窗口,计算更稳定的市场噪声
|
||||
# --- 趋势跟踪参数 ---
|
||||
atr_period: int = 23,
|
||||
entry_threshold: float = 0.5, # 差值需超过0.5倍ATR才考虑入场
|
||||
trailing_stop_multiplier: float = 4.0, # 关键:4倍ATR跟踪止损,给趋势留足空间
|
||||
structural_stop_multiplier: float = 1.0, # 价格破位慢线多少ATR才出场
|
||||
indicator: Indicator = None,
|
||||
):
|
||||
super().__init__(context, main_symbol, enable_log)
|
||||
self.trade_volume = trade_volume
|
||||
|
||||
# 参数初始化
|
||||
self.fast_sensitivity = fast_sensitivity
|
||||
self.slow_sensitivity = slow_sensitivity
|
||||
self.lookback_variance = lookback_variance
|
||||
self.atr_period = atr_period
|
||||
self.entry_threshold = entry_threshold
|
||||
self.trailing_stop_multiplier = trailing_stop_multiplier
|
||||
self.structural_stop_multiplier = structural_stop_multiplier
|
||||
self.indicator = indicator
|
||||
|
||||
# 状态变量
|
||||
self.kf_fast = {'x': 0.0, 'P': 1.0}
|
||||
self.kf_slow = {'x': 0.0, 'P': 1.0}
|
||||
self.kalman_initialized = False
|
||||
|
||||
self.position_meta: Dict[str, Any] = {}
|
||||
self.order_id_counter = 0
|
||||
|
||||
|
||||
def _update_kalman(self, state: dict, measurement: float, Q: float, R: float) -> float:
|
||||
p_minus = state['P'] + Q
|
||||
k_gain = p_minus / (p_minus + R)
|
||||
state['x'] = state['x'] + k_gain * (measurement - state['x'])
|
||||
state['P'] = (1 - k_gain) * p_minus
|
||||
return state['x']
|
||||
|
||||
|
||||
def on_open_bar(self, open_price: float, symbol: str):
|
||||
bar_history = self.get_bar_history()
|
||||
if len(bar_history) < 100: return
|
||||
|
||||
self.cancel_all_pending_orders()
|
||||
|
||||
closes = np.array([b.close for b in bar_history], dtype=float)
|
||||
last_price = closes[-1]
|
||||
|
||||
# 1. 动态计算卡尔曼参数
|
||||
# 增加基础噪声 R,使曲线更平滑
|
||||
rolling_var = np.var(closes[-self.lookback_variance:])
|
||||
r_base = rolling_var if rolling_var > 0 else 1.0
|
||||
|
||||
# 计算快慢线
|
||||
if not self.kalman_initialized:
|
||||
self.kf_fast['x'] = self.kf_slow['x'] = last_price
|
||||
self.kalman_initialized = True
|
||||
return
|
||||
|
||||
fast_line = self._update_kalman(self.kf_fast, last_price, r_base * self.fast_sensitivity, r_base)
|
||||
slow_line = self._update_kalman(self.kf_slow, last_price, r_base * self.slow_sensitivity, r_base * 5.0)
|
||||
|
||||
# 2. 计算 ATR 和 趋势指标
|
||||
highs = np.array([b.high for b in bar_history], dtype=float)
|
||||
lows = np.array([b.low for b in bar_history], dtype=float)
|
||||
atr = talib.ATR(highs, lows, closes, self.atr_period)[-1]
|
||||
|
||||
diff = fast_line - slow_line
|
||||
diff_in_atr = diff / atr if atr > 0 else 0
|
||||
|
||||
# 3. 仓位管理逻辑
|
||||
pos = self.get_current_positions().get(symbol, 0)
|
||||
|
||||
if pos == 0 and (self.indicator is None or self.indicator.is_condition_met(*self.get_indicator_tuple())):
|
||||
# --- 入场逻辑:必须形成明显的发散 ---
|
||||
if diff_in_atr > self.entry_threshold:
|
||||
self.open_trade(symbol, "BUY", open_price, atr, slow_line)
|
||||
elif diff_in_atr < -self.entry_threshold:
|
||||
self.open_trade(symbol, "SELL", open_price, atr, slow_line)
|
||||
else:
|
||||
# --- 出场逻辑:保护肥尾收益 ---
|
||||
self.manage_exit(symbol, pos, last_price, atr, slow_line)
|
||||
|
||||
|
||||
def open_trade(self, symbol, direction, price, atr, slow_line):
|
||||
# 记录入场时的最高/最低价,用于动态跟踪止损
|
||||
meta = {
|
||||
'entry_price': price,
|
||||
'extreme_price': price, # 记录持仓期间到达过的最高(多头)或最低(空头)
|
||||
'direction': direction,
|
||||
'initial_atr': atr
|
||||
}
|
||||
self.send_limit_order(symbol, direction, price, self.trade_volume, "OPEN", meta)
|
||||
self.log(f"TREND ENTRY {direction}: Price={price}, ATR={atr:.2f}")
|
||||
|
||||
|
||||
def manage_exit(self, symbol, pos, price, atr, slow_line):
|
||||
meta = self.position_meta.get(symbol)
|
||||
if not meta: return
|
||||
|
||||
is_long = pos > 0
|
||||
should_close = False
|
||||
|
||||
# 更新持仓期间的极端价格(用于计算吊灯止损)
|
||||
if is_long:
|
||||
meta['extreme_price'] = max(meta['extreme_price'], price)
|
||||
# 吊灯止损位:最高价回落 N 倍 ATR
|
||||
chandelier_stop = meta['extreme_price'] - self.trailing_stop_multiplier * atr
|
||||
# 结构止损位:跌破慢速趋势线一定距离
|
||||
structural_stop = slow_line - self.structural_stop_multiplier * atr
|
||||
|
||||
# 综合取较严的价格作为保护,但不轻易离场
|
||||
if price < max(chandelier_stop, structural_stop):
|
||||
should_close = True
|
||||
exit_reason = "Trailing/Structural Break"
|
||||
else:
|
||||
meta['extreme_price'] = min(meta['extreme_price'], price)
|
||||
chandelier_stop = meta['extreme_price'] + self.trailing_stop_multiplier * atr
|
||||
structural_stop = slow_line + self.structural_stop_multiplier * atr
|
||||
|
||||
if price > min(chandelier_stop, structural_stop):
|
||||
should_close = True
|
||||
exit_reason = "Trailing/Structural Break"
|
||||
|
||||
if should_close:
|
||||
direction = "CLOSE_LONG" if is_long else "CLOSE_SHORT"
|
||||
self.log(f"EXIT {direction}: Price={price}, Reason={exit_reason}")
|
||||
self.close_position(symbol, direction, abs(pos))
|
||||
|
||||
|
||||
# (底层 send_market_order / close_position 同前,注意更新 state 时保留 meta['extreme_price'])
|
||||
|
||||
# --- 底层封装 ---
|
||||
def send_market_order(self, symbol, direction, volume, offset, meta=None):
|
||||
if offset == "OPEN": self.position_meta[symbol] = meta
|
||||
|
||||
order_id = f"{symbol}_{direction}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(id=order_id, symbol=symbol, direction=direction,
|
||||
volume=volume, price_type="MARKET", offset=offset)
|
||||
self.send_order(order)
|
||||
self.save_state(self.position_meta)
|
||||
|
||||
|
||||
def send_limit_order(self, symbol, direction, price, volume, offset, meta=None):
|
||||
if offset == "OPEN": self.position_meta[symbol] = meta
|
||||
|
||||
order_id = f"{symbol}_{direction}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(id=order_id, symbol=symbol, direction=direction,
|
||||
volume=volume, price_type="LIMIT", offset=offset, limit_price=price)
|
||||
self.send_order(order)
|
||||
self.save_state(self.position_meta)
|
||||
|
||||
|
||||
def close_position(self, symbol, direction, volume):
|
||||
order_id = f"{symbol}_{direction}_{self.order_id_counter}"
|
||||
self.order_id_counter += 1
|
||||
order = Order(id=order_id, symbol=symbol, direction=direction,
|
||||
volume=volume, price_type="MARKET", offset="CLOSE")
|
||||
|
||||
self.send_order(order)
|
||||
self.position_meta.pop(symbol, None)
|
||||
self.save_state(self.position_meta)
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
118
futures_trading_strategies/hc/KalmanTrendFollower/utils.py
Normal file
118
futures_trading_strategies/hc/KalmanTrendFollower/utils.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import multiprocessing
|
||||
from typing import Tuple, Dict, Any, Optional
|
||||
|
||||
from src.analysis.result_analyzer import ResultAnalyzer
|
||||
from src.backtest_engine import BacktestEngine
|
||||
from src.data_manager import DataManager
|
||||
|
||||
|
||||
# --- 单个回测任务函数 ---
|
||||
# 这个函数将在每个独立的进程中运行,因此它必须是自包含的
|
||||
def run_single_backtest(
|
||||
combination: Tuple[float, float], # 传入当前参数组合
|
||||
common_config: Dict[str, Any] # 传入公共配置 (如数据路径, 初始资金等)
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
运行单个参数组合的回测任务。
|
||||
此函数将在一个独立的进程中执行。
|
||||
"""
|
||||
p1_value, p2_value = combination
|
||||
|
||||
# 从 common_config 中获取必要的配置
|
||||
symbol = common_config['symbol']
|
||||
data_path = common_config['data_path']
|
||||
initial_capital = common_config['initial_capital']
|
||||
slippage_rate = common_config['slippage_rate']
|
||||
commission_rate = common_config['commission_rate']
|
||||
start_time = common_config['start_time']
|
||||
end_time = common_config['end_time']
|
||||
roll_over_mode = common_config['roll_over_mode']
|
||||
# bar_duration_seconds = common_config['bar_duration_seconds'] # 如果DataManager需要,可以再传
|
||||
param1_name = common_config['param1_name']
|
||||
param2_name = common_config['param2_name']
|
||||
|
||||
# 每个进程内部独立初始化 DataManager 和 BacktestEngine
|
||||
# 确保每个进程有自己的数据副本和模拟状态
|
||||
data_manager = DataManager(
|
||||
file_path=data_path,
|
||||
symbol=symbol,
|
||||
# bar_duration_seconds=bar_duration_seconds, # 如果DataManager需要,根据数据文件路径推断或者额外参数传入
|
||||
# start_date=start_time.date(), # DataManager 现在通过 file_path 和 symbol 处理数据
|
||||
# end_date=end_time.date(),
|
||||
)
|
||||
# data_manager.load_data() # DataManager 内部加载数据
|
||||
|
||||
strategy_parameters = {
|
||||
'main_symbol': common_config['main_symbol'],
|
||||
'trade_volume': 1,
|
||||
param1_name: p1_value, # 15分钟扫荡K线下影线占其总范围的最小比例。
|
||||
param2_name: p2_value, # 15分钟限价单的入场点位于扫荡K线低点到收盘价的斐波那契回撤比例。
|
||||
# 'order_direction': common_config['order_direction'],
|
||||
'enable_log': False, # 建议在调试和测试时开启日志
|
||||
}
|
||||
|
||||
if 'order_direction' in common_config:
|
||||
strategy_parameters['order_direction'] = common_config['order_direction']
|
||||
|
||||
if 'strategy_mode' in common_config:
|
||||
strategy_parameters['strategy_mode'] = common_config['strategy_mode']
|
||||
|
||||
if 'kalman_measurement_noise' in common_config:
|
||||
strategy_parameters['kalman_measurement_noise'] = common_config['kalman_measurement_noise']
|
||||
|
||||
# if 'entry_threshold_atr' in common_config and 'entry_threshold_atr' not in strategy_parameters:
|
||||
# strategy_parameters['entry_threshold_atr'] = common_config['entry_threshold_atr']
|
||||
# elif 'entry_threshold_atr' not in strategy_parameters:
|
||||
# strategy_parameters['entry_threshold_atr'] = strategy_parameters['structural_stop_atr_multiplier']
|
||||
|
||||
# 打印当前进程正在处理的组合信息
|
||||
# 注意:多进程打印会交错显示
|
||||
# print(f"--- 正在运行组合: {strategy_parameters} (PID: {multiprocessing.current_process().pid}) ---")
|
||||
|
||||
try:
|
||||
# 初始化回测引擎
|
||||
engine = BacktestEngine(
|
||||
data_manager=data_manager,
|
||||
strategy_class=common_config['strategy'],
|
||||
strategy_params=strategy_parameters,
|
||||
initial_capital=initial_capital,
|
||||
slippage_rate=slippage_rate,
|
||||
commission_rate=commission_rate,
|
||||
roll_over_mode=True, # 保持换月模式
|
||||
start_time=common_config['start_time'],
|
||||
end_time=common_config['end_time']
|
||||
)
|
||||
# 运行回测,传入时间范围
|
||||
engine.run_backtest()
|
||||
|
||||
# 获取回测结果并分析
|
||||
results = engine.get_backtest_results()
|
||||
portfolio_snapshots = results["portfolio_snapshots"]
|
||||
trade_history = results["trade_history"]
|
||||
bars = results["all_bars"]
|
||||
initial_capital_result = results["initial_capital"]
|
||||
|
||||
if portfolio_snapshots:
|
||||
analyzer = ResultAnalyzer(portfolio_snapshots, trade_history, bars, initial_capital_result)
|
||||
|
||||
# analyzer.generate_report()
|
||||
# analyzer.plot_performance()
|
||||
metrics = analyzer.calculate_all_metrics()
|
||||
|
||||
# 将当前组合的参数和性能指标存储起来
|
||||
result_entry = {**strategy_parameters, **metrics}
|
||||
return result_entry
|
||||
else:
|
||||
print(
|
||||
f" 组合 {strategy_parameters} 没有生成投资组合快照,无法进行结果分析。(PID: {multiprocessing.current_process().pid})")
|
||||
# 返回一个包含参数和默认0值的结果,以便追踪失败组合
|
||||
return {**strategy_parameters, "total_return": 0.0, "annualized_return": 0.0, "sharpe_ratio": 0.0,
|
||||
"max_drawdown": 0.0, "error": "No portfolio snapshots"}
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_trace = traceback.format_exc()
|
||||
print(
|
||||
f" 组合 {strategy_parameters} 运行失败: {e}\n{error_trace} (PID: {multiprocessing.current_process().pid})")
|
||||
# 返回错误信息,以便后续处理
|
||||
return {**strategy_parameters, "error": str(e), "traceback": error_trace}
|
||||
|
||||
Reference in New Issue
Block a user