训练模型

上一篇文档 中,我们学会了如何在 PyBroker 中编写股票指标。指标是开发交易策略的良好起点。但是要创建一个成功的策略,可能需要使用预测建模的更复杂方法。

幸运的是,PyBroker 的主要功能之一就是能够训练和回测机器学习模型。这些模型可以利用指标作为特征来更准确地预测市场走势。一旦训练完成,可以使用一种称为向前分析(Walkforward Analysis)的流行技术对这些模型进行回测,该技术模拟了策略在实际交易中的表现。

稍后我们将在本笔记本中更深入地解释向前分析。但首先,让我们从一些必要的导入开始!

[1]:
import numpy as np
import pandas as pd
import pybroker
from numba import njit
from pybroker import Strategy, StrategyConfig, YFinance

DataSourceIndicator 数据一样,PyBroker 也可以将训练过的模型缓存到磁盘。你可以通过调用 pybroker.enable_caches 来启用这三者的缓存:

[2]:
pybroker.enable_caches('walkforward_strategy')

上一篇文档 中,我们使用 NumPyNumba 实现了一个计算收盘价减去移动平均价(CMMA)的指标。以下是 CMMA 指标的代码:

[3]:
def cmma(bar_data, lookback):

    @njit  # Enable Numba JIT.
    def vec_cmma(values):
        # Initialize the result array.
        n = len(values)
        out = np.array([np.nan for _ in range(n)])

        # For all bars starting at lookback:
        for i in range(lookback, n):
            # Calculate the moving average for the lookback.
            ma = 0
            for j in range(i - lookback, i):
                ma += values[j]
            ma /= lookback
            # Subtract the moving average from value.
            out[i] = values[i] - ma
        return out

    # Calculate for close prices.
    return vec_cmma(bar_data.close)

cmma_20 = pybroker.indicator('cmma_20', cmma, lookback=20)

训练和回测

接下来,我们想要构建一个模型,使用 20 天的 CMMA 预测第二天的回报。使用 简单线性回归 是开始实验的好方法。下面我们从 scikit-learn 导入一个 LinearRegression 模型:

[4]:
from sklearn.linear_model import LinearRegression
from sklearn.metrics import r2_score

我们创建一个 train_slr 函数来训练 LinearRegression 模型:

[5]:
def train_slr(symbol, train_data, test_data):
    # Train
    # Previous day close prices.
    train_prev_close = train_data['close'].shift(1)
    # Calculate daily returns.
    train_daily_returns = (train_data['close'] - train_prev_close) / train_prev_close
    # Predict next day's return.
    train_data['pred'] = train_daily_returns.shift(-1)
    train_data = train_data.dropna()
    # Train the LinearRegession model to predict the next day's return
    # given the 20-day CMMA.
    X_train = train_data[['cmma_20']]
    y_train = train_data[['pred']]
    model = LinearRegression()
    model.fit(X_train, y_train)

    # Test
    test_prev_close = test_data['close'].shift(1)
    test_daily_returns = (test_data['close'] - test_prev_close) / test_prev_close
    test_data['pred'] = test_daily_returns.shift(-1)
    test_data = test_data.dropna()
    X_test = test_data[['cmma_20']]
    y_test = test_data[['pred']]
    # Make predictions from test data.
    y_pred = model.predict(X_test)
    # Print goodness of fit.
    r2 = r2_score(y_test, np.squeeze(y_pred))
    print(symbol, f'R^2={r2}')

    # Return the trained model and columns to use as input data.
    return model, ['cmma_20']

train_slr 函数使用 20 天的 CMMA 作为 LinearRegression 模型的输入特征或预测因子。然后,该函数将 LinearRegression 模型拟合到该股票代码的训练数据。

拟合模型后,该函数使用测试数据评估模型的准确性,具体地说,是通过计算 R 平方 得分。R 平方得分提供了一个衡量 LinearRegression 模型拟合测试数据有多好的方法。

train_slr 函数的最终输出是针对该股票代码的训练过的 LinearRegression 模型,以及用作预测输入数据的 cmma_20 列。回测过程中,PyBroker 将使用此模型预测股票的第二天回报。对于每个股票代码,都会调用 train_slr 函数,训练过的模型将用于预测每个股票的第二天回报。

定义了训练模型的函数之后,需要将其注册到 PyBroker。这是通过使用 pybroker.model 函数创建一个新的 ModelSource 实例来完成的。此函数的参数是模型的名称(在本例中为 'slr')、将训练模型的函数(train_slr)以及作为模型输入的指标列表(在本例中为 cmma_20)。

[6]:
model_slr = pybroker.model('slr', train_slr, indicators=[cmma_20])

