实现单品种连续多合约回测

This commit is contained in:
2025-06-19 15:28:26 +08:00
parent ce5ad27bab
commit 355e451aac
6 changed files with 2016 additions and 22 deletions

View File

@@ -160,7 +160,7 @@ def collect_and_save_tqsdk_data_stream(
save_folder = os.path.join(output_dir, safe_symbol)
os.makedirs(save_folder, exist_ok=True)
file_name = f"{safe_symbol}_{freq_folder}_{start_date_str.replace('-', '')}_{end_date_str.replace('-', '')}_{freq}.csv"
file_name = f"{safe_symbol}_{freq}.csv"
file_path = os.path.join(save_folder, file_name)
df.to_csv(file_path, index=True)
@@ -190,10 +190,10 @@ if __name__ == "__main__":
# 示例1: 在回测模式下获取沪深300指数主连的日线数据 (用于历史回测)
# 这种方式适合获取相对较短或中等长度的历史K线数据。
df_if_backtest_daily = collect_and_save_tqsdk_data_stream(
symbol="SHFE.rb2501",
freq="min1",
start_date_str="2024-09-01",
end_date_str="2024-12-01",
symbol="SHFE.rb2410",
freq="min60",
start_date_str="2024-05-01",
end_date_str="2024-09-01",
mode="backtest", # 指定为回测模式
tq_user=TQ_USER_NAME,
tq_pwd=TQ_PASSWORD

View File

@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 7,
"execution_count": 3,
"id": "initial_id",
"metadata": {
"ExecuteTime": {
@@ -19,7 +19,7 @@
"The autoreload extension is already loaded. To reload it, use:\n",
" %reload_ext autoreload\n",
"初始化数据管理器...\n",
"数据加载成功: /mnt/d/PyProject/NewQuant/data/data/SHFE_rb2501/SHFE_rb2501_m60_20240901_20241201_min60.csv\n",
"数据加载成功: /mnt/d/PyProject/NewQuant/data/data/SHFE_rb2501/SHFE_rb2501_min60.csv\n",
"数据范围从 2024-08-30 14:00:00 到 2024-11-29 21:00:00\n",
"总计 404 条记录。\n",
"\n",
@@ -444,9 +444,12 @@
"[2024-11-29 14:00:00] Strategy processing Bar. Current close price: 3318.00. Current Portfolio Value: 99973.05\n",
"[2024-11-29 21:00:00] Strategy processing Bar. Current close price: 3301.00. Current Portfolio Value: 99990.05\n",
"Bar 对象流生成完毕。\n",
"\n",
"--- 回测片段结束,检查并平仓所有持仓 ---\n",
"[2024-11-29 21:00:00] 回测结束平仓: 平仓 SHFE_rb2501 (-1 手) @ 3301.00。\n",
"--- 回测结束 ---\n",
"总计处理了 404 根K线。\n",
"总计发生了 1 笔交易。\n",
"总计发生了 2 笔交易。\n",
"\n",
"回测运行完毕。\n",
"\n",
@@ -462,11 +465,12 @@
"最大回撤 : 0.63%\n",
"夏普比率 : -0.02\n",
"卡玛比率 : -0.04\n",
"总交易次数 : 1\n",
"交易成本 : 0.66\n",
"总交易次数 : 2\n",
"交易成本 : 1.32\n",
"\n",
"--- 部分交易明细 (最近5笔) ---\n",
" 2024-08-30 21:00:00 | SELL | SHFE_rb2501 | Vol: 1 | Price: 3291.70 | Commission: 0.66\n",
" 2024-11-29 21:00:00 | BUY | SHFE_rb2501 | Vol: 1 | Price: 3304.30 | Commission: 0.66\n",
"正在绘制绩效图表...\n"
]
},
@@ -512,7 +516,7 @@
"def main():\n",
" # --- 配置参数 ---\n",
" # 获取当前脚本所在目录,假设数据文件在项目根目录下的 data 文件夹内\n",
" data_file_path = '/mnt/d/PyProject/NewQuant/data/data/SHFE_rb2501/SHFE_rb2501_m60_20240901_20241201_min60.csv'\n",
" data_file_path = '/mnt/d/PyProject/NewQuant/data/data/SHFE_rb2501/SHFE_rb2501_min60.csv'\n",
"\n",
" initial_capital = 100000.0\n",
" slippage_rate = 0.001 # 假设每笔交易0.1%的滑点\n",
@@ -536,6 +540,7 @@
" engine = BacktestEngine(\n",
" data_manager=data_manager,\n",
" strategy_class=SimpleLimitBuyStrategy,\n",
" current_segment_symbol='SHFE_rb2501',\n",
" strategy_params=strategy_parameters,\n",
" initial_capital=initial_capital,\n",
" slippage_rate=slippage_rate,\n",
@@ -582,7 +587,7 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": 4,
"id": "9dd93e564f0e2b55",
"metadata": {
"ExecuteTime": {
@@ -597,7 +602,7 @@
"'/home/liaozhaorun/.fonts/simhei.ttf'"
]
},
"execution_count": 8,
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
@@ -611,7 +616,7 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": 5,
"id": "a14196c49af33461",
"metadata": {
"ExecuteTime": {

1942
main_multi.ipynb Normal file

File diff suppressed because one or more lines are too long

View File

@@ -20,6 +20,7 @@ class BacktestEngine:
data_manager: DataManager,
strategy_class: Type[Strategy],
strategy_params: Dict[str, Any],
current_segment_symbol: str,
initial_capital: float = 100000.0,
slippage_rate: float = 0.0001,
commission_rate: float = 0.0002):
@@ -41,6 +42,7 @@ class BacktestEngine:
commission_rate=commission_rate
)
self.context = BacktestContext(self.data_manager, self.simulator)
self.current_segment_symbol = current_segment_symbol
# 实例化策略
self.strategy = strategy_class(self.context, **strategy_params)
@@ -107,6 +109,8 @@ class BacktestEngine:
self.portfolio_snapshots.append(snapshot)
self.all_bars.append(current_bar)
last_processed_bar = current_bar
# 记录交易历史(从模拟器获取)
# 简化处理每次获取模拟器中的所有交易历史并更新引擎的trade_history
# 更好的做法是模拟器提供一个方法,返回自上次查询以来的新增交易
@@ -117,7 +121,25 @@ class BacktestEngine:
# 这里可以做一个增量获取,或者简单地在循环结束后统一获取
# 目前我们在执行模拟器中已经将成交记录在了 trade_log 中,所以这里不用重复记录,
# 而是等到回测结束后再统一获取。
pass # 不在此处记录 self.trade_history
# 不在此处记录 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 = "SELL" if quantity > 0 else "BUY"
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)
else:
print("没有处理任何Bar无需平仓。")
# 回测结束后,获取所有交易记录
self.trade_history = self.simulator.get_trade_history()
@@ -135,4 +157,10 @@ class BacktestEngine:
"trade_history": self.trade_history,
"initial_capital": self.simulator.initial_capital, # 或 self.initial_capital
"all_bars": self.all_bars
}
}
def get_simulator(self) -> ExecutionSimulator: # <--- 新增的方法
"""
返回引擎内部的 ExecutionSimulator 实例,以便外部可以访问和修改其状态。
"""
return self.simulator

