Multi-Symbol Models
The model training used so far has trained a separate instance for every ticker symbol. Instead, a single model can be trained across symbols that behave similarly, such as stocks in a specific industry.
PyBroker v2 supports training one model across all symbols passed to Strategy.add_execution, as shown in this notebook.
[1]:
import pybroker
from pybroker import Strategy, YFinance
from sklearn.linear_model import LinearRegression
pybroker.enable_data_source_cache("multi_symbol_models")
[1]:
<pybroker.cache._L1Cache at 0x7f0d105fee40>
Training One Model on Multiple Symbols
This notebook repurposes the linear regression example from Training a Model. Below, one shared LinearRegression model is trained with the close_minus_ma indicator on four chip stocks:
[2]:
from pybroker.indicator import close_minus_ma
cmma_20 = close_minus_ma("cmma_20", lookback=20, atr_length=14)
def train_slr(symbols, train_data, test_data):
# Shift within symbols so returns never cross a symbol boundary.
next_close = train_data.groupby("symbol")["close"].shift(-1)
train_data["target"] = next_close / train_data["close"] - 1
train_data = train_data.dropna()
model = LinearRegression()
model.fit(train_data[["cmma_20"]], train_data["target"])
return model, ["cmma_20"]
model_slr = pybroker.model("slr", train_slr, indicators=[cmma_20], pooled=True)
SYMBOLS = ["MU", "TXN", "ADI", "AMAT"]
Registering the model with pooled=True will train it only once per execution. This replaces the training function’s single symbol argument with a symbols tuple and passes the combined train and test splits.
During the backtest, the trained model is shared across all symbols in the execution:
[3]:
POS_SIZE = 1 / len(SYMBOLS)
def hold_long(ctx):
pred = ctx.preds("slr")[-1]
if not ctx.long_pos():
if pred > 0:
ctx.buy_shares = ctx.calc_target_shares(POS_SIZE)
elif pred < 0:
ctx.sell_all_shares()
strategy = Strategy(YFinance(), start_date="1/1/2021", end_date="1/1/2026")
strategy.add_execution(hold_long, SYMBOLS, models=model_slr)
result = strategy.walkforward(
warmup=20, windows=3, train_size=0.5, lookahead=1
)
result.metrics_df.head(20)
Backtesting: 2021-01-01 00:00:00 to 2026-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 4 of 4 completed
Loaded bar data: 0:00:00
Computing indicators...
100% (4 of 4) |##########################| Elapsed Time: 0:00:00 Time: 0:00:00
Train split: 2021-01-07 00:00:00 to 2022-04-04 00:00:00
Finished training models: 0:00:00
Test split: 2022-04-05 00:00:00 to 2023-07-05 00:00:00
100% (313 of 313) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Train split: 2022-04-05 00:00:00 to 2023-07-05 00:00:00
Finished training models: 0:00:00
Test split: 2023-07-06 00:00:00 to 2024-10-01 00:00:00
100% (313 of 313) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Train split: 2023-07-06 00:00:00 to 2024-10-01 00:00:00
Finished training models: 0:00:00
Test split: 2024-10-02 00:00:00 to 2025-12-31 00:00:00
100% (313 of 313) |######################| Elapsed Time: 0:00:00 Time: 0:00:00
Finished backtest: 0:00:01
[3]:
| name | value | |
|---|---|---|
| 0 | trade_count | 170 |
| 1 | initial_market_value | 100000.0 |
| 2 | end_market_value | 173448.08 |
| 3 | total_pnl | 76163.08 |
| 4 | unrealized_pnl | -2715.0 |
| 5 | total_return_pct | 76.16308 |
| 6 | total_profit | 150256.55 |
| 7 | total_loss | -74093.47 |
| 8 | total_fees | 0.0 |
| 9 | max_drawdown | -41802.76 |
| 10 | max_drawdown_pct | -28.604164 |
| 11 | max_drawdown_date | 2025-04-08 00:00:00 |
| 12 | win_rate | 70.0 |
| 13 | loss_rate | 30.0 |
| 14 | winning_trades | 119 |
| 15 | losing_trades | 51 |
| 16 | avg_pnl | 448.018118 |
| 17 | avg_return_pct | 1.465824 |
| 18 | avg_trade_bars | 14.005882 |
| 19 | avg_profit | 1262.660084 |