为了创建使用训练过的模型的交易策略,需要使用 YFinance 数据源创建一个新的 Strategy 对象,并指定回测周期的开始和结束日期。

[7]:
config = StrategyConfig(bootstrap_sample_size=100)
strategy = Strategy(YFinance(), '3/1/2017', '3/1/2022', config)
strategy.add_execution(None, ['NVDA', 'AMD'], models=model_slr)

然后在 Strategy 对象上调用 add_execution 方法来指定交易执行的详细信息。在这种情况下,将 None 值作为第一个参数传递,这意味着在回测期间不会使用交易功能。

最后一步是在 Strategy 对象上调用 backtest 方法进行回测,设置 train_size0.5 以指定模型应该在回测数据的前半部分进行训练,并在后半部分进行测试。

[8]:
strategy.backtest(train_size=0.5)
Backtesting: 2017-03-01 00:00:00 to 2022-03-01 00:00:00

Loading bar data...
[*********************100%***********************]  2 of 2 completed
Loaded bar data: 0:00:00

Computing indicators...
100% (2 of 2) |##########################| Elapsed Time: 0:00:01 Time:  0:00:01

Train split: 2017-03-01 00:00:00 to 2019-08-28 00:00:00
AMD R^2=-0.006808549721842416
NVDA R^2=-0.004416132743176426
Finished training models: 0:00:00

Finished backtest: 0:00:01

向前分析

PyBroker 使用一种称为向前分析(Walkforward Analysis)的强大算法来进行回测。该算法将回测数据划分为固定数量的时间窗口,每个窗口包含数据的训练-测试划分。

然后,向前分析算法以与现实世界中执行交易策略相同的方式“向前行进”。首先在最早的窗口上训练模型,然后在该窗口的测试数据上进行评估。

当算法向前移动以评估时间中的下一个窗口时,将前一个窗口的测试数据添加到训练数据中。这个过程持续进行,直到评估完所有的时间窗口。

向前分析图

通过使用这种方法,向前分析算法能够模拟交易策略在现实世界中的表现,并产生更可靠、更准确的回测结果。

让我们考虑一个从我们之前训练的 LinearRegression 模型生成买卖信号的交易策略。该策略作为 hold_long 函数实现:

[9]:
def hold_long(ctx):
    if not ctx.long_pos():
        # Buy if the next bar is predicted to have a positive return:
        if ctx.preds('slr')[-1] > 0:
            ctx.buy_shares = 100
    else:
        # Sell if the next bar is predicted to have a negative return:
        if ctx.preds('slr')[-1] < 0:
            ctx.sell_shares = 100

strategy.clear_executions()
strategy.add_execution(hold_long, ['NVDA', 'AMD'], models=model_slr)

hold_long 函数在模型预测下一个柱状图的回报为正时开启一个多头仓位,然后在模型预测回报为负时平仓。

ctx.preds(‘slr’) 方法用于访问当前在函数中执行的股票代码(NVDA 或 AMD)的 'slr' 模型所做的预测。预测值存储在一个 NumPy 数组 中,使用 ctx.preds('slr')[-1] 可以访问当前股票代码的最新预测,这是模型对下一个柱状图回报的预测。

现在我们已经定义了一个交易策略并注册了 'slr' 模型,我们可以使用向前分析算法进行回测。

通过在 Strategy 对象上调用 walkforward 方法并指定所需的时间窗口数量和训练/测试划分比例来运行回测。在这种情况下,我们将使用 3 个时间窗口,每个窗口的训练-测试划分为 50/50。

此外,由于我们的 'slr' 模型对未来一个柱状图的回报进行了预测,我们需要将 lookahead 参数设置为 1。这是为了确保训练数据不会泄露到测试边界。lookahead 参数应始终设置为预测的未来柱状图数量。

[10]:
result = strategy.walkforward(
    warmup=20,
    windows=3,
    train_size=0.5,
    lookahead=1,
    calc_bootstrap=True
)
Backtesting: 2017-03-01 00:00:00 to 2022-03-01 00:00:00

Loaded cached bar data.

Loaded cached indicator data.

Train split: 2017-03-06 00:00:00 to 2018-06-01 00:00:00
AMD R^2=-0.007950114729117885
NVDA R^2=-0.04203364470839133
Finished training models: 0:00:00

Test split: 2018-06-04 00:00:00 to 2019-08-30 00:00:00
100% (314 of 314) |######################| Elapsed Time: 0:00:00 Time:  0:00:00

Train split: 2018-06-04 00:00:00 to 2019-08-30 00:00:00
AMD R^2=0.0006422677593683757
NVDA R^2=-0.023591728578221893
Finished training models: 0:00:00

