训练模型

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

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

我们将在本文档的后面更深入地解释向前分析。但首先,让我们从一些必要的导入开始:

[1]:
import numpy as np
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()
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:00 Time:  0:00:00

Train split: 2017-03-02 00:00:00 to 2019-08-29 00:00:00
AMD R^2=-0.006951269993268561
NVDA R^2=-0.004188579889899735
Finished training models: 0:00:00

Finished backtest: 0:00:01
[8]:
TestResult(start_date=datetime.datetime(2017, 3, 1, 0, 0), end_date=datetime.datetime(2022, 3, 1, 0, 0), portfolio=Empty DataFrame
Columns: []
Index: [], positions=Empty DataFrame
Columns: []
Index: [], orders=Empty DataFrame
Columns: []
Index: [], trades=Empty DataFrame
Columns: []
Index: [], metrics=EvalMetrics(trade_count=0, initial_market_value=0, end_market_value=0, total_pnl=0, unrealized_pnl=0, total_return_pct=0, annual_return_pct=None, total_profit=0, total_loss=0, total_fees=0, max_drawdown=0, max_drawdown_pct=0, max_drawdown_date=None, win_rate=0, loss_rate=0, winning_trades=0, losing_trades=0, avg_pnl=0, avg_return_pct=0, avg_trade_bars=0, avg_profit=0, avg_profit_pct=0, avg_winning_trade_bars=0, avg_loss=0, avg_loss_pct=0, avg_losing_trade_bars=0, largest_win=0, largest_win_pct=0, largest_win_bars=0, largest_loss=0, largest_loss_pct=0, largest_loss_bars=0, max_wins=0, max_losses=0, sharpe=0, sortino=0, calmar=None, profit_factor=0, ulcer_index=0, upi=0, equity_r2=0, std_error=0, annual_std_error=None, annual_volatility_pct=None), metrics_df=Empty DataFrame
Columns: []
Index: [], bootstrap=None, signals=None, stops=None, symbols=frozenset({'NVDA', 'AMD'}))

向前分析

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.04203365318219077
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.0235917302991151
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.4567200811037322
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: bars=941, samples=10000...
Calculated bootstrap metrics: 0:00:00

Finished backtest: 0:00:00

在向前分析过程中,'slr' 模型在给定窗口的训练数据上进行训练,hold_long 函数则在同一窗口的测试数据上运行。

模型使用训练数据预测第二天的价格变动。然后,hold_long 函数依据这些预测,为当前交易日的交易会话做出买卖决策。

这个过程会针对回测中的每个时间窗口重复进行。我们可以查看结果来评估该策略:

[11]:
result.metrics_df
[11]:
name value
0 trade_count 43
1 initial_market_value 100000.0
2 end_market_value 107287.5
3 total_pnl 10409.0
4 unrealized_pnl -3121.5
5 total_return_pct 10.409
6 total_profit 12129.0
7 total_loss -1720.0
8 total_fees 0.0
9 max_drawdown -6129.6
10 max_drawdown_pct -5.537077
11 max_drawdown_date 2022-01-27 00:00:00
12 win_rate 76.744186
13 loss_rate 23.255814
14 winning_trades 33
15 losing_trades 10
16 avg_pnl 242.069767
17 avg_return_pct 5.268372
18 avg_trade_bars 25.488372
19 avg_profit 367.545455
20 avg_profit_pct 9.236364
21 avg_winning_trade_bars 19.151515
22 avg_loss -172.0
23 avg_loss_pct -7.826
24 avg_losing_trade_bars 46.4
25 largest_win 2004.0
26 largest_win_pct 23.82
27 largest_win_bars 201
28 largest_loss -754.0
29 largest_loss_pct -27.04
30 largest_loss_bars 32
31 max_wins 13
32 max_losses 2
33 sharpe 0.037646
34 sortino 0.054195
35 profit_factor 1.13091
36 ulcer_index 0.984188
37 upi 0.007807
38 equity_r2 0.906783
39 std_error 3117.783269
[12]:
result.bootstrap.conf_intervals
[12]:
lower upper
name conf
Profit Factor 97.5% 0.907461 1.415745
95% 0.941685 1.365034
90% 0.982327 1.310340
Sharpe Ratio 97.5% -0.025962 0.102061
95% -0.015605 0.091741
90% -0.003145 0.080173
[13]:
result.bootstrap.drawdown_conf
[13]:
amount percent
conf
99.9% -17114.10 -14.753068
99% -12992.42 -11.386747
95% -9880.41 -8.791603
90% -8543.49 -7.641047

我们现在已经借助向前分析,完成了使用 PyBroker 训练和回测线性回归模型的整个过程。

除了线性回归之外,PyBroker 还可以训练其他类型的模型,例如梯度提升机、神经网络或我们选择的任何其他架构。为了支持这些模型,PyBroker 提供了丰富的定制选项。例如,你可以指定一个 input_data_fn 来控制输入数据的构建方式,并提供你自己的 predict_fn 来自定义预测的生成方式(覆盖模型默认的预测函数)。

此外,PyBroker v2 现在支持训练 时间序列模型多股票代码模型