Files
NewQuant/src/execution_simulator.py

388 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# src/execution_simulator.py (修改部分)
from datetime import datetime
from typing import Dict, List, Optional
import pandas as pd
from .core_data import Order, Trade, Bar, PortfolioSnapshot
class ExecutionSimulator:
"""
模拟交易执行和管理账户资金、持仓。
"""
def __init__(self, initial_capital: float,
slippage_rate: float = 0.0001,
commission_rate: float = 0.0002,
initial_positions: Optional[Dict[str, int]] = None):
"""
Args:
initial_capital (float): 初始资金。
slippage_rate (float): 滑点率(相对于成交价格的百分比)。
commission_rate (float): 佣金率(相对于成交金额的百分比)。
initial_positions (Optional[Dict[str, int]]): 初始持仓,格式为 {symbol: quantity}。
"""
self.initial_capital = initial_capital
self.cash = initial_capital
self.positions: Dict[str, int] = initial_positions if initial_positions is not None else {}
# 新增:跟踪持仓的平均成本 {symbol: average_cost}
self.average_costs: Dict[str, float] = {}
# 如果有初始持仓需要设置初始成本简化为0或在外部配置
if initial_positions:
for symbol, qty in initial_positions.items():
# 初始持仓成本,如果需要精确,应该从外部传入
self.average_costs[symbol] = 0.0 # 简化处理初始持仓成本为0
self.slippage_rate = slippage_rate
self.commission_rate = commission_rate
self.trade_log: List[Trade] = [] # 存储所有成交记录
self.pending_orders: Dict[str, Order] = {} # {order_id: Order_object}
self._current_time = None
print(
f"模拟器初始化:初始资金={self.initial_capital:.2f}, 滑点率={self.slippage_rate}, 佣金率={self.commission_rate}")
if self.positions:
print(f"初始持仓:{self.positions}")
def update_time(self, current_time: datetime):
"""
更新模拟器的当前时间。
这个方法由 BacktestEngine 在遍历 K 线时调用。
"""
self._current_time = current_time
# --- 新增的公共方法 ---
def get_current_time(self) -> datetime:
"""
获取模拟器的当前时间。
"""
if self._current_time is None:
# 可以在这里抛出错误或者返回一个默认值,取决于你对未初始化时间的处理
# 抛出错误可以帮助你发现问题,例如在模拟器时间未设置时就尝试获取
# raise RuntimeError("Simulator time has not been set. Ensure update_time is called.")
return None
return self._current_time
def _calculate_fill_price(self, order: Order, current_bar: Bar) -> float:
"""
内部方法:根据订单类型和滑点计算实际成交价格。
- 市价单通常以当前K线的开盘价成交考虑滑点
- 限价单判断是否触及限价,如果触及,以限价成交(考虑滑点)。
"""
fill_price = -1.0 # 默认未成交
if order.price_type == "MARKET":
# 市价单通常以开盘价成交,或者根据你的策略需求选择收盘价
# 这里我们仍然使用开盘价作为市价单的基准成交价
base_price = current_bar.open
if order.direction == "BUY" or order.direction == "CLOSE_SHORT": # 买入或平空,价格向上偏离
fill_price = base_price * (1 + self.slippage_rate)
elif order.direction == "SELL" or order.direction == "CLOSE_LONG": # 卖出或平多,价格向下偏离
fill_price = base_price * (1 - self.slippage_rate)
else: # 默认情况,理论上不应该到这里,因为方向应该明确
fill_price = base_price # 不考虑滑点
# 市价单只要有价格就会成交,无需额外价格区间判断
elif order.price_type == "LIMIT" and order.limit_price is not None:
limit_price = order.limit_price
high = current_bar.high
low = current_bar.low
open_price = current_bar.open # 也可以在限价单成交时用开盘价作为实际成交价,或限价本身
if order.direction == "BUY" or order.direction == "CLOSE_SHORT": # 限价买入或限价平空
# 买入如果K线最低价 <= 限价,则限价单可能成交
if low <= limit_price:
# 成交价通常是限价,但考虑滑点后可能略高(对买方不利)
# 或者可以以 open/low 之间的一个价格成交,这里简化为限价+滑点
fill_price_candidate = limit_price * (1 + self.slippage_rate)
# 确保成交价不会比当前K线的最低价还低如果不是在最低点成交
# 也可以简单就返回 limit_price * (1 + self.slippage_rate)
# 如果开盘价低于或等于限价,那么通常会以开盘价成交(或者略差)
# 否则,如果价格是从上方跌落到限价区,那么会在限价附近成交
# 这里简化如果K线触及限价则以限价成交考虑滑点
# 更精细的模拟会考虑价格穿越顺序 (例如,是否开盘就跳过了限价)
# 如果开盘价已经低于或等于限价,那么就以开盘价成交(考虑滑点)
if open_price <= limit_price:
fill_price = open_price * (1 + self.slippage_rate)
else: # 价格从高处跌落到限价
fill_price = limit_price * (1 + self.slippage_rate)
# 确保成交价不会超过限价 (虽然加滑点可能会略超,但这是交易成本的一部分)
# 这个检查是为了避免逻辑错误,理论上加滑点后应该可以接受比限价略高的价格
# 如果你严格要求成交价不能高于限价,则需要移除滑点或者更复杂的逻辑
# 这里我们接受加滑点后的价格
if fill_price > high and fill_price > limit_price: # 如果计算出来的成交价高于K线最高价则可能不合理
fill_price = -1.0 # 理论上不该发生,除非滑点过大
else:
return -1.0 # 未触及限价
elif order.direction == "SELL" or order.direction == "CLOSE_LONG": # 限价卖出或限价平多
# 卖出如果K线最高价 >= 限价,则限价单可能成交
if high >= limit_price:
# 成交价通常是限价,但考虑滑点后可能略低(对卖方不利)
fill_price_candidate = limit_price * (1 - self.slippage_rate)
# 如果开盘价已经高于或等于限价,那么就以开盘价成交(考虑滑点)
if open_price >= limit_price:
fill_price = open_price * (1 - self.slippage_rate)
else: # 价格从低处上涨到限价
fill_price = limit_price * (1 - self.slippage_rate)
# 确保成交价不会低于限价
if fill_price < low and fill_price < limit_price: # 如果计算出来的成交价低于K线最低价则可能不合理
fill_price = -1.0 # 理论上不该发生
else:
return -1.0 # 未触及限价
# 最后检查成交价是否有效
if fill_price <= 0:
return -1.0 # 如果计算出来价格无效,返回未成交
return fill_price
def send_order(self, order: Order, current_bar: Bar) -> Optional[Trade]:
"""
接收策略发出的订单,并模拟执行。
如果订单未立即成交,则加入待处理订单列表。
特殊处理:如果 order.direction 是 "CANCEL",则调用 cancel_order。
Args:
order (Order): 待执行的订单对象。
current_bar (Bar): 当前的Bar数据用于确定成交价格。
Returns:
Optional[Trade]: 如果订单成功执行则返回 Trade 对象,否则返回 None。
"""
# --- 处理撤单指令 ---
if order.direction == "CANCEL":
success = self.cancel_order(order.id)
if success:
# print(f"[{current_bar.datetime}] 模拟器: 收到并成功处理撤单指令 for Order ID: {order.id}")
pass
else:
# print(f"[{current_bar.datetime}] 模拟器: 收到撤单指令 for Order ID: {order.id}, 但订单已成交或不存在。")
pass
return None # 撤单操作不返回Trade
# --- 正常买卖订单处理 ---
symbol = order.symbol
volume = order.volume
# 尝试计算成交价格
fill_price = self._calculate_fill_price(order, current_bar)
executed_trade: Optional[Trade] = None
realized_pnl = 0.0 # 初始化实现盈亏
is_open_trade = False
is_close_trade = False
if fill_price <= 0: # 表示未成交或不满足限价条件
if order.price_type == "LIMIT":
self.pending_orders[order.id] = order
# print(f'撮合失败,order id:{order.id},fill_price:{fill_price}')
return None # 未成交返回None
# --- 以下是订单成功成交的逻辑 ---
trade_value = volume * fill_price
commission = trade_value * self.commission_rate
current_position = self.positions.get(symbol, 0)
current_average_cost = self.average_costs.get(symbol, 0.0)
actual_direction = order.direction
if order.direction == "CLOSE_SHORT":
actual_direction = "BUY"
elif order.direction == "CLOSE_LONG":
actual_direction = "SELL"
is_close_order_intent = (order.direction == "CLOSE_LONG" or
order.direction == "CLOSE_SHORT")
if actual_direction == "BUY": # 处理买入 (开多 / 平空)
# 开多仓或平空仓
if current_position >= 0: # 当前持有多仓或无仓位 (开多)
is_open_trade = not is_close_order_intent # 如果是平仓意图,则不是开仓交易
# 更新平均成本 (加权平均)
new_total_cost = (current_average_cost * current_position) + (fill_price * volume)
new_total_volume = current_position + volume
self.average_costs[symbol] = new_total_cost / new_total_volume if new_total_volume > 0 else 0.0
self.positions[symbol] = new_total_volume
else: # 当前持有空仓 (平空)
is_close_trade = is_close_order_intent # 这是平仓交易
# 计算平空盈亏
pnl_per_share = current_average_cost - fill_price
realized_pnl = pnl_per_share * volume
# 更新持仓和成本
self.positions[symbol] += volume
if self.positions[symbol] == 0: # 如果全部平仓
del self.positions[symbol]
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:
self.cash -= (trade_value + commission)
else:
print(f"[{current_bar.datetime}] 资金不足,无法执行买入 {volume} {symbol}")
return None
elif actual_direction == "SELL": # 处理卖出 (开空 / 平多)
# 开空仓或平多仓
if current_position <= 0: # 当前持有空仓或无仓位 (开空)
is_open_trade = not is_close_order_intent # 如果是平仓意图,则不是开仓交易
# 更新平均成本 (空头成本为负值)
# 对于空头,平均成本通常是指你卖出开仓的平均价格
# 这里需要根据你的空头成本计算方式来调整
# 常见的做法是:总卖出价值 / 总卖出数量
new_total_value = (current_average_cost * abs(current_position)) + (fill_price * volume)
new_total_volume = abs(current_position) + volume
self.average_costs[symbol] = new_total_value / new_total_volume if new_total_volume > 0 else 0.0
self.positions[symbol] -= volume # 空头数量增加,持仓量变为负更多
else: # 当前持有多仓 (平多)
is_close_trade = is_close_order_intent # 这是平仓交易
# 计算平多盈亏
pnl_per_share = fill_price - current_average_cost
realized_pnl = pnl_per_share * volume
# 更新持仓和成本
self.positions[symbol] -= volume
if self.positions[symbol] == 0: # 如果全部平仓
del self.positions[symbol]
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:
self.cash -= commission
self.cash += trade_value
else:
print(f"[{current_bar.datetime}] 资金不足,无法执行卖出 {volume} {symbol}")
return None
else: # 既不是 BUY 也不是 SELL且也不是 CANCEL。这可能是未知的 direction
print(
f"[{current_bar.datetime}] 模拟器: 收到未知订单方向 {order.direction} for Order ID: {order.id}. 订单未处理。")
return None
# 创建 Trade 对象
executed_trade = Trade(
order_id=order.id, fill_time=current_bar.datetime, symbol=symbol,
direction=order.direction, # 记录原始订单方向 (BUY/SELL/CLOSE_X)
volume=volume, price=fill_price, commission=commission,
cash_after_trade=self.cash, positions_after_trade=self.positions.copy(),
realized_pnl=realized_pnl, # 填充实现盈亏
is_open_trade=is_open_trade,
is_close_trade=is_close_trade
)
self.trade_log.append(executed_trade)
# 如果订单成交,无论它是市价单还是限价单,都从待处理订单中移除
if order.id in self.pending_orders:
del self.pending_orders[order.id]
return executed_trade
def cancel_order(self, order_id: str) -> bool:
"""
尝试取消一个待处理订单。
Args:
order_id (str): 要取消的订单ID。
Returns:
bool: 如果成功取消则返回 True否则返回 False例如订单不存在或已成交
"""
if order_id in self.pending_orders:
# print(f"订单 {order_id} 已成功取消。")
del self.pending_orders[order_id]
return True
# print(f"订单 {order_id} 不存在或已成交,无法取消。")
return False
def get_pending_orders(self) -> Dict[str, Order]:
"""
获取当前所有待处理订单的副本。
"""
return self.pending_orders.copy()
def get_portfolio_value(self, current_bar: Bar) -> float:
"""
计算当前的投资组合总价值(包括现金和持仓市值)。
Args:
current_bar (Bar): 当前的Bar数据用于计算持仓市值。
Returns:
float: 当前的投资组合总价值。
"""
total_value = self.cash
# 在单品种场景下,我们假设 self.positions 最多只包含一个品种
# 并且这个品种就是 current_bar.symbol 所代表的品种
symbol_in_position = list(self.positions.keys())[0] if self.positions else None
if symbol_in_position and symbol_in_position == current_bar.symbol:
quantity = self.positions[symbol_in_position]
# 持仓市值 = 数量 * 当前市场价格 (current_bar.close)
# 无论多头(quantity > 0)还是空头(quantity < 0),这个计算都是正确的
total_value += quantity * current_bar.open
# 您也可以选择在这里打印调试信息
# print(f" DEBUG Portfolio Value Calculation: Cash={self.cash:.2f}, "
# f"Position for {symbol_in_position}: {quantity} @ {current_bar.close:.2f}, "
# f"Position Value={quantity * current_bar.close:.2f}, Total Value={total_value:.2f}")
# 如果没有持仓或者持仓品种与当前Bar品种不符 (理论上单品种不会发生)
# 那么 total_value 依然是 self.cash
return total_value
def get_current_positions(self) -> Dict[str, int]:
"""
返回当前持仓字典的副本。
"""
return self.positions.copy()
def get_trade_history(self) -> List[Trade]:
"""
返回所有成交记录的副本。
"""
return self.trade_log.copy()
def reset(self, new_initial_capital: float = None, new_initial_positions: Dict[str, int] = None) -> None:
"""
重置模拟器状态到新的初始条件。
可以在总回测开始时调用,或在合约切换时调整资金和持仓。
"""
print("ExecutionSimulator: 重置状态。")
self.cash = new_initial_capital if new_initial_capital is not None else self.initial_capital
self.positions = new_initial_positions.copy() if new_initial_positions is not None else {}
self.trade_history = []
self.current_orders = {}
def clear_trade_history(self) -> None:
"""
清空当前模拟器的交易历史。
在每个合约片段结束时调用,以便我们只收集当前片段的交易记录。
"""
print("ExecutionSimulator: 清空交易历史。")
self.trade_history = []
def get_average_position_price(self, symbol: str) -> Optional[float]:
"""
获取指定合约的平均持仓成本。
如果无持仓或无该合约记录,返回 None。
"""
# 返回 average_costs 字典中对应 symbol 的值
# 如果没有持仓或者没有记录,返回 None
if symbol in self.positions and self.positions[symbol] != 0:
return self.average_costs.get(symbol)
return None