快速开始

本指南帮你用 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,包含 OpenHighLowClose 列(Volume 可选),索引为 DatetimeIndex。列名大小写不敏感(openOpen 都行)。

本指南使用 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 含每个窗口的细节。