View File

@@ -236,7 +236,7 @@ class ExecutionSimulator:
float: 当前的投资组合总价值。
"""
total_value = self.cash
# 在单品种场景下,我们假设 self.positions 最多只包含一个品种
# 并且这个品种就是 current_bar.symbol 所代表的品种
symbol_in_position = list(self.positions.keys())[0] if self.positions else None
@@ -246,7 +246,7 @@ class ExecutionSimulator:
# 持仓市值 = 数量 * 当前市场价格 (current_bar.close)
# 无论多头(quantity > 0)还是空头(quantity < 0),这个计算都是正确的
total_value += quantity * current_bar.close
# 您也可以选择在这里打印调试信息
# print(f" DEBUG Portfolio Value Calculation: Cash={self.cash:.2f}, "
# f"Position for {symbol_in_position}: {quantity} @ {current_bar.close:.2f}, "
@@ -254,7 +254,7 @@ class ExecutionSimulator:
# 如果没有持仓或者持仓品种与当前Bar品种不符 (理论上单品种不会发生)
# 那么 total_value 依然是 self.cash
return total_value
def get_current_positions(self) -> Dict[str, int]:
@@ -268,3 +268,22 @@ class ExecutionSimulator:
返回所有成交记录的副本。
"""
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 = []

View File

@@ -101,10 +101,10 @@ class SimpleLimitBuyStrategy(Strategy):
order = Order(
id=order_id,
symbol=self.symbol,
direction="SELL",
direction="BUY",
volume=trade_volume,
price_type="LIMIT",
limit_price=limit_price,
price_type="MARKET",
# limit_price=limit_price,
submitted_time=bar.datetime
)