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