实现单品种连续多合约回测
This commit is contained in:
@@ -160,7 +160,7 @@ def collect_and_save_tqsdk_data_stream(
|
|||||||
save_folder = os.path.join(output_dir, safe_symbol)
|
save_folder = os.path.join(output_dir, safe_symbol)
|
||||||
os.makedirs(save_folder, exist_ok=True)
|
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)
|
file_path = os.path.join(save_folder, file_name)
|
||||||
|
|
||||||
df.to_csv(file_path, index=True)
|
df.to_csv(file_path, index=True)
|
||||||
@@ -190,10 +190,10 @@ if __name__ == "__main__":
|
|||||||
# 示例1: 在回测模式下获取沪深300指数主连的日线数据 (用于历史回测)
|
# 示例1: 在回测模式下获取沪深300指数主连的日线数据 (用于历史回测)
|
||||||
# 这种方式适合获取相对较短或中等长度的历史K线数据。
|
# 这种方式适合获取相对较短或中等长度的历史K线数据。
|
||||||
df_if_backtest_daily = collect_and_save_tqsdk_data_stream(
|
df_if_backtest_daily = collect_and_save_tqsdk_data_stream(
|
||||||
symbol="SHFE.rb2501",
|
symbol="SHFE.rb2410",
|
||||||
freq="min1",
|
freq="min60",
|
||||||
start_date_str="2024-09-01",
|
start_date_str="2024-05-01",
|
||||||
end_date_str="2024-12-01",
|
end_date_str="2024-09-01",
|
||||||
mode="backtest", # 指定为回测模式
|
mode="backtest", # 指定为回测模式
|
||||||
tq_user=TQ_USER_NAME,
|
tq_user=TQ_USER_NAME,
|
||||||
tq_pwd=TQ_PASSWORD
|
tq_pwd=TQ_PASSWORD
|
||||||
|
|||||||
23
main.ipynb
23
main.ipynb
@@ -2,7 +2,7 @@
|
|||||||
"cells": [
|
"cells": [
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 7,
|
"execution_count": 3,
|
||||||
"id": "initial_id",
|
"id": "initial_id",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"The autoreload extension is already loaded. To reload it, use:\n",
|
"The autoreload extension is already loaded. To reload it, use:\n",
|
||||||
" %reload_ext autoreload\n",
|
" %reload_ext autoreload\n",
|
||||||
"初始化数据管理器...\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",
|
"数据范围从 2024-08-30 14:00:00 到 2024-11-29 21:00:00\n",
|
||||||
"总计 404 条记录。\n",
|
"总计 404 条记录。\n",
|
||||||
"\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 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",
|
"[2024-11-29 21:00:00] Strategy processing Bar. Current close price: 3301.00. Current Portfolio Value: 99990.05\n",
|
||||||
"Bar 对象流生成完毕。\n",
|
"Bar 对象流生成完毕。\n",
|
||||||
|
"\n",
|
||||||
|
"--- 回测片段结束,检查并平仓所有持仓 ---\n",
|
||||||
|
"[2024-11-29 21:00:00] 回测结束平仓: 平仓 SHFE_rb2501 (-1 手) @ 3301.00。\n",
|
||||||
"--- 回测结束 ---\n",
|
"--- 回测结束 ---\n",
|
||||||
"总计处理了 404 根K线。\n",
|
"总计处理了 404 根K线。\n",
|
||||||
"总计发生了 1 笔交易。\n",
|
"总计发生了 2 笔交易。\n",
|
||||||
"\n",
|
"\n",
|
||||||
"回测运行完毕。\n",
|
"回测运行完毕。\n",
|
||||||
"\n",
|
"\n",
|
||||||
@@ -462,11 +465,12 @@
|
|||||||
"最大回撤 : 0.63%\n",
|
"最大回撤 : 0.63%\n",
|
||||||
"夏普比率 : -0.02\n",
|
"夏普比率 : -0.02\n",
|
||||||
"卡玛比率 : -0.04\n",
|
"卡玛比率 : -0.04\n",
|
||||||
"总交易次数 : 1\n",
|
"总交易次数 : 2\n",
|
||||||
"交易成本 : 0.66\n",
|
"交易成本 : 1.32\n",
|
||||||
"\n",
|
"\n",
|
||||||
"--- 部分交易明细 (最近5笔) ---\n",
|
"--- 部分交易明细 (最近5笔) ---\n",
|
||||||
" 2024-08-30 21:00:00 | SELL | SHFE_rb2501 | Vol: 1 | Price: 3291.70 | Commission: 0.66\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"
|
"正在绘制绩效图表...\n"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -512,7 +516,7 @@
|
|||||||
"def main():\n",
|
"def main():\n",
|
||||||
" # --- 配置参数 ---\n",
|
" # --- 配置参数 ---\n",
|
||||||
" # 获取当前脚本所在目录,假设数据文件在项目根目录下的 data 文件夹内\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",
|
"\n",
|
||||||
" initial_capital = 100000.0\n",
|
" initial_capital = 100000.0\n",
|
||||||
" slippage_rate = 0.001 # 假设每笔交易0.1%的滑点\n",
|
" slippage_rate = 0.001 # 假设每笔交易0.1%的滑点\n",
|
||||||
@@ -536,6 +540,7 @@
|
|||||||
" engine = BacktestEngine(\n",
|
" engine = BacktestEngine(\n",
|
||||||
" data_manager=data_manager,\n",
|
" data_manager=data_manager,\n",
|
||||||
" strategy_class=SimpleLimitBuyStrategy,\n",
|
" strategy_class=SimpleLimitBuyStrategy,\n",
|
||||||
|
" current_segment_symbol='SHFE_rb2501',\n",
|
||||||
" strategy_params=strategy_parameters,\n",
|
" strategy_params=strategy_parameters,\n",
|
||||||
" initial_capital=initial_capital,\n",
|
" initial_capital=initial_capital,\n",
|
||||||
" slippage_rate=slippage_rate,\n",
|
" slippage_rate=slippage_rate,\n",
|
||||||
@@ -582,7 +587,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 8,
|
"execution_count": 4,
|
||||||
"id": "9dd93e564f0e2b55",
|
"id": "9dd93e564f0e2b55",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
@@ -597,7 +602,7 @@
|
|||||||
"'/home/liaozhaorun/.fonts/simhei.ttf'"
|
"'/home/liaozhaorun/.fonts/simhei.ttf'"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"execution_count": 8,
|
"execution_count": 4,
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"output_type": "execute_result"
|
"output_type": "execute_result"
|
||||||
}
|
}
|
||||||
@@ -611,7 +616,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": 9,
|
"execution_count": 5,
|
||||||
"id": "a14196c49af33461",
|
"id": "a14196c49af33461",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"ExecuteTime": {
|
"ExecuteTime": {
|
||||||
|
|||||||
1942
main_multi.ipynb
Normal file
1942
main_multi.ipynb
Normal file
File diff suppressed because one or more lines are too long
@@ -20,6 +20,7 @@ class BacktestEngine:
|
|||||||
data_manager: DataManager,
|
data_manager: DataManager,
|
||||||
strategy_class: Type[Strategy],
|
strategy_class: Type[Strategy],
|
||||||
strategy_params: Dict[str, Any],
|
strategy_params: Dict[str, Any],
|
||||||
|
current_segment_symbol: str,
|
||||||
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):
|
||||||
@@ -41,6 +42,7 @@ class BacktestEngine:
|
|||||||
commission_rate=commission_rate
|
commission_rate=commission_rate
|
||||||
)
|
)
|
||||||
self.context = BacktestContext(self.data_manager, self.simulator)
|
self.context = BacktestContext(self.data_manager, self.simulator)
|
||||||
|
self.current_segment_symbol = current_segment_symbol
|
||||||
|
|
||||||
# 实例化策略
|
# 实例化策略
|
||||||
self.strategy = strategy_class(self.context, **strategy_params)
|
self.strategy = strategy_class(self.context, **strategy_params)
|
||||||
@@ -107,6 +109,8 @@ class BacktestEngine:
|
|||||||
self.portfolio_snapshots.append(snapshot)
|
self.portfolio_snapshots.append(snapshot)
|
||||||
self.all_bars.append(current_bar)
|
self.all_bars.append(current_bar)
|
||||||
|
|
||||||
|
last_processed_bar = current_bar
|
||||||
|
|
||||||
# 记录交易历史(从模拟器获取)
|
# 记录交易历史(从模拟器获取)
|
||||||
# 简化处理:每次获取模拟器中的所有交易历史,并更新引擎的trade_history
|
# 简化处理:每次获取模拟器中的所有交易历史,并更新引擎的trade_history
|
||||||
# 更好的做法是模拟器提供一个方法,返回自上次查询以来的新增交易
|
# 更好的做法是模拟器提供一个方法,返回自上次查询以来的新增交易
|
||||||
@@ -117,7 +121,25 @@ class BacktestEngine:
|
|||||||
# 这里可以做一个增量获取,或者简单地在循环结束后统一获取
|
# 这里可以做一个增量获取,或者简单地在循环结束后统一获取
|
||||||
# 目前我们在执行模拟器中已经将成交记录在了 trade_log 中,所以这里不用重复记录,
|
# 目前我们在执行模拟器中已经将成交记录在了 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()
|
self.trade_history = self.simulator.get_trade_history()
|
||||||
@@ -135,4 +157,10 @@ class BacktestEngine:
|
|||||||
"trade_history": self.trade_history,
|
"trade_history": self.trade_history,
|
||||||
"initial_capital": self.simulator.initial_capital, # 或 self.initial_capital
|
"initial_capital": self.simulator.initial_capital, # 或 self.initial_capital
|
||||||
"all_bars": self.all_bars
|
"all_bars": self.all_bars
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_simulator(self) -> ExecutionSimulator: # <--- 新增的方法
|
||||||
|
"""
|
||||||
|
返回引擎内部的 ExecutionSimulator 实例,以便外部可以访问和修改其状态。
|
||||||
|
"""
|
||||||
|
return self.simulator
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ class ExecutionSimulator:
|
|||||||
float: 当前的投资组合总价值。
|
float: 当前的投资组合总价值。
|
||||||
"""
|
"""
|
||||||
total_value = self.cash
|
total_value = self.cash
|
||||||
|
|
||||||
# 在单品种场景下,我们假设 self.positions 最多只包含一个品种
|
# 在单品种场景下,我们假设 self.positions 最多只包含一个品种
|
||||||
# 并且这个品种就是 current_bar.symbol 所代表的品种
|
# 并且这个品种就是 current_bar.symbol 所代表的品种
|
||||||
symbol_in_position = list(self.positions.keys())[0] if self.positions else None
|
symbol_in_position = list(self.positions.keys())[0] if self.positions else None
|
||||||
@@ -246,7 +246,7 @@ class ExecutionSimulator:
|
|||||||
# 持仓市值 = 数量 * 当前市场价格 (current_bar.close)
|
# 持仓市值 = 数量 * 当前市场价格 (current_bar.close)
|
||||||
# 无论多头(quantity > 0)还是空头(quantity < 0),这个计算都是正确的
|
# 无论多头(quantity > 0)还是空头(quantity < 0),这个计算都是正确的
|
||||||
total_value += quantity * current_bar.close
|
total_value += quantity * current_bar.close
|
||||||
|
|
||||||
# 您也可以选择在这里打印调试信息
|
# 您也可以选择在这里打印调试信息
|
||||||
# print(f" DEBUG Portfolio Value Calculation: Cash={self.cash:.2f}, "
|
# print(f" DEBUG Portfolio Value Calculation: Cash={self.cash:.2f}, "
|
||||||
# f"Position for {symbol_in_position}: {quantity} @ {current_bar.close:.2f}, "
|
# f"Position for {symbol_in_position}: {quantity} @ {current_bar.close:.2f}, "
|
||||||
@@ -254,7 +254,7 @@ class ExecutionSimulator:
|
|||||||
|
|
||||||
# 如果没有持仓,或者持仓品种与当前Bar品种不符 (理论上单品种不会发生)
|
# 如果没有持仓,或者持仓品种与当前Bar品种不符 (理论上单品种不会发生)
|
||||||
# 那么 total_value 依然是 self.cash
|
# 那么 total_value 依然是 self.cash
|
||||||
|
|
||||||
return total_value
|
return total_value
|
||||||
|
|
||||||
def get_current_positions(self) -> Dict[str, int]:
|
def get_current_positions(self) -> Dict[str, int]:
|
||||||
@@ -268,3 +268,22 @@ class ExecutionSimulator:
|
|||||||
返回所有成交记录的副本。
|
返回所有成交记录的副本。
|
||||||
"""
|
"""
|
||||||
return self.trade_log.copy()
|
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 = []
|
||||||
|
|||||||
@@ -101,10 +101,10 @@ class SimpleLimitBuyStrategy(Strategy):
|
|||||||
order = Order(
|
order = Order(
|
||||||
id=order_id,
|
id=order_id,
|
||||||
symbol=self.symbol,
|
symbol=self.symbol,
|
||||||
direction="SELL",
|
direction="BUY",
|
||||||
volume=trade_volume,
|
volume=trade_volume,
|
||||||
price_type="LIMIT",
|
price_type="MARKET",
|
||||||
limit_price=limit_price,
|
# limit_price=limit_price,
|
||||||
submitted_time=bar.datetime
|
submitted_time=bar.datetime
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user