快速开始¶
本指南帮你用 QTrade 上手回测交易策略。
策略¶
我们写一个简单的 SMA 交叉策略。自定义策略继承自 qtrade.Strategy,实现 prepare()(一次性初始化)和 on_bar_close()(每根 bar 调用):
from qtrade.backtest import Strategy
class SMAStrategy(Strategy):
def prepare(self):
# Indicators are computed up-front so on_bar_close can read them cheaply.
# self._data is dict[str, DataFrame]; for single-asset there's only
# one entry — iterate to support both single- and multi-asset.
for df in self._data.values():
df['SMA3'] = df['Close'].rolling(3).mean()
df['SMA10'] = df['Close'].rolling(10).mean()
def on_bar_close(self):
# self.data is the slice up to the current bar (no look-ahead).
sma3 = self.data['SMA3']
sma10 = self.data['SMA10']
# Golden cross → go long
if sma3.iloc[-2] < sma10.iloc[-2] and sma3.iloc[-1] > sma10.iloc[-1]:
self.buy()
# Death cross → flatten
elif sma3.iloc[-2] > sma10.iloc[-2] and sma3.iloc[-1] < sma10.iloc[-1]:
self.close()
数据¶
QTrade 不内置数据源 —— 你需要自备 OHLCV 的 pandas.DataFrame,包含 Open、High、Low、Close 列(Volume 可选),索引为 DatetimeIndex。列名大小写不敏感(open、Open 都行)。
本指南使用 yfinance 获取数据:
$ pip install yfinance
import yfinance as yf
# Download gold data with daily intervals
data = yf.download(
"GC=F",
start="2023-01-01",
end="2024-01-01",
interval="1d",
multi_level_index=False,
)
指标可以在策略的 prepare() 中添加(如上所示),也可以在传入 Backtest 之前直接加到 DataFrame 上。无论哪种方式,指标都能在策略里通过 self.data 访问。
回测¶
现在来用准备好的数据回测策略。
from qtrade.backtest import Backtest
bt = Backtest(
data=data,
strategy_class=SMAStrategy,
cash=10000,
)
bt.run()
# Show backtest results
bt.show_stats()
Start : 2023-01-03 00:00:00
End : 2023-12-29 00:00:00
Duration : 360 days 00:00:00
Start Value : 5000.0
End Value : 5504.3994140625
Total Return [%] : 10.08798828125
Total Commission Cost[%] : 0
Buy & Hold Return [%] : 12.10523221626531
Return (Ann.) [%] : 18.074280342264217
Volatility (Ann.) [%] : 7.44
Max Drawdown [%] : -3.8180767955781354
Max Drawdown Duration : 186 days 00:00:00
Total Trades : 14
Win Rate [%] : 42.857142857142854
Best Trade [%] : 239.0
Worst Trade [%] : -58.0
Avg Winning Trade [%] : 126.69986979166667
Avg Losing Trade [%] : -31.9749755859375
Avg Winning Trade Duration : 21 days 00:00:00
Avg Losing Trade Duration : 4 days 15:00:00
Profit Factor : 2.9718522251363866
Expectancy : 36.028529575892854
Sharpe Ratio : 2.271145457445316
Sortino Ratio : 2.6654676881799224
Calmar Ratio : 4.733870299098423
Omega Ratio : 1.7873724100138564
可视化¶
QTrade 使用 Bokeh 绘制结果图表。用以下命令生成回测结果可视化:
bt.plot()
交易明细¶
也可以查看所有交易明细:
trade_details = bt.get_trade_history()
print(trade_details)
Asset Type Size Entry Price Exit Price Entry Time Exit Date Profit Tag Exit Reason Duration
0 default Long 2.0 1833.500000 1812.699951 2023-03-02 2023-03-08 -41.600098 None signal 6 days
1 default Long 2.0 1862.000000 2007.400024 2023-03-10 2023-04-18 290.800049 None signal 39 days
... (rows omitted)
单资产回测下 Asset 列恒为 "default";多资产组合回测时则为对应资产代码(详见多资产 / 投资组合回测)。
参数优化(以及如何避免过拟合)¶
Backtest.optimize 在参数空间做网格搜索:
best_params, best_stats, all_results = bt.optimize(
n1=[3, 5, 10],
n2=[10, 20, 30],
maximize='Sharpe Ratio',
constraint=lambda p: p['n1'] < p['n2'],
)
陷阱在于:这些「最优」参数是在同一份数据上既调参又评估得到的。回测里看起来漂亮,实盘往往让人失望。使用 walk_forward_optimize 获得真正的样本外估计:
result = bt.walk_forward_optimize(
train_window=120, # bars used for parameter selection per window
test_window=30, # bars of out-of-sample evaluation that follow
maximize='Sharpe Ratio',
n1=[3, 5, 10],
n2=[10, 20, 30],
constraint=lambda p: p['n1'] < p['n2'],
)
print(result['summary'])
# {'n_windows': 7, 'mean_oos_return': 0.84, 'hit_rate': 0.57, ...}
每个窗口在训练片段挑选自己的最优参数,并在紧接的测试片段评估;summary 给出整体的样本外统计,windows 含每个窗口的细节。