finlab.ml
Machine learning module providing feature engineering, label generation, and model training integration.
Use Cases
- Build feature sets for ML stock selection strategies
- Generate technical indicators (TA-Lib integration)
- Design training labels (returns, risk metrics)
- Integrate with the qlib framework for model training
- Predict future stock performance
Quick Examples
Feature Engineering
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')
net_income = data.get('us_income_statement:net_income')
equity = data.get('us_balance_sheet:total_stockholders_equity')
# Combine fundamental features (ratios only where the denominator is positive)
ttm_eps = eps.rolling(4).sum()
features = mlf.combine(
{
'pb': market_value / equity[equity > 0],
'pe': raw_close / ttm_eps[ttm_eps > 0], # Trailing-12-month P/E
'roe': net_income / equity[equity > 0],
},
resample='W',
)
# Add technical indicators
indicators = [
'talib.RSI__real__timeperiod__14__',
'talib.MACD__macd__fastperiod__12__slowperiod__26__signalperiod__9__',
]
features_ta = mlf.combine(
{'fundamental': features, 'technical': mlf.ta(indicators)}, resample='W'
)
Label Generation
from finlab.ml import label as mll
# Generate next-week return labels
label = mll.return_percentage(features.index, resample='W', period=1)
qlib Model Training
import finlab.ml.qlib as q
# Split train/test sets, keeping rows where the features and label are known
complete = features.notna().all(axis=1) & label.notna()
is_train = features.index.get_level_values('datetime') < '2023-01-01'
X_train, y_train = features[complete & is_train], label[complete & is_train]
X_test = features[~is_train].dropna()
# Train LightGBM model
model = q.LGBModel()
model.fit(X_train, y_train)
# Predict and convert to position weights
pred = model.predict(X_test)
position = pred.is_largest(30) # Buy top 30
Detailed Guide
See Machine Learning Strategy Development for: - Complete ML strategy development workflow - Feature engineering best practices - Label design techniques - Model training and optimization - Overfitting prevention
API Reference
finlab.ml.feature
Feature engineering module for combining and processing various features.
combine()
Combines a dictionary of features into one pandas DataFrame indexed by (datetime, instrument), with one column per feature. Wide features (dates by symbols) are reindexed onto the union of their dates and the symbols they share, forward-filled, then stacked; features that already have a (datetime, instrument) MultiIndex are concatenated as they are. Quarterly FinLab data is converted to filing dates first.
| Parameter | Type | Description |
|---|---|---|
features |
dict[str, pd.DataFrame or Callable] |
Feature name to a DataFrame (dates by symbols, or a MultiIndex frame), or a function returning one. |
resample |
str, pd.DatetimeIndex or None |
Optional resampling. String frequencies use calendar period labels (e.g. Sunday for 'W'); pass a DatetimeIndex for explicit trading-date anchors. MultiIndex features use the same resampling. Default None keeps the original dates. |
sample_filter |
pd.DataFrame or None |
Boolean DataFrame (dates by symbols) selecting which samples to keep. |
**kwargs |
Passed to the resampler. |
Returns: pd.DataFrame with all input features combined.
To anchor features on each week's last trading day and build labels on the same dates:
import pandas as pd
from finlab import data
from finlab.ml import feature as mlf, label as mll
data.set_market('us')
rsi = data.indicator('RSI')
calendar = rsi.index
weekly = pd.DatetimeIndex(calendar.to_series().resample('W').last().dropna())
features = mlf.combine({'rsi': rsi}, resample=weekly)
labels = mll.return_percentage(features.index, resample=weekly, period=2)
Usage Examples:
from finlab import data
from finlab.ml import feature as mlf
data.set_market('us')
with data.us_universe(index='SP500'):
close = data.get('price:adj_close')
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')
net_income = data.get('us_income_statement:net_income')
equity = data.get('us_balance_sheet:total_stockholders_equity')
ttm_eps = eps.rolling(4).sum()
pb = market_value / equity[equity > 0]
pe = raw_close / ttm_eps[ttm_eps > 0] # Trailing-12-month P/E
# Example 1: Combine fundamental features
features = mlf.combine(
{
'pb': pb,
'pe': pe,
'roe': net_income / equity[equity > 0],
},
resample='W',
)
# Example 2: Combine technical indicators
indicators = [
'talib.RSI__real__timeperiod__14__',
'talib.MACD__macd__fastperiod__12__slowperiod__26__signalperiod__9__',
]
with data.us_universe(index='SP500'):
features = mlf.combine({'talib': mlf.ta(indicators)}, resample='D')
# Example 3: Mix multiple feature types
with data.us_universe(index='SP500'):
technical = mlf.ta(indicators)
features = mlf.combine(
{
'fundamental': mlf.combine({'pb': pb, 'pe': pe}),
'technical': technical,
'custom': close / close.average(20), # Price relative to its 20-day average
},
resample='W',
)
resample Parameter
'D'- Daily'W'- Weekly (labeled on the calendar week end, Sunday)'M'- Monthly (month-end)- Features and labels must use the same resample frequency!
ta()
finlab.ml.feature.ta
ta(
feature_names=None,
factories=None,
resample=None,
start_time=None,
end_time=None,
adj=False,
cpu=-1,
**kwargs,
)
Calculate technical indicator values for a list of feature names.
| PARAMETER | DESCRIPTION |
|---|---|
feature_names
|
A list of technical indicator feature names. Defaults to None.
TYPE:
|
factories
|
A dictionary of factories to generate technical indicators. Defaults to {"talib": TalibIndicatorFactory()}.
TYPE:
|
resample
|
The frequency to resample the data to. Defaults to None.
TYPE:
|
start_time
|
The start time of the data. Defaults to None.
TYPE:
|
end_time
|
The end time of the data. Defaults to None.
TYPE:
|
**kwargs
|
Additional keyword arguments to pass to the resampler function.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DataFrame
|
pd.DataFrame: technical indicator feature names and their corresponding values. |
Technical Indicator Computation:
from finlab.ml import feature as mlf
data.set_market('us')
with data.us_universe(index='SP500'):
# Compute specific indicators
indicators = mlf.ta(
[
'talib.RSI__real__timeperiod__14__',
'talib.MACD__macd__fastperiod__12__slowperiod__26__signalperiod__9__',
'talib.BBANDS__upperband__timeperiod__20__nbdevup__2.0__nbdevdn__2.0__matype__0__',
],
resample='W',
)
# Random parameter sets for chosen indicators
names = [
name
for name in mlf.ta_names(n=3)
if name.startswith(('talib.RSI__', 'talib.MACD__', 'talib.ATR__'))
]
random_indicators = mlf.ta(names, resample='W')
ta_names()
finlab.ml.feature.ta_names
Generate a list of technical indicator feature names.
| PARAMETER | DESCRIPTION |
|---|---|
lb
|
The lower bound of the multiplier of the default parameter for the technical indicators.
TYPE:
|
ub
|
The upper bound of the multiplier of the default parameter for the technical indicators.
TYPE:
|
n
|
The number of random samples for each technical indicator.
TYPE:
|
factory
|
A factory object to generate technical indicators. Defaults to TalibIndicatorFactory.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
list[str]
|
list[str]: A list of technical indicator feature names. |
Examples:
import finlab.ml.feature as f
# method 1: generate each indicator with random parameters
features = f.ta()
# method 2: generate specific indicator
feature_names = ['talib.MACD__macdhist__fastperiod__52__slowperiod__212__signalperiod__75__']
features = f.ta(feature_names, resample='W')
# method 3: generate some indicator
feature_names = f.ta_names()
features = f.ta(feature_names)
Generate Indicator Name List:
from finlab.ml import feature as mlf
# Generate all TA-Lib indicators with 10 random parameter sets each
indicator_names = mlf.ta_names(n=10)
print(f"Total {len(indicator_names)} indicators")
# View examples
for name in indicator_names[:5]:
print(name)
# Output (parameters are drawn at random; indicators without parameters appear once):
# talib.HT_DCPERIOD__real__
# talib.HT_DCPHASE__real__
# talib.HT_PHASOR__inphase__
# talib.HT_PHASOR__quadrature__
# talib.HT_SINE__sine__
Recommended n Values
- n=1: one random parameter set per indicator (about 130 names with TA-Lib 0.8)
- n=5: up to 5 random parameter sets per indicator (about 560 names)
- n=10: more variety but longer computation time (about 1,070 names)
- Start with n=1 to test feasibility, then increase
Notes
- Too many indicators can lead to:
- Long computation time
- High memory usage
- Increased overfitting risk
- Recommend using feature selection to reduce indicator count
finlab.ml.label
Label generation module for designing machine learning target variables.
daytrading_percentage()
finlab.ml.label.daytrading_percentage
Calculate the percentage change of market prices over a given period.
| PARAMETER | DESCRIPTION |
|---|---|
index
|
A multi-level index of datetime and instrument.
TYPE:
|
resample
|
The resample frequency for the output data. Defaults to "D".
TYPE:
|
**kwargs
|
Additional arguments to be passed to the resampler function.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Series
|
pd.Series: A pd.Series containing the percentage change of stock prices. |
Next-Day Open-to-Close Return (day-trading label):
from finlab.ml import feature as mlf, label as mll
# Build daily features
features = mlf.combine({...}, resample='D')
# Label: return from the next trading day's open to its close
label = mll.daytrading_percentage(features.index)
For forward returns over N periods, use mll.return_percentage():
# Generate label: predict next 1-week return
label = mll.return_percentage(features.index, resample='W', period=1)
# Generate label: predict next 4-week return
label_4w = mll.return_percentage(features.index, resample='W', period=4)
Choosing the period Parameter of return_percentage()
- period=1: Short-term prediction (suitable for weekly/monthly rebalancing)
- period=4: Medium-term prediction (suitable for quarterly rebalancing)
- Larger period: More stable prediction but slower signal
- Recommendation: Match with strategy rebalancing frequency
maximum_adverse_excursion()
finlab.ml.label.maximum_adverse_excursion
Calculate the maximum adverse excursion of market prices over a given period.
| PARAMETER | DESCRIPTION |
|---|---|
index
|
A multi-level index of datetime and instrument.
TYPE:
|
period
|
The number of periods to calculate the percentage change over. Defaults to 1.
TYPE:
|
trade_at_price
|
The price for execution. Defaults to
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
Series
|
pd.Series: A pd.Series containing the maximum adverse excursion. |
Maximum Adverse Excursion (Risk Metric):
from finlab.ml import label as mll
# Compute maximum drawdown over next N periods
mae_label = mll.maximum_adverse_excursion(features.index, period=5)
# Can be used to train risk prediction models
# More negative values indicate higher risk
finlab.ml.qlib
qlib framework integration module providing multiple machine learning models for stock prediction.
Model Classes
finlab.ml.qlib provides the following models:
finlab.ml.qlib.LGBModel
finlab.ml.qlib.XGBModel
finlab.ml.qlib.CatBoostModel
finlab.ml.qlib.LinearModel
Basic Usage Example:
from finlab import data
from finlab.ml import feature as mlf, label as mll
import finlab.ml.qlib as q
# 1. Prepare features
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()
features = mlf.combine(
{
'pb': market_value / equity[equity > 0],
'pe': raw_close / ttm_eps[ttm_eps > 0],
},
resample='W',
)
# 2. Prepare labels
label = mll.return_percentage(features.index, resample='W', period=1)
# 3. Split train/test sets, keeping rows where the features and label are known
complete = features.notna().all(axis=1) & label.notna()
is_train = features.index.get_level_values('datetime') < '2020-01-01'
X_train, y_train = features[complete & is_train], label[complete & is_train]
X_test = features[~is_train].dropna()
# 4. Build and train model
model = q.XGBModel() # Options: q.LGBModel(), q.CatBoostModel(), q.LinearModel()
model.fit(X_train, y_train)
# 5. Predict
y_pred = model.predict(X_test)
Supported Model Types:
| Model | Description | Pros | Cons |
|---|---|---|---|
LGBModel() |
LightGBM | Fast, good performance, low memory usage | Requires lightgbm |
XGBModel() |
XGBoost | Stable, high interpretability | Slower training |
CatBoostModel() |
CatBoost | Handles categorical features well, low overfitting risk | High memory usage |
LinearModel() |
Linear Regression | Simple, fast, low overfitting risk | Usually lower performance |
Model Selection Recommendations
- Beginners:
LGBModel()(balanced speed and performance) - Stability-focused:
XGBModel() - Categorical features:
CatBoostModel() - Quick idea validation:
LinearModel() - Avoid overfitting: Start with
LinearModel()as baseline, then try complex models
Common Mistakes
- Data leakage: Ensure train and test sets do not overlap in time (use temporal split, not random split)
- Overfitting: Test set performance far worse than training set (IC < 0.02 may indicate overfitting)
- Look-ahead bias: Labels should use
.shift(-1)orperiod=1to avoid leaking future information
Complete Example
Building an ML Stock Selection Strategy
from finlab import data
from finlab.ml import feature as mlf, label as mll
import finlab.ml.qlib as q
from finlab.backtest import sim
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')
net_income = data.get('us_income_statement:net_income')
equity = data.get('us_balance_sheet:total_stockholders_equity')
# Technical indicators (use few to avoid overfitting)
technical = mlf.ta(mlf.ta_names(n=1)[:20]) # Only the first 20
# Step 1: Build feature set
print("Building features...")
ttm_eps = eps.rolling(4).sum()
features = mlf.combine(
{
# Fundamental features, only where the denominator is positive
'pb': market_value / equity[equity > 0],
'pe': raw_close / ttm_eps[ttm_eps > 0], # Trailing-12-month P/E
'roe': net_income / equity[equity > 0],
'technical': technical,
},
resample='W',
)
# Step 2: Generate labels
print("Generating labels...")
label = mll.return_percentage(features.index, resample='W', period=1)
# Step 3: Split train/test sets
print("Splitting data...")
is_train = features.index.get_level_values('datetime') < '2023-01-01'
X_train, y_train = features[is_train], label[is_train]
X_test = features[~is_train] # LightGBM handles missing values itself
# Step 4: Train model
print("Training model...")
model = q.LGBModel()
model.fit(X_train, y_train)
# Step 5: Predict
print("Generating predictions...")
pred = model.predict(X_test)
# Step 6: Convert to trading signals
print("Generating trading signals...")
position = pred.is_largest(30) # Buy stocks with highest predicted returns
# Step 7: Backtest
print("Running backtest...")
report = sim(position, resample='W')
report.display()
FAQ
Q: What happens if feature and label resample frequencies don't match?
Mismatched frequencies cause date misalignment, preventing correct feature-label pairing:
# Wrong: Inconsistent resample
features = mlf.combine({...}, resample='W') # Weekly
label = mll.return_percentage(features.index, resample='D', period=1) # Daily
# -> Labels measure 1-day returns, not 1-week returns
# Correct: Consistent resample
features = mlf.combine({...}, resample='W')
label = mll.return_percentage(features.index, resample='W', period=1)
Q: How do I prevent overfitting?
# Method 1: Reduce feature count
features = mlf.ta(mlf.ta_names(n=1)[:20]) # Only 20 indicators
# Method 2: Use regularization (reduce model complexity)
# LightGBM supports L1/L2 regularization, tree depth limits, etc.
# Method 3: Use a simple model
model = q.LinearModel() # Linear model is less prone to overfitting
# Method 4: Out-of-sample testing
# Ensure test set performance is close to training set (no more than 50% gap)
Q: Training is too slow, what can I do?
# Method 1: Reduce data range
data.truncate_start = '2020-01-01'
# Method 2: Reduce feature count
features = mlf.ta(mlf.ta_names(n=1)[:10]) # Only 10 indicators
# Method 3: Use resample='W' or 'M' (instead of 'D')
features = mlf.combine({...}, resample='W') # Weekly data is faster
Q: How do I handle missing values?
# First check the missing value situation
print(f"Feature missing ratio: {features.isna().sum().sum() / features.size:.2%}")
print(f"Label missing ratio: {label.isna().sum() / len(label):.2%}")
# Method 1: Drop samples with missing values
features_clean = features.dropna()
label_clean = label.dropna()
# Method 2: Forward fill each stock's own history (suitable for time series)
features_filled = features.groupby(level='instrument').ffill()
label_filled = label.groupby(level='instrument').ffill()
# Method 3: Fill with 0 (suitable for technical indicators)
features_zero = features.fillna(0)
Q: How do I convert prediction results to trading signals?
# Method 1: Buy top N
position = pred.is_largest(30)
# Method 2: Set threshold
position = pred > pred.quantile(0.8) # Buy top 20%
# Method 3: Long-short strategy
long_position = pred.is_largest(20) # Long top 20
short_position = pred.is_smallest(20) # Short bottom 20
position = long_position.astype(float) - short_position.astype(float)
# Method 4: Allocate weights by prediction rank (weights sum to 1 each date)
scores = pred.rank(axis=1, pct=True)
position = scores.div(scores.sum(axis=1), axis=0)
Resources
- Machine Learning Strategy Workflow - End-to-end development guide
- Machine Learning Strategy Development - Detailed tutorial
- qlib Official Documentation - qlib framework docs
- TA-Lib Indicator List - All technical indicators