主力合约回测
This commit is contained in:
@@ -1,42 +1,31 @@
|
||||
# src/execution_simulator.py (修改部分)
|
||||
# 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.average_costs[symbol] = 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
|
||||
self.trade_log: List[Trade] = []
|
||||
self.pending_orders: Dict[str, Order] = {}
|
||||
self._current_time: Optional[datetime] = None
|
||||
|
||||
print(
|
||||
f"模拟器初始化:初始资金={self.initial_capital:.2f}, 滑点率={self.slippage_rate}, 佣金率={self.commission_rate}")
|
||||
@@ -44,20 +33,11 @@ class ExecutionSimulator:
|
||||
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:
|
||||
# 可以在这里抛出错误或者返回一个默认值,取决于你对未初始化时间的处理
|
||||
# 抛出错误可以帮助你发现问题,例如在模拟器时间未设置时就尝试获取
|
||||
# 改进:如果时间未设置,可以抛出错误,防止策略在 on_init 阶段意外调用
|
||||
# raise RuntimeError("Simulator time has not been set. Ensure update_time is called.")
|
||||
return None
|
||||
return self._current_time
|
||||
@@ -65,126 +45,100 @@ class ExecutionSimulator:
|
||||
def _calculate_fill_price(self, order: Order, current_bar: Bar) -> float:
|
||||
"""
|
||||
内部方法:根据订单类型和滑点计算实际成交价格。
|
||||
- 市价单通常以当前K线的开盘价成交(考虑滑点)。
|
||||
- 限价单判断是否触及限价,如果触及,以限价成交(考虑滑点)。
|
||||
撮合逻辑:所有订单(市价/限价)都以当前K线的 **开盘价 (open)** 为基准进行撮合。
|
||||
"""
|
||||
fill_price = -1.0 # 默认未成交
|
||||
|
||||
base_price = current_bar.open # 所有成交都以当前K线的开盘价为基准
|
||||
|
||||
if order.price_type == "MARKET":
|
||||
# 市价单通常以开盘价成交,或者根据你的策略需求选择收盘价
|
||||
# 这里我们仍然使用开盘价作为市价单的基准成交价
|
||||
base_price = current_bar.open
|
||||
|
||||
if order.direction == "BUY" or order.direction == "CLOSE_SHORT": # 买入或平空,价格向上偏离
|
||||
# 市价单:直接以开盘价成交,考虑滑点
|
||||
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": # 卖出或平多,价格向下偏离
|
||||
elif order.direction == "SELL" or order.direction == "CLOSE_LONG": # 卖出/平多:向下偏离(少收)
|
||||
fill_price = base_price * (1 - self.slippage_rate)
|
||||
else: # 默认情况,理论上不应该到这里,因为方向应该明确
|
||||
fill_price = base_price # 不考虑滑点
|
||||
|
||||
# 市价单只要有价格就会成交,无需额外价格区间判断
|
||||
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 order.direction == "BUY" or order.direction == "CLOSE_SHORT": # 限价买入/平空
|
||||
# 买单只有当开盘价低于或等于限价时才可能成交
|
||||
# 即:我愿意出 limit_price 买,开盘价 open_price 更低或一样,当然买
|
||||
if base_price <= limit_price:
|
||||
fill_price = base_price * (1 + self.slippage_rate)
|
||||
# else: 未满足限价条件,不成交
|
||||
elif order.direction == "SELL" or order.direction == "CLOSE_LONG": # 限价卖出/平多
|
||||
# 卖单只有当开盘价高于或等于限价时才可能成交
|
||||
# 即:我愿意出 limit_price 卖,开盘价 open_price 更高或一样,当然卖
|
||||
if base_price >= limit_price:
|
||||
fill_price = base_price * (1 - self.slippage_rate)
|
||||
# else: 未满足限价条件,不成交
|
||||
|
||||
# 最终检查成交价是否有效且合理(大于0)
|
||||
if fill_price <= 0:
|
||||
return -1.0 # 如果计算出来价格无效,返回未成交
|
||||
return -1.0 # 未成交或价格无效
|
||||
|
||||
return fill_price
|
||||
|
||||
def send_order(self, order: Order, current_bar: Bar) -> Optional[Trade]:
|
||||
def send_order_to_pending(self, order: Order) -> Optional[Order]:
|
||||
"""
|
||||
接收策略发出的订单,并模拟执行。
|
||||
如果订单未立即成交,则加入待处理订单列表。
|
||||
特殊处理:如果 order.direction 是 "CANCEL",则调用 cancel_order。
|
||||
将订单添加到待处理队列。由 BacktestEngine 或 Strategy 调用。
|
||||
此方法不进行撮合,撮合由 process_pending_orders 统一处理。
|
||||
"""
|
||||
if order.id in self.pending_orders:
|
||||
# print(f"订单 {order.id} 已经存在于待处理队列。")
|
||||
return None
|
||||
self.pending_orders[order.id] = order
|
||||
# print(f"订单 {order.id} 加入待处理队列。")
|
||||
return order
|
||||
|
||||
def process_pending_orders(self, current_bar: Bar):
|
||||
"""
|
||||
处理所有待撮合的订单。在每个K线数据到来时调用。
|
||||
"""
|
||||
# 复制一份待处理订单的键,防止在迭代时修改字典
|
||||
order_ids_to_process = list(self.pending_orders.keys())
|
||||
|
||||
for order_id in order_ids_to_process:
|
||||
if order_id not in self.pending_orders: # 订单可能已被取消
|
||||
continue
|
||||
|
||||
Args:
|
||||
order (Order): 待执行的订单对象。
|
||||
current_bar (Bar): 当前的Bar数据,用于确定成交价格。
|
||||
order = self.pending_orders[order_id]
|
||||
|
||||
# 只有当订单的symbol与当前bar的symbol一致时才尝试撮合
|
||||
# 这样确保了在换月后,旧合约的挂单不会被尝试撮合 (尽管换月时会强制取消)
|
||||
if order.symbol != current_bar.symbol:
|
||||
# 这种情况理论上应该被换月逻辑清理掉的旧合约挂单,
|
||||
# 如果因为某种原因漏掉了,这里直接跳过,避免异常。
|
||||
continue
|
||||
|
||||
Returns:
|
||||
Optional[Trade]: 如果订单成功执行则返回 Trade 对象,否则返回 None。
|
||||
# 尝试成交订单
|
||||
self._execute_single_order(order, current_bar)
|
||||
|
||||
def _execute_single_order(self, order: Order, current_bar: Bar) -> Optional[Trade]:
|
||||
"""
|
||||
内部方法:尝试执行单个订单,并处理资金和持仓变化。
|
||||
由 send_order 或 process_pending_orders 调用。
|
||||
"""
|
||||
# --- 处理撤单指令 ---
|
||||
if order.direction == "CANCEL":
|
||||
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
|
||||
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
|
||||
if fill_price <= 0: # 未成交或不满足限价条件
|
||||
return None
|
||||
|
||||
# --- 以下是订单成功成交的逻辑 ---
|
||||
trade_value = volume * fill_price
|
||||
@@ -193,195 +147,215 @@ class ExecutionSimulator:
|
||||
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"
|
||||
realized_pnl = 0.0
|
||||
|
||||
# 根据 direction 判断开平仓意图
|
||||
# 如果 direction 是 CLOSE_LONG 或 CLOSE_SELL (平多), CLOSE_SHORT (平空) 则是平仓交易
|
||||
is_close_trade = order.direction in ["CLOSE_LONG", "CLOSE_SELL", "CLOSE_SHORT"]
|
||||
# 如果 direction 是 BUY 或 SELL 且不是平仓意图,则是开仓交易
|
||||
is_open_trade = (order.direction in ["BUY", "SELL"]) and (not is_close_trade)
|
||||
|
||||
is_close_order_intent = (order.direction == "CLOSE_LONG" or
|
||||
order.direction == "CLOSE_SHORT")
|
||||
|
||||
if actual_direction == "BUY": # 处理买入 (开多 / 平空)
|
||||
# 开多仓或平空仓
|
||||
# 区分实际的买卖方向
|
||||
actual_execution_direction = ""
|
||||
if order.direction == "BUY" or order.direction == "CLOSE_SHORT":
|
||||
actual_execution_direction = "BUY"
|
||||
elif order.direction == "SELL" or order.direction == "CLOSE_LONG" or order.direction == "CLOSE_SELL":
|
||||
actual_execution_direction = "SELL"
|
||||
else:
|
||||
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]
|
||||
return None
|
||||
|
||||
if actual_execution_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
|
||||
pnl_per_share = current_average_cost - fill_price # 空头平仓盈亏
|
||||
realized_pnl = pnl_per_share * volume
|
||||
|
||||
# 更新持仓和成本
|
||||
self.positions[symbol] += volume
|
||||
if self.positions[symbol] == 0: # 如果全部平仓
|
||||
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
|
||||
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}")
|
||||
if self.cash < trade_value + commission:
|
||||
print(f"[{current_bar.datetime}] 模拟器: 资金不足,无法执行买入 {volume} {symbol} @ {fill_price:.2f}")
|
||||
if order.id in self.pending_orders: del self.pending_orders[order.id]
|
||||
return None
|
||||
self.cash -= (trade_value + commission)
|
||||
|
||||
|
||||
elif actual_direction == "SELL": # 处理卖出 (开空 / 平多)
|
||||
# 开空仓或平多仓
|
||||
elif actual_execution_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 # 空头数量增加,持仓量变为负更多
|
||||
|
||||
self.positions[symbol] -= volume
|
||||
else: # 当前持有多仓 (平多)
|
||||
is_close_trade = is_close_order_intent # 这是平仓交易
|
||||
# 计算平多盈亏
|
||||
pnl_per_share = fill_price - current_average_cost
|
||||
pnl_per_share = fill_price - current_average_cost # 多头平仓盈亏
|
||||
realized_pnl = pnl_per_share * volume
|
||||
|
||||
# 更新持仓和成本
|
||||
self.positions[symbol] -= volume
|
||||
if self.positions[symbol] == 0: # 如果全部平仓
|
||||
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}")
|
||||
elif self.positions[symbol] < 0 and current_position > 0: # 多转空
|
||||
self.average_costs[symbol] = fill_price # 新空头仓位成本以成交价为准
|
||||
|
||||
if self.cash < commission: # 卖出交易,佣金先扣
|
||||
print(f"[{current_bar.datetime}] 模拟器: 资金不足(佣金),无法执行卖出 {volume} {symbol} @ {fill_price:.2f}")
|
||||
if order.id in self.pending_orders: del self.pending_orders[order.id]
|
||||
return None
|
||||
else: # 既不是 BUY 也不是 SELL,且也不是 CANCEL。这可能是未知的 direction
|
||||
print(
|
||||
f"[{current_bar.datetime}] 模拟器: 收到未知订单方向 {order.direction} for Order ID: {order.id}. 订单未处理。")
|
||||
return None
|
||||
|
||||
# 创建 Trade 对象
|
||||
self.cash -= commission
|
||||
self.cash += trade_value
|
||||
|
||||
# 创建 Trade 对象时,direction 使用原始订单的 direction
|
||||
executed_trade = Trade(
|
||||
order_id=order.id, fill_time=current_bar.datetime, symbol=symbol,
|
||||
direction=order.direction, # 记录原始订单方向 (BUY/SELL/CLOSE_X)
|
||||
direction=order.direction, # 使用原始订单的 direction
|
||||
volume=volume, price=fill_price, commission=commission,
|
||||
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_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 force_close_all_positions_for_symbol(self, symbol_to_close: str, closing_bar: Bar) -> List[Trade]:
|
||||
"""
|
||||
强制平仓指定合约的所有持仓。
|
||||
Args:
|
||||
symbol_to_close (str): 需要平仓的合约代码。
|
||||
closing_bar (Bar): 用于获取平仓价格的当前K线数据(通常是旧合约的最后一根K线)。
|
||||
Returns:
|
||||
List[Trade]: 因强制平仓而产生的交易记录。
|
||||
"""
|
||||
closed_trades: List[Trade] = []
|
||||
|
||||
# 仅处理指定symbol的持仓
|
||||
if symbol_to_close in self.positions and self.positions[symbol_to_close] != 0:
|
||||
volume_to_close = self.positions[symbol_to_close]
|
||||
|
||||
# 根据持仓方向决定平仓订单的方向
|
||||
direction = "SELL" if volume_to_close > 0 else "BUY" # 多头平仓是卖出,空头平仓是买入
|
||||
|
||||
# 构造一个市价平仓订单
|
||||
rollover_order = Order(
|
||||
id=f"FORCE_CLOSE_{symbol_to_close}_{closing_bar.datetime.strftime('%Y%m%d%H%M%S%f')}",
|
||||
symbol=symbol_to_close,
|
||||
direction=direction,
|
||||
volume=abs(volume_to_close),
|
||||
price_type="MARKET",
|
||||
limit_price=None,
|
||||
submitted_time=closing_bar.datetime,
|
||||
)
|
||||
|
||||
# 使用内部的执行逻辑进行撮合
|
||||
trade = self._execute_single_order(rollover_order, closing_bar)
|
||||
if trade:
|
||||
closed_trades.append(trade)
|
||||
else:
|
||||
print(f"[{closing_bar.datetime}] 警告: 强制平仓 {symbol_to_close} 失败!")
|
||||
|
||||
return closed_trades
|
||||
|
||||
# --- 新增:取消指定合约的所有挂单 ---
|
||||
def cancel_all_pending_orders_for_symbol(self, symbol_to_cancel: str) -> int:
|
||||
"""
|
||||
取消指定合约的所有待处理订单。
|
||||
"""
|
||||
cancelled_count = 0
|
||||
order_ids_to_cancel = [
|
||||
order_id for order_id, order in self.pending_orders.items()
|
||||
if order.symbol == symbol_to_cancel
|
||||
]
|
||||
for order_id in order_ids_to_cancel:
|
||||
if self.cancel_order(order_id): # 调用现有的 cancel_order 方法
|
||||
cancelled_count += 1
|
||||
return cancelled_count
|
||||
|
||||
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数据,用于计算持仓市值。
|
||||
current_bar (Bar): 当前的Bar数据,用于计算**当前活跃合约**的持仓市值。
|
||||
注意:如果 simulator 中持有多个合约,这里需要更复杂的逻辑。
|
||||
目前假设主力合约回测时,simulator.positions 主要只包含当前主力合约。
|
||||
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
|
||||
# 遍历所有持仓,计算市值。
|
||||
# 注意:这里假设 current_bar 提供了当前活跃主力合约的价格。
|
||||
# 如果 self.positions 中包含其他非 current_bar.symbol 的旧合约,
|
||||
# 它们的市值将无法用 current_bar.open 来准确计算。
|
||||
# 在换月模式下,旧合约会被强制平仓,因此 simulator.positions 通常只包含一个合约。
|
||||
for symbol, quantity in self.positions.items():
|
||||
# 这里简单处理:如果持仓合约与 current_bar.symbol 相同,则使用 current_bar.open 计算。
|
||||
# 如果是其他合约,则需要外部提供其最新价格,但这超出了本函数当前的能力范围。
|
||||
# 考虑到换月模式,旧合约会被平仓,所以大部分时候这不会是问题。
|
||||
if symbol == current_bar.symbol:
|
||||
total_value += quantity * current_bar.open
|
||||
else:
|
||||
# 警告:如果这里出现,说明有未平仓的旧合约持仓,且没有其最新价格来计算市值。
|
||||
# 在严谨的主力连续回测中,这不应该发生,因为换月会强制平仓。
|
||||
print(f"[{current_bar.datetime}] 警告:持仓中存在非当前K线合约 {symbol},无法准确计算其市值。")
|
||||
# 可以选择将这部分持仓价值计为0,或者使用上一个已知价格(需要额外数据结构)
|
||||
# 这里我们假设它不影响总价值计算,因为换月时会处理掉
|
||||
pass
|
||||
|
||||
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 = {}
|
||||
self.average_costs = {}
|
||||
for symbol, qty in self.positions.items(): # 重置平均成本
|
||||
self.average_costs[symbol] = 0.0
|
||||
self.trade_log = []
|
||||
self.pending_orders = {} # 清空挂单
|
||||
self._current_time = None
|
||||
|
||||
def clear_trade_history(self) -> None:
|
||||
"""
|
||||
清空当前模拟器的交易历史。
|
||||
在每个合约片段结束时调用,以便我们只收集当前片段的交易记录。
|
||||
"""
|
||||
print("ExecutionSimulator: 清空交易历史。")
|
||||
self.trade_history = []
|
||||
# Removed clear_trade_history as trade_log is cleared in reset
|
||||
|
||||
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
|
||||
return None
|
||||
Reference in New Issue
Block a user