Test split: 2019-09-03 00:00:00 to 2020-11-27 00:00:00
100% (314 of 314) |######################| Elapsed Time: 0:00:00 Time:  0:00:00

Train split: 2019-09-03 00:00:00 to 2020-11-27 00:00:00
AMD R^2=-0.015508227883924253
NVDA R^2=-0.4567200095787838
Finished training models: 0:00:00

Test split: 2020-11-30 00:00:00 to 2022-02-28 00:00:00
100% (314 of 314) |######################| Elapsed Time: 0:00:00 Time:  0:00:00

Calculating bootstrap metrics: sample_size=100, samples=10000...
Calculated bootstrap metrics: 0:00:03

Finished backtest: 0:00:04

在使用向前分析算法进行回测过程中,'slr' 模型在给定窗口的训练数据上进行训练,然后 hold_long 函数在相同窗口的测试数据上运行。

模型在训练数据上进行训练以预测第二天的回报。然后,hold_long 函数使用这些预测为当前交易日的交易会话做出买卖决策。

通过评估每个窗口测试数据上交易策略的表现,我们可以了解策略在现实交易环境中可能的表现。这个过程在回测的每个时间窗口中重复进行,使用结果来评估交易策略的整体表现:

[11]:
result.metrics_df
[11]:
name value
0 trade_count 43.000000
1 initial_market_value 100000.000000
2 end_market_value 109831.000000
3 total_pnl 12645.000000
4 unrealized_pnl -2814.000000
5 total_return_pct 12.645000
6 total_profit 20566.000000
7 total_loss -7921.000000
8 total_fees 0.000000
9 max_drawdown -14177.000000
10 max_drawdown_pct -12.272121
11 win_rate 76.744186
12 loss_rate 23.255814
13 winning_trades 33.000000
14 losing_trades 10.000000
15 avg_pnl 294.069767
16 avg_return_pct 5.267674
17 avg_trade_bars 25.488372
18 avg_profit 623.212121
19 avg_profit_pct 9.237576
20 avg_winning_trade_bars 19.151515
21 avg_loss -792.100000
22 avg_loss_pct -7.833000
23 avg_losing_trade_bars 46.400000
24 largest_win 2715.000000
25 largest_win_pct 9.320000
26 largest_win_bars 2.000000
27 largest_loss -5054.000000
28 largest_loss_pct -16.140000
29 largest_loss_bars 43.000000
30 max_wins 13.000000
31 max_losses 2.000000
32 sharpe 0.023425
33 profit_factor 1.094471
34 ulcer_index 1.177116
35 upi 0.009193
36 equity_r2 0.772082
37 std_error 4191.846954
[12]:
result.bootstrap.conf_intervals
[12]:
lower upper
name conf
Profit Factor 97.5% 0.259819 1.296660
95% 0.303435 1.151299
90% 0.373167 1.002514
Sharpe Ratio 97.5% -0.359565 0.050383
95% -0.332180 0.018154
90% -0.276757 -0.018004
[13]:
result.bootstrap.drawdown_conf
[13]:
amount percent
conf
99.9% -13917.50 -12.190522
99% -11058.25 -9.693729
95% -8380.25 -7.480589
90% -7129.00 -6.403027

总之,我们现在已经完成了使用 PyBroker 对线性回归模型进行训练和回测的过程,并借助向前分析。我们看到的指标是基于回测中所有时间窗口的测试数据。尽管我们的交易策略需要改进,但我们已经很好地理解了如何在 PyBroker 中训练和评估模型。

请注意,在进行回归分析之前,验证某些假设(如 同方差性、残差的正态性等)是很重要的。为了简洁起见,我在这里没有提供这些假设的细节,建议你自行进行这个练习。

我们在 PyBroker 中构建模型不仅限于线性回归模型。我们可以训练其他类型的模型,如梯度提升机、神经网络或任何我们选择的其他架构。这种灵活性使我们可以探索和尝试各种模型和方法,以找到最适合我们特定交易目标的最佳表现模型。

PyBroker 还提供了定制选项,例如为我们的模型指定一个 input_data_fn,以便我们可以自定义其输入数据的构建方式。当构建用于自回归模型(如 ARMA 或 RNN)的输入时,需要使用多个过去的值进行预测,此时就需要这样做。同样,我们可以指定自己的 predict_fn 来自定义预测的方式(默认情况下,会调用模型的 预测 函数)。

有了这些知识,你可以开始在 PyBroker 中构建和测试自己的模型和交易策略,并开始探索这个框架所提供的广泛可能性!