修复回测bug

This commit is contained in:
2025-06-23 23:49:43 +08:00
parent afed83f96f
commit 4521939b95
5 changed files with 9935 additions and 212 deletions

9824
main.ipynb

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,5 @@
# src/backtest_engine.py # src/backtest_engine.py
from datetime import datetime
from typing import Type, Dict, Any, List, Optional from typing import Type, Dict, Any, List, Optional
import pandas as pd import pandas as pd
@@ -22,7 +22,10 @@ class BacktestEngine:
initial_capital: float = 100000.0, initial_capital: float = 100000.0,
slippage_rate: float = 0.0001, slippage_rate: float = 0.0001,
commission_rate: float = 0.0002, commission_rate: float = 0.0002,
roll_over_mode: bool = False): # 新增换月模式参数 roll_over_mode: bool = False,
start_time: Optional[datetime] = None, # 新增开始时间
end_time: Optional[datetime] = None # 新增结束时间
): # 新增换月模式参数
""" """
初始化回测引擎。 初始化回测引擎。
@@ -63,6 +66,10 @@ class BacktestEngine:
self._last_processed_bar_symbol: Optional[str] = None # 记录上一根 K 线的 symbol self._last_processed_bar_symbol: Optional[str] = None # 记录上一根 K 线的 symbol
self.is_rollover_bar: bool = False # 标记当前 K 线是否为换月 K 线(禁止开仓) self.is_rollover_bar: bool = False # 标记当前 K 线是否为换月 K 线(禁止开仓)
# 新增时间过滤属性
self.start_time = start_time
self.end_time = end_time
print("\n--- 回测引擎初始化完成 ---") print("\n--- 回测引擎初始化完成 ---")
print(f" 策略: {strategy_class.__name__}") print(f" 策略: {strategy_class.__name__}")
print(f" 初始资金: {initial_capital:.2f}") print(f" 初始资金: {initial_capital:.2f}")
@@ -85,6 +92,14 @@ class BacktestEngine:
if current_bar is None: if current_bar is None:
break # 没有更多数据,回测结束 break # 没有更多数据,回测结束
if self.start_time and current_bar.datetime < self.start_time:
continue
# 如果设置了结束时间且当前K线在结束时间之后则终止回测
if self.end_time and current_bar.datetime >= self.end_time:
print(f"到达结束时间 {self.end_time},回测终止。")
break
# --- 换月逻辑判断和处理 (在处理 current_bar 之前进行) --- # --- 换月逻辑判断和处理 (在处理 current_bar 之前进行) ---
# 1. 重置 is_rollover_bar 标记 # 1. 重置 is_rollover_bar 标记
self.is_rollover_bar = False self.is_rollover_bar = False

View File

