多资产 / 投资组合回测¶
QTrade 支持投资组合回测:单个策略同时交易多个资产,共用一个现金池。快速开始里的单资产用法保持不变 —— 启用多资产只需要把 DataFrame 换成 dict 传进去。
数据¶
传入一个以资产代码为键的 dict[str, pd.DataFrame]。每个 DataFrame 都需要标准的 OHLCV 列,并且必须共享完全一致的 DatetimeIndex(数据源不一致时请预先对齐 / reindex):
import yfinance as yf
import pandas as pd
aapl = yf.download("AAPL", start="2023-01-01", end="2024-01-01",
interval="1d", multi_level_index=False)
nvda = yf.download("NVDA", start="2023-01-01", end="2024-01-01",
interval="1d", multi_level_index=False)
# Inner-join indexes to ensure both assets are present on every bar.
common_index = aapl.index.intersection(nvda.index)
data = {
"AAPL": aapl.loc[common_index],
"NVDA": nvda.loc[common_index],
}
Backtest 会单独验证每个 DataFrame,如果索引不一致会抛出清晰的错误。
策略¶
多资产模式下,策略必须指定每个订单针对哪个资产。遍历 self.assets,并使用 self.positions[symbol] / self.data_by_asset[symbol]:
from qtrade.backtest import Strategy
class PortfolioMeanReversion(Strategy):
def prepare(self):
# Compute indicators per asset.
for asset, df in self._data.items():
df['ma'] = df['Close'].rolling(self.window).mean()
df['z'] = (df['Close'] - df['ma']) / df['Close'].rolling(self.window).std()
def on_bar_close(self):
for asset in self.assets:
df = self.data_by_asset[asset]
z = df['z'].iloc[-1]
if pd.isna(z):
continue
pos = self.positions[asset].size
if abs(z) < 0.3 and pos != 0:
self.close(asset)
elif z < -1.0 and pos <= 0:
self.close(asset)
self.buy(asset, size=10)
elif z > 1.0 and pos >= 0:
self.close(asset)
self.sell(asset, size=10)
多资产模式下的 API 速查:
单资产简写 |
多资产对应写法 |
|---|---|
|
|
|
|
|
|
|
|
|
|
在多资产模式下,self.position 和 self.data 会抛出 AttributeError,提示信息会指向对应的 dict 访问器。
运行回测¶
from qtrade.backtest import Backtest
from qtrade.core import PercentageCommission
bt = Backtest(
data,
PortfolioMeanReversion,
cash=200_000,
commission=PercentageCommission(percentage=0.0005),
margin_ratio=0.5,
trade_on_close=True,
)
bt.run(window=10)
bt.show_stats() # portfolio-level metrics
show_stats() 输出的 Buy & Hold Return [%] 是各资产的等权平均收益 —— 对组合策略而言这是一个合理的默认基准。
按资产拆分¶
查看每个资产的交易级统计:
from qtrade.utils.stats import calculate_stats_per_asset
per = calculate_stats_per_asset(bt.broker)
for asset, stats in per.items():
print(f"--- {asset} ---")
print(f" Trades: {stats['Total Trades']}")
print(f" Win Rate: {stats['Win Rate [%]']:.1f}%")
print(f" Buy & Hold: {stats['Buy & Hold Return [%]']:.2f}%")
组合级指标(Sharpe、Sortino、回撤、净值曲线)本质上是组合范围的,只能通过 calculate_stats(broker) 获取 —— 把它们归因到单个资产需要额外的会计逻辑,QTrade 有意不去做这件事。
可视化¶
bt.plot() 渲染多面板布局:
顶部:组合净值 vs 等权 Buy & Hold(叠加回撤区域)。
中部:所有资产的交易收益散点图。
底部:每个资产一个 OHLC 面板,纵向堆叠,X 轴联动缩放。
每个资产面板上的赢/输交易框和买卖标记仅显示该资产自身的交易。
交易历史¶
bt.get_trade_history() 返回的 DataFrame 包含 Asset 列,方便后续按代码筛选或分组:
df = bt.get_trade_history()
print(df.groupby('Asset')['Profit'].sum())