Docs
Open Studio

Guides

Machine learning

On this page6

This page takes you through stock selection with machine learning: merge features → generate labels → train a model → backtest. finlab.ml shapes the data the way models expect and wraps qlib's LightGBM, XGBoost, CatBoost and other models behind one interface.

Install

Install FinLab with its Qlib dependencies (including TA-Lib and model libraries on supported platforms):

pip install 'finlab[qlib]'

finlab[qlib] includes LightGBM, XGBoost, CatBoost, and other model dependencies; PyTorch is also installed on supported platforms.

Install or update model libraries individually

Build an ML strategy in four steps

Merge features

finlab.ml.feature.combine() merges features from multiple sources (technical, fundamental, custom) into one table and resamples them with resample. This merges the P/B and P/E ratios of S&P 500 stocks, computed from SEC filings, into a weekly feature set:

from finlab import data
from finlab.ml import feature as mlf

data.set_market('us')
with data.us_universe(index='SP500'):
    raw_close = data.get('price:close')  # unadjusted price for valuation
    market_value = data.get('etl:us_market_value')
    eps = data.get('us_income_statement:eps_diluted')
    equity = data.get('us_balance_sheet:total_stockholders_equity')

ttm_eps = eps.rolling(4).sum()  # Trailing-12-month EPS
features = mlf.combine(
    {
        # Ratios only where the denominator is positive
        'pb': market_value / equity[equity > 0],
        'pe': raw_close / ttm_eps[ttm_eps > 0],
    },
    resample='W',
)

features.head()

The result is indexed by (datetime, instrument), for example (2024-01-07, 'AAPL'), with one column per feature: pb and pe.

Generate labels

Labels are the answers the model learns to predict, such as each stock's return over the next week. The functions in finlab.ml.label take the features' index and return a pd.Series with the same index, so labels pair with features row by row. Use the same resample as in combine() so the dates line up:

from finlab.ml import label as mll

# Next-week returns as labels
labels = mll.return_percentage(features.index, resample='W', period=1)

For other labels (day-trading return, MAE/MFE, excess return), see Choose a different label below.

Split the data and train a model

Split by date: train on data before 2023 and predict afterwards. Train only on rows where both the features and the label are present:

import finlab.ml.qlib as q

complete = features.notna().all(axis=1) & labels.notna()
is_train = features.index.get_level_values('datetime') < '2023-01-01'
X_train = features[complete & is_train]
y_train = labels[complete & is_train]
X_test = features[~is_train].dropna()

model = q.LGBModel()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)

model.predict() returns a table of predictions with one row per date and one column per stock.

Backtest

Pick the 50 stocks with the highest predictions each period and backtest them with sim to get the strategy's returns and risk metrics:

from finlab.backtest import sim

position = y_pred.is_largest(50)

sim(position, resample='4W')

Add technical indicator features

finlab.ml.feature exposes ta_names and ta for generating TA-Lib indicator features.

Generate indicator names: ta_names

ta_names generates a list of TA-Lib indicator names, one per parameter configuration.

  • n: random parameter configurations per indicator. n=10 yields up to 10 variants per TA-Lib indicator; indicators without parameters appear once.
from finlab.ml import feature as mlf

mlf.ta_names(n=1)
['talib.HT_DCPERIOD__real__',
 'talib.HT_DCPHASE__real__',
 'talib.HT_PHASOR__quadrature__',
 'talib.HT_PHASOR__inphase__',
 'talib.HT_SINE__sine__',
 'talib.HT_SINE__leadsine__'
 ...
 ]

Compute indicator values: ta

ta computes the actual values for a list of names produced by ta_names.

  • resample: optional; resamples the computed indicator values to a given frequency.
from finlab.ml import feature as mlf

mlf.ta(
    [
        'talib.HT_DCPERIOD__real__',
        'talib.HT_DCPHASE__real__',
        'talib.HT_PHASOR__quadrature__',
    ],
    resample='W',
)

With resample='W' the dates move to the weekly period labels (Sundays), one row per symbol per week.

Merge technical indicators into one feature set

Pass the output of ta() to combine(). This draws three random parameter sets each for RSI, MACD and ATR:

from finlab import data
from finlab.ml import feature as mlf

data.set_market('us')
# Three random parameter sets each for RSI, MACD and ATR
names = [
    name
    for name in mlf.ta_names(n=3)
    if name.startswith(('talib.RSI__', 'talib.MACD__', 'talib.ATR__'))
]
with data.us_universe(index='SP500'):
    technical = mlf.combine({'talib': mlf.ta(names)}, resample='W')

The result has the same (datetime, instrument) index with one column per indicator output, named like talib.RSI__real__timeperiod__37__ and talib.MACD__macdsignal__fastperiod__48__slowperiod__104__signalperiod__36__. Indicators are computed from the prices of the active market and universe.

Choose a different label

finlab.ml.label provides various return and risk labels:

FunctionLabelresampleperiod
daytrading_percentageReturn from the next trading day's open to its close (a day-trading target)Yes, default 'D'No
return_percentageReturn over the next N periods, for medium- to long-term performanceYesYes
maximum_adverse_excursionMAE: maximum adverse movement during the holding periodNot supported yetYes
maximum_favorable_excursionMFE: maximum favorable movement during the holding periodNot supported yetYes
excess_over_medianExcess return over the market-wide median return for the same periodYesYes
excess_over_meanExcess return over the market-wide mean return for the same periodYesYes
  • resample: must match the resample used in combine to align time periods.
  • period: the number of future periods; the length of a period is set by resample.
from finlab.ml import label as mll

# Labels for the weekly features built above; use the same resample
label = mll.daytrading_percentage(features.index, resample='W')
label = mll.return_percentage(features.index, resample='W', period=1)
label = mll.maximum_adverse_excursion(features.index, period=1)
label = mll.maximum_favorable_excursion(features.index, period=1)
label = mll.excess_over_median(features.index, resample='W', period=1)
label = mll.excess_over_mean(features.index, resample='W', period=1)

Each label is a pd.Series with the same (datetime, instrument) index as the features. Ensure the index and market settings are correct; labels can be combined with features directly for model training.

Switch to another model

WrapperModel adapts LightGBM, XGBoost, CatBoost, linear models, TabNet, DNN and others into a uniform fit/predict interface, so you can focus on features and strategy without getting bogged down in model details. Every model is used the same way; only the line that creates it changes:

import finlab.ml.qlib as q

model = q.XGBModel()  # any model from the table below
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
ModelWraps
LGBModelLightGBM
XGBModelXGBoost
DEnsmbleModelDouble Ensemble
CatBoostModelCatBoost
LinearModelA linear model
TabnetModelTabNet
DNNModelA deep neural network

Try several models: get_models

get_models() retrieves the available models, making multi-model experiments easy:

import finlab.ml.qlib as q

# Get all available models
models = q.get_models()

# Print all model names
print(list(models.keys()))

# Select and instantiate a model, e.g., LightGBM
model = models['LGBModel']()

# Assuming X_train, y_train, X_test are already prepared
model.fit(X_train, y_train)

# Predict using the trained model
y_pred = model.predict(X_test)

Next steps

↑↓ Move Enter Open Esc Close