@@ -5,10 +5,12 @@ from typing import Dict, List, Optional
import pandas as pd import pandas as pd
from .core_data import Order, Trade, Bar, PortfolioSnapshot from .core_data import Order, Trade, Bar, PortfolioSnapshot
class ExecutionSimulator: class ExecutionSimulator:
""" """
模拟交易执行和管理账户资金、持仓。 模拟交易执行和管理账户资金、持仓。
""" """
def __init__(self, initial_capital: float, def __init__(self, initial_capital: float,
slippage_rate: float = 0.0001, slippage_rate: float = 0.0001,
commission_rate: float = 0.0002, commission_rate: float = 0.0002,
@@ -140,80 +142,128 @@ class ExecutionSimulator:
if fill_price <= 0: # 未成交或不满足限价条件 if fill_price <= 0: # 未成交或不满足限价条件
return None return None
# --- 以下是订单成功成交逻辑 --- # --- 以下是订单成功成交前的预检查逻辑 ---
trade_value = volume * fill_price trade_value = volume * fill_price
commission = trade_value * self.commission_rate commission = trade_value * self.commission_rate
current_position = self.positions.get(symbol, 0) current_position = self.positions.get(symbol, 0)
current_average_cost = self.average_costs.get(symbol, 0.0) current_average_cost = self.average_costs.get(symbol, 0.0)
realized_pnl = 0.0 realized_pnl = 0.0 # 预先计算的实现盈亏
# 根据 direction 判断开平仓意图 # -----------------------------------------------------------
# 如果 direction 是 CLOSE_LONG 或 CLOSE_SELL (平多), CLOSE_SHORT (平空) 则是平仓交易 # 精确判断 is_open_trade 和 is_close_trade
is_close_trade = order.direction in ["CLOSE_LONG", "CLOSE_SELL", "CLOSE_SHORT"] # -----------------------------------------------------------
# 如果 direction 是 BUY 或 SELL 且不是平仓意图,则是开仓交易 is_trade_a_close_operation = False
is_open_trade = (order.direction in ["BUY", "SELL"]) and (not is_close_trade) is_trade_an_open_operation = False
# 1. 判断是否为平仓操作
# 显式平仓指令
if order.direction in ["CLOSE_LONG", "CLOSE_SELL", "CLOSE_SHORT"]:
is_trade_a_close_operation = True
# 隐式平仓 (例如,持有空头时买入,或持有多头时卖出)
elif order.direction == "BUY" and current_position < 0: # 买入平空
is_trade_a_close_operation = True
elif order.direction == "SELL" and current_position > 0: # 卖出平多
is_trade_a_close_operation = True
# 区分实际的买卖方向 # 2. 判断是否为开仓操作
if order.direction == "BUY":
# 买入开多: 如果当前持有多头或无仓位,或者从空头转为多头
if current_position >= 0 or (current_position < 0 and (current_position + volume) > 0):
is_trade_an_open_operation = True
elif order.direction == "SELL":
# 卖出开空: 如果当前持有空头或无仓位,或者从多头转为空头
if current_position <= 0 or (current_position > 0 and (current_position - volume) < 0):
is_trade_an_open_operation = True
# -----------------------------------------------------------
# 区分实际的买卖方向 (用于资金和持仓计算)
actual_execution_direction = "" actual_execution_direction = ""
if order.direction == "BUY" or order.direction == "CLOSE_SHORT": if order.direction == "BUY" or order.direction == "CLOSE_SHORT":
actual_execution_direction = "BUY" actual_execution_direction = "BUY"
elif order.direction == "SELL" or order.direction == "CLOSE_LONG" or order.direction == "CLOSE_SELL": elif order.direction == "SELL" or order.direction == "CLOSE_LONG" or order.direction == "CLOSE_SELL":
actual_execution_direction = "SELL" actual_execution_direction = "SELL"
else: else:
print(f"[{current_bar.datetime}] 模拟器: 收到未知订单方向 {order.direction} for Order ID: {order.id}. 订单未处理。") print(
f"[{current_bar.datetime}] 模拟器: 收到未知订单方向 {order.direction} for Order ID: {order.id}. 订单未处理。")
if order.id in self.pending_orders: del self.pending_orders[order.id] if order.id in self.pending_orders: del self.pending_orders[order.id]
return None return None
# --- 临时变量,用于预计算新的资金和持仓状态 ---
temp_cash = self.cash
temp_positions = self.positions.copy()
temp_average_costs = self.average_costs.copy()
# 根据实际执行方向进行预计算和资金检查
if actual_execution_direction == "BUY": # 处理实际的买入 (开多 / 平空) if actual_execution_direction == "BUY": # 处理实际的买入 (开多 / 平空)
if current_position >= 0: # 当前持有多仓或无仓位 (开多) if current_position >= 0: # 当前持有多仓或无仓位 (开多)
new_total_cost = (current_average_cost * current_position) + (fill_price * volume) required_cash = trade_value + commission
new_total_volume = current_position + volume if temp_cash < required_cash:
self.average_costs[symbol] = new_total_cost / new_total_volume if new_total_volume > 0 else 0.0 print(
self.positions[symbol] = new_total_volume f"[{current_bar.datetime}] 模拟器: 资金不足 (开多), 无法执行买入 {volume} {symbol} @ {fill_price:.2f}. 需要: {required_cash:.2f}, 当前: {temp_cash:.2f}")
else: # 当前持有空仓 (平空) if order.id in self.pending_orders: del self.pending_orders[order.id]
return None
temp_cash -= required_cash
new_total_cost = (temp_average_costs.get(symbol, 0.0) * temp_positions.get(symbol, 0)) + (
fill_price * volume)
new_total_volume = temp_positions.get(symbol, 0) + volume
temp_average_costs[symbol] = new_total_cost / new_total_volume if new_total_volume > 0 else 0.0
temp_positions[symbol] = new_total_volume
else: # 当前持有空仓 (平空) - 平仓交易,佣金从交易价值中扣除,不单独检查现金余额
pnl_per_share = current_average_cost - fill_price # 空头平仓盈亏 pnl_per_share = current_average_cost - fill_price # 空头平仓盈亏
realized_pnl = pnl_per_share * volume realized_pnl = pnl_per_share * volume
self.positions[symbol] += volume temp_cash -= commission # 扣除佣金
if self.positions[symbol] == 0: temp_cash += trade_value # 回收平仓价值
del self.positions[symbol] temp_cash += realized_pnl # 计入实现盈亏
if symbol in self.average_costs: del self.average_costs[symbol]
elif self.positions[symbol] > 0 and current_position < 0: # 空转多
self.average_costs[symbol] = fill_price # 新多头仓位成本以成交价为准
if self.cash < trade_value + commission: temp_positions[symbol] += volume
print(f"[{current_bar.datetime}] 模拟器: 资金不足,无法执行买入 {volume} {symbol} @ {fill_price:.2f}") if temp_positions[symbol] == 0:
if order.id in self.pending_orders: del self.pending_orders[order.id] del temp_positions[symbol]
return None if symbol in temp_average_costs: del temp_average_costs[symbol]
self.cash -= (trade_value + commission) elif current_position < 0 and temp_positions[symbol] > 0: # 发生空转多
temp_average_costs[symbol] = fill_price # 新多头仓位成本以成交价为准
elif actual_execution_direction == "SELL": # 处理实际的卖出 (开空 / 平多) elif actual_execution_direction == "SELL": # 处理实际的卖出 (开空 / 平多)
if current_position <= 0: # 当前持有空仓或无仓位 (开空) if current_position <= 0: # 当前持有空仓或无仓位 (开空)
new_total_value = (current_average_cost * abs(current_position)) + (fill_price * volume) # 开空主要检查佣金是否足够
new_total_volume = abs(current_position) + volume if temp_cash < commission:
self.average_costs[symbol] = new_total_value / new_total_volume if new_total_volume > 0 else 0.0 print(
self.positions[symbol] -= volume f"[{current_bar.datetime}] 模拟器: 资金不足 (开空佣金), 无法执行卖出 {volume} {symbol} @ {fill_price:.2f}. 佣金: {commission:.2f}, 当前: {temp_cash:.2f}")
else: # 当前持有多仓 (平多) if order.id in self.pending_orders: del self.pending_orders[order.id]
return None
temp_cash -= commission
new_total_value = (temp_average_costs.get(symbol, 0.0) * abs(temp_positions.get(symbol, 0))) + (
fill_price * volume)
new_total_volume = abs(temp_positions.get(symbol, 0)) + volume
temp_average_costs[symbol] = new_total_value / new_total_volume if new_total_volume > 0 else 0.0 # 平均成本
temp_positions[symbol] -= volume
else: # 当前持有多仓 (平多) - 平仓交易,佣金从交易价值中扣除,不单独检查现金余额
pnl_per_share = fill_price - current_average_cost # 多头平仓盈亏 pnl_per_share = fill_price - current_average_cost # 多头平仓盈亏
realized_pnl = pnl_per_share * volume realized_pnl = pnl_per_share * volume
self.positions[symbol] -= volume temp_cash -= commission # 扣除佣金
if self.positions[symbol] == 0: temp_cash += trade_value # 回收平仓价值
del self.positions[symbol] temp_cash += realized_pnl # 计入实现盈亏
if symbol in self.average_costs: del self.average_costs[symbol]
elif self.positions[symbol] < 0 and current_position > 0: # 多转空
self.average_costs[symbol] = fill_price # 新空头仓位成本以成交价为准
if self.cash < commission: # 卖出交易,佣金先扣 temp_positions[symbol] -= volume
print(f"[{current_bar.datetime}] 模拟器: 资金不足(佣金),无法执行卖出 {volume} {symbol} @ {fill_price:.2f}") if temp_positions[symbol] == 0:
if order.id in self.pending_orders: del self.pending_orders[order.id] del temp_positions[symbol]
return None if symbol in temp_average_costs: del temp_average_costs[symbol]
self.cash -= commission elif current_position > 0 and temp_positions[symbol] < 0: # 发生多转空
self.cash += trade_value temp_average_costs[symbol] = fill_price # 新空头仓位成本以成交价为准
# --- 所有检查通过后,才正式更新模拟器状态 ---
self.cash = temp_cash
self.positions = temp_positions
self.average_costs = temp_average_costs
# 创建 Trade 对象时direction 使用原始订单的 direction # 创建 Trade 对象时direction 使用原始订单的 direction
executed_trade = Trade( executed_trade = Trade(
@@ -222,8 +272,8 @@ class ExecutionSimulator:
volume=volume, price=fill_price, commission=commission, volume=volume, price=fill_price, commission=commission,
cash_after_trade=self.cash, positions_after_trade=self.positions.copy(), cash_after_trade=self.cash, positions_after_trade=self.positions.copy(),
realized_pnl=realized_pnl, realized_pnl=realized_pnl,
is_open_trade=is_open_trade, is_open_trade=is_trade_an_open_operation, # 使用更精确的判断
is_close_trade=is_close_trade is_close_trade=is_trade_a_close_operation # 使用更精确的判断
) )
self.trade_log.append(executed_trade) self.trade_log.append(executed_trade)
@@ -232,6 +282,7 @@ class ExecutionSimulator:
del self.pending_orders[order.id] del self.pending_orders[order.id]
return executed_trade return executed_trade
def cancel_order(self, order_id: str) -> bool: def cancel_order(self, order_id: str) -> bool:
""" """
尝试取消一个待处理订单。 尝试取消一个待处理订单。
@@ -258,7 +309,7 @@ class ExecutionSimulator:
volume_to_close = self.positions[symbol_to_close] volume_to_close = self.positions[symbol_to_close]
# 根据持仓方向决定平仓订单的方向 # 根据持仓方向决定平仓订单的方向
direction = "SELL" if volume_to_close > 0 else "BUY" # 多头平仓是卖出,空头平仓是买入 direction = "CLOSE_LONG" if volume_to_close > 0 else "CLOSE_SELL" # 多头平仓是卖出,空头平仓是买入
# 构造一个市价平仓订单 # 构造一个市价平仓订单
rollover_order = Order( rollover_order = Order(

File diff suppressed because one or more lines are too long

View File

@@ -157,6 +157,7 @@ class SimpleLimitBuyStrategy(Strategy):
self.log(f"[{current_datetime}] 开多仓信号 - 当前Open={bar.open:.2f}, " self.log(f"[{current_datetime}] 开多仓信号 - 当前Open={bar.open:.2f}, "
f"前1Range={range_1_ago:.2f}, 前7Range={range_7_ago:.2f}, " f"前1Range={range_1_ago:.2f}, 前7Range={range_7_ago:.2f}, "
f"计算目标买入价={target_buy_price:.2f}") f"计算目标买入价={target_buy_price:.2f}")
self.log(f'{self.context._simulator.get_current_positions()}')
order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}" order_id = f"{self.symbol}_BUY_{bar.datetime.strftime('%Y%m%d%H%M%S')}_{self.order_id_counter}"
self.order_id_counter += 1 self.order_id_counter += 1