主力合约回测

This commit is contained in:
2025-06-23 22:21:59 +08:00
parent a81a32ce73
commit afed83f96f
12 changed files with 739 additions and 100713 deletions

View File

@@ -1,6 +1,6 @@
# src/backtest_engine.py
from typing import Type, Dict, Any, List
from typing import Type, Dict, Any, List, Optional
import pandas as pd
# 导入所有需要协调的模块
@@ -8,22 +8,21 @@ from .core_data import Bar, Order, Trade, PortfolioSnapshot
from .data_manager import DataManager
from .execution_simulator import ExecutionSimulator
from .backtest_context import BacktestContext
from .strategies.base_strategy import Strategy # 导入策略基类
from .strategies.base_strategy import Strategy
class BacktestEngine:
"""
回测引擎:协调数据流、策略执行、订单模拟和结果记录。
"""
def __init__(self,
data_manager: DataManager,
strategy_class: Type[Strategy],
strategy_params: Dict[str, Any],
current_segment_symbol: str,
# current_segment_symbol: str, # 这个参数不再需要,因为 symbol 会动态更新
initial_capital: float = 100000.0,
slippage_rate: float = 0.0001,
commission_rate: float = 0.0002):
commission_rate: float = 0.0002,
roll_over_mode: bool = False): # 新增换月模式参数
"""
初始化回测引擎。
@@ -34,6 +33,7 @@ class BacktestEngine:
initial_capital (float): 初始交易资金。
slippage_rate (float): 交易滑点率。
commission_rate (float): 交易佣金率。
roll_over_mode (bool): 是否启用主连合约换月模式。
"""
self.data_manager = data_manager
self.initial_capital = initial_capital
@@ -42,64 +42,110 @@ class BacktestEngine:
slippage_rate=slippage_rate,
commission_rate=commission_rate
)
# 传入引擎自身给 context以便 context 可以获取引擎的状态(如 is_rollover_bar
self.context = BacktestContext(self.data_manager, self.simulator)
self.current_segment_symbol = current_segment_symbol
self.context.set_engine(self) # 建立 Context 到 Engine 的引用
# 实例化策略
self.strategy = strategy_class(self.context, **strategy_params)
# self.current_segment_symbol = current_segment_symbol # 此行移除或作为内部变量动态管理
self.portfolio_snapshots: List[PortfolioSnapshot] = [] # 存储每天的投资组合快照
self.trade_history: List[Trade] = [] # 存储所有成交记录
# 实例化策略。初始 symbol 会在 run_backtest 中根据第一根 Bar 动态设置。
self.strategy = strategy_class(self.context, symbol="INITIAL_PLACEHOLDER_SYMBOL", **strategy_params)
self.portfolio_snapshots: List[PortfolioSnapshot] = []
self.trade_history: List[Trade] = []
self.all_bars: List[Bar] = []
# 历史Bar缓存,用于特征计算
self._history_bars: List[Bar] = []
self._max_history_bars: int = 200 # 例如只保留最近200根Bar的历史数据可根据策略需求调整
self._history_bars: List[Bar] = [] # 引擎层面保留的历史 Bar,通常供策略在 on_bar 中使用
self._max_history_bars: int = strategy_params.get('history_bars_limit', 200)
# 换月相关状态
self.roll_over_mode = roll_over_mode # 是否启用换月模式
self._last_processed_bar_symbol: Optional[str] = None # 记录上一根 K 线的 symbol
self.is_rollover_bar: bool = False # 标记当前 K 线是否为换月 K 线(禁止开仓)
print("\n--- 回测引擎初始化完成 ---")
print(f" 策略: {strategy_class.__name__}")
print(f" 初始资金: {initial_capital:.2f}")
print(f" 换月模式: {'启用' if roll_over_mode else '禁用'}")
def run_backtest(self):
"""
运行整个回测流程。
运行整个回测流程,包含换月逻辑
"""
print("\n--- 回测开始 ---")
# 调用策略的初始化方法
self.strategy.on_init()
last_processed_bar: Optional[Bar] = None # 用于在换月时引用旧合约的最后一根 K 线
# 主回测循环
while True:
current_bar = self.data_manager.get_next_bar()
if current_bar is None:
break # 没有更多数据,回测结束
# 设置当前Bar到Context供策略访问
# --- 换月逻辑判断和处理 (在处理 current_bar 之前进行) ---
# 1. 重置 is_rollover_bar 标记
self.is_rollover_bar = False
# 2. 如果启用换月模式,并且检测到合约 symbol 变化
if current_bar.symbol != self._last_processed_bar_symbol:
print(self.roll_over_mode,
self._last_processed_bar_symbol,
current_bar.symbol, self._last_processed_bar_symbol)
if self.roll_over_mode and \
self._last_processed_bar_symbol is not None and \
current_bar.symbol != self._last_processed_bar_symbol:
old_symbol = self._last_processed_bar_symbol
new_symbol = current_bar.symbol
# 确认 last_processed_bar 确实是旧合约的最后一根 K 线
if last_processed_bar and last_processed_bar.symbol == old_symbol:
self.strategy.log(f"检测到换月!从 [{old_symbol}] 切换到 [{new_symbol}]。"
f"在旧合约最后一根K线 ({last_processed_bar.datetime}) 执行强制平仓和取消操作。")
# A. 强制平仓旧合约的所有持仓
self.simulator.force_close_all_positions_for_symbol(old_symbol, last_processed_bar)
# B. 取消旧合约的所有挂单
self.simulator.cancel_all_pending_orders_for_symbol(old_symbol)
# C. 标记【当前这根 Bar (即新合约的第一根 K 线)】为换月 K 线
# 此时 self.is_rollover_bar 变为 True将通过 Context 传递给策略,
# 策略在该 K 线周期内不能开仓。
self.is_rollover_bar = True
# D. 通知策略换月事件,让策略有机会重置内部状态
self.strategy.on_rollover(old_symbol, new_symbol)
else:
self.strategy.log(f"警告: 检测到换月从 {old_symbol}{new_symbol},但 last_processed_bar 为空或与旧合约不符。"
"强制平仓/取消操作可能未正确执行。")
# 3. 更新策略关注的当前合约 symbol
self.strategy.symbol = current_bar.symbol
# 4. 更新 Context 和 Simulator 的当前 Bar 和时间
self.context.set_current_bar(current_bar)
self.simulator.update_time(current_time=current_bar.datetime)
# 更新历史Bar缓存
# 5. 更新引擎内部的历史 Bar 缓存
self._history_bars.append(current_bar)
if len(self._history_bars) > self._max_history_bars:
self._history_bars.pop(0) # 移除最旧的Bar
self._history_bars.pop(0)
# 1. 计算特征 (使用纯函数)
# 注意: extract_bar_features 接收的是完整的历史数据不包含当前Bar
# 但为了简单起见这里传入的是包含当前bar在内的历史数据但内部函数应确保不使用“未来”数据
# 严格来说,应该传入 self._history_bars[:-1]
# features = extract_bar_features(current_bar, self._history_bars[:-1]) # 传入当前Bar之前的所有历史Bar
# 6. 处理待撮合订单 (在调用策略 on_bar 之前,确保订单在当前 K 线开盘价撮合)
self.simulator.process_pending_orders(current_bar)
# 2. 调用策略的 on_bar 方法
# 7. 调用策略的 on_bar 方法
self.strategy.on_bar(current_bar)
# 3. 记录投资组合快照
# 8. 记录投资组合快照
current_portfolio_value = self.simulator.get_portfolio_value(current_bar)
current_positions = self.simulator.get_current_positions()
# 创建 PortfolioSnapshot记录当前Bar的收盘价
price_at_snapshot = {
current_bar.symbol if hasattr(current_bar, 'symbol') else "DEFAULT_SYMBOL": current_bar.close}
price_at_snapshot = {current_bar.symbol: current_bar.close} # 使用当前 Bar 的收盘价记录快照
snapshot = PortfolioSnapshot(
datetime=current_bar.datetime,
@@ -111,49 +157,36 @@ class BacktestEngine:
self.portfolio_snapshots.append(snapshot)
self.all_bars.append(current_bar)
# 9. 更新 `_last_processed_bar_symbol` 和 `last_processed_bar` 为当前 Bar为下一轮循环做准备
self._last_processed_bar_symbol = current_bar.symbol
last_processed_bar = current_bar
# 记录交易历史(从模拟器获取)
# 简化处理每次获取模拟器中的所有交易历史并更新引擎的trade_history
# 更好的做法是模拟器提供一个方法,返回自上次查询以来的新增交易
# 这里为了不重复添加,可以在 trade_log 中只添加当前 Bar 生成的交易
# 在 on_bar 循环的末尾获取本Bar周期内新产生的交易
# 模拟器在每次send_order成功时会将trade添加到其trade_log
# 这里可以做一个增量获取,或者简单地在循环结束后统一获取
# 目前我们在执行模拟器中已经将成交记录在了 trade_log 中,所以这里不用重复记录,
# 而是等到回测结束后再统一获取。
# 不在此处记录 self.trade_history
print("\n--- 回测片段结束,检查并平仓所有持仓 ---")
if last_processed_bar: # 确保至少有一根Bar被处理过
positions_to_close = self.simulator.get_current_positions()
for symbol_held, quantity in positions_to_close.items():
if quantity != 0:
print(f"[{last_processed_bar.datetime}] 回测结束平仓: 平仓 {symbol_held} ({quantity} 手) @ {last_processed_bar.close:.2f}")
direction = "CLOSE_LONG" if quantity > 0 else "CLOSE_SELL"
volume = abs(quantity)
# 使用当前合约的最后一根Bar的价格进行平仓
# 注意这里假设平仓的symbol_held就是当前segment的symbol
# 如果策略可能同时持有其他旧合约的仓位(多主力同时持有),这里需要更复杂的逻辑来获取正确的平仓价格
# 但在主力合约切换场景下,通常只持有当前主力合约的仓位。
rollover_order = Order(symbol=symbol_held, direction=direction, volume=volume, price_type="MARKET")
self.simulator.send_order(rollover_order, current_bar=last_processed_bar)
# --- 回测结束后的清理工作 ---
print("\n--- 回测结束,检查并平仓所有剩余持仓 ---")
if last_processed_bar: # 确保至少有一根 Bar 被处理过
# 在回测结束时,强制平仓所有可能存在的剩余持仓
# 遍历所有持仓,确保全部清算
remaining_positions_symbols = list(self.simulator.get_current_positions().keys())
for symbol_held in remaining_positions_symbols:
if self.simulator.get_current_positions().get(symbol_held, 0) != 0:
self.strategy.log(f"回测结束清理: 强制平仓合约 {symbol_held} 的剩余持仓。")
# 使用 simulator 的 force_close_all_positions_for_symbol 方法进行清理
self.simulator.force_close_all_positions_for_symbol(symbol_held, last_processed_bar)
self.simulator.cancel_all_pending_orders_for_symbol(symbol_held)
else:
print("没有处理任何Bar无需平仓。")
print("没有处理任何 Bar无需平仓。")
# 回测结束后,获取所有交易记录
self.trade_history = self.simulator.get_trade_history()
print("--- 回测结束 ---")
print(f"总计处理了 {len(self.portfolio_snapshots)} 根K线。")
print(f"总计处理了 {len(self.all_bars)} 根K线。")
print(f"总计发生了 {len(self.trade_history)} 笔交易。")
final_portfolio_value = 0.0
if last_processed_bar:
final_portfolio_value = self.simulator.get_portfolio_value(last_processed_bar)
else: # 如果数据为空,或者回测根本没跑,则净值为初始资金
else:
final_portfolio_value = self.initial_capital
total_return_percentage = ((final_portfolio_value - self.initial_capital) / self.initial_capital) * 100
@@ -168,12 +201,9 @@ class BacktestEngine:
return {
"portfolio_snapshots": self.portfolio_snapshots,
"trade_history": self.trade_history,
"initial_capital": self.simulator.initial_capital, # 或 self.initial_capital
"initial_capital": self.simulator.initial_capital,
"all_bars": self.all_bars
}
def get_simulator(self) -> ExecutionSimulator: # <--- 新增的方法
"""
返回引擎内部的 ExecutionSimulator 实例,以便外部可以访问和修改其状态。
"""
return self.simulator
def get_simulator(self) -> ExecutionSimulator:
return self.simulator