训练模型
在 上一篇文档 中,我们学会了如何在 PyBroker 中编写股票指标。指标是开发交易策略的良好起点。但是要创建一个成功的策略,可能需要使用预测建模的更复杂方法。
PyBroker 的主要功能之一就是能够训练和回测机器学习模型。这些模型可以利用指标作为特征来更准确地预测市场走势。一旦训练完成,可以使用一种称为向前分析(Walkforward Analysis)的流行技术对这些模型进行回测,该技术模拟了策略在实际交易中的表现。
我们将在本文档的后面更深入地解释向前分析。但首先,让我们从一些必要的导入开始:
[1]:
import numpy as np
import pybroker
from numba import njit
from pybroker import Strategy, StrategyConfig, YFinance
与 DataSource 和 Indicator 数据一样,PyBroker 也可以将训练过的模型缓存到磁盘。你可以通过调用 pybroker.enable_caches 来启用这三者的缓存:
[2]:
pybroker.enable_caches("walkforward_strategy")
在 上一篇文档 中,我们使用 NumPy 和 Numba 实现了一个计算收盘价减去移动平均价(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_size 为 0.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 来自定义预测的生成方式(覆盖模型默认的预测函数)。