Workflows
Machine Learning Strategies
On this page12
This page builds a machine learning stock-selection strategy: combine fundamental, technical and revenue data into features, label each row with its next-week return, train a model, turn the 30 highest predictions into holdings and backtest them, then re-predict and place orders every week.
By the end you will have a weekly feature table with labels, a trained model saved as ml_model.pkl, its backtest from 2023 onward, and a function that rebalances once a week.
Before you start, you need:
- The Qlib dependencies: run
pip install 'finlab[qlib]'before importingfinlab.ml.qlib. It installs the Qlib dependencies and model libraries, including LightGBM and XGBoost. - To know how to backtest with
sim()(see Backtesting). Iffinlab.mlis new to you, start with the shorter Machine learning tutorial.
The examples use S&P 500 stocks. An AI coding assistant can help at every stage: after installing FinLab Skill, it can provide code examples and debugging help.
The whole workflow looks like this; if performance falls short or the out-of-sample test fails, go back and adjust the features or the model:
1. Build features
The success of machine learning depends 80% on feature engineering. mlf.combine() from finlab.ml.feature (imported as mlf) merges several tables into one feature table: the index has two levels, (datetime, instrument), each feature is a column, and resample='W' resamples to weekly frequency.
Quarterly fundamentals are aligned to SEC filing dates, the day the data became available, so the features use only what was known at each date.
Fundamental features
from finlab import data
from finlab.ml import feature as mlf
data.set_market('us')
# Load S&P 500 price and fundamental data (quarterly data is aligned to SEC filing dates)
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')
total_assets = data.get('us_balance_sheet:total_assets')
# Ratios only where the denominator is positive
ttm_eps = eps.rolling(4).sum() # Trailing-12-month EPS
pb_ratio = market_value / equity[equity > 0]
pe_ratio = raw_close / ttm_eps[ttm_eps > 0]
roe = net_income / equity[equity > 0]
roa = net_income / total_assets[total_assets > 0]
# Combine into feature set
fundamental_features = mlf.combine(
{'pb': pb_ratio, 'pe': pe_ratio, 'roe': roe, 'roa': roa}, resample='W'
) # Resample to weekly frequency
print(fundamental_features.head())
# Index: (datetime, instrument), e.g. (2024-01-07, 'AAPL'); columns: pb, pe, roe, roaTechnical indicator features
Two ways: let mlf.ta_names() draw random indicator parameters to explore, or pick the indicators yourself.
# Method 1: Use random indicator parameters (explore the best settings)
names = [
name
for name in mlf.ta_names(n=3) # Three random parameter sets per indicator
if name.startswith(('talib.RSI__', 'talib.MACD__', 'talib.ATR__', 'talib.ADX__'))
]
with data.us_universe(index='SP500'):
ta_features = mlf.combine({'talib': mlf.ta(names)}, resample='W')
print(f"Random indicator features: {ta_features.shape[1]}")# Method 2: Specify particular technical indicators
with data.us_universe(index='SP500'):
rsi = data.indicator('RSI', timeperiod=14)
macd, macd_signal, macd_hist = data.indicator('MACD')
upper, middle, lower = data.indicator('BBANDS', timeperiod=20)
obv = data.indicator('OBV')
specific_ta = mlf.combine(
{
'rsi': rsi,
'macd': macd,
'bb_width': (upper - lower) / middle,
'obv': obv,
},
resample='W',
)
print(f"Specific indicator features: {specific_ta.shape[1]}")Custom features
Any table you can compute can be a feature, such as revenue momentum:
# Revenue-related features (quarterly, aligned to SEC filing dates)
with data.us_universe(index='SP500'):
revenue = data.get('us_income_statement:revenue')
custom_features = mlf.combine(
{
'rev_ma2': revenue.average(2), # Trailing 2-quarter revenue average
'rev_ma4': revenue.average(4), # Trailing 4-quarter revenue average
'rev_momentum': revenue.average(2) / revenue.average(4), # Revenue momentum
'rev_yoy': revenue.pct_change(4), # Revenue YoY growth
},
resample='W',
)
print(custom_features.head())Merge everything and drop sparse features
import numpy as np
# Merge all feature sets (the specific indicators from method 2)
all_features = mlf.combine(
{
'fundamental': fundamental_features,
'technical': specific_ta,
'custom': custom_features,
},
resample='W',
)
# Ratios such as revenue growth are infinite when the base is zero
all_features = all_features.replace([np.inf, -np.inf], np.nan)
print(f"Total feature count: {all_features.shape[1]}")
print(f"Row count: {all_features.shape[0]}")
# Check missing values
missing_ratio = all_features.isna().sum() / len(all_features)
print(
f"Missing value ratio:\n{missing_ratio[missing_ratio > 0.5]}"
) # Show features with > 50% missing
# Remove features with too many missing values
all_features = all_features.loc[:, missing_ratio < 0.5]
print(f"Filtered feature count: {all_features.shape[1]}")2. Generate labels
Labels define the prediction target. Every function in finlab.ml.label (imported as mll) takes features.index (MultiIndex) as its first argument and returns a Series with the same index as the features. Pick one:
| Label | Use it for |
|---|---|
return_percentage | Most commonly used, directly predicts returns |
excess_over_median | Predicts relative performance, reduces market-wide movement impact |
daytrading_percentage | Day trading strategies |
maximum_adverse_excursion | Risk management models |
period should match the strategy's rebalancing frequency: resample='W', period=1 predicts the 1-week return.
Future returns
from finlab.ml import label as mll
# Predict 1-week future return (most common)
label = mll.return_percentage(all_features.index, resample='W', period=1)
print(
label.head()
) # pd.Series with the same (datetime, instrument) index as the features
# Check label distribution
print(label.describe())Excess returns
# Excess return over market median for the same period
label_excess_median = mll.excess_over_median(all_features.index, resample='W', period=1)
# Excess return over market mean for the same period
label_excess_mean = mll.excess_over_mean(all_features.index, resample='W', period=1)
print(label_excess_median.describe())Other labels
# Day trading return (open-to-close change)
label_daytrading = mll.daytrading_percentage(all_features.index, resample='W')
# Risk metric: Maximum Adverse Excursion (max decline during holding period)
label_mae = mll.maximum_adverse_excursion(all_features.index, period=5)
# Risk metric: Maximum Favorable Excursion (max gain during holding period)
label_mfe = mll.maximum_favorable_excursion(all_features.index, period=5)
# Multi-period prediction (predict different time horizons)
label_1w = mll.return_percentage(all_features.index, resample='W', period=1)
label_2w = mll.return_percentage(all_features.index, resample='W', period=2)
label_4w = mll.return_percentage(all_features.index, resample='W', period=4)3. Split training and test data by time
Features (all_features) and labels (label) share the same MultiIndex (datetime, instrument) and can be split directly. First check that they line up:
# Select label
label = mll.return_percentage(all_features.index, resample='W', period=1)
# Check alignment
print(f"Feature row count: {len(all_features)}")
print(f"Label row count: {len(label)}")
print(f"Label NaN ratio: {label.isna().mean():.2%}")Then train on data before 2023-01-01 and test on data after, keeping only rows where every feature and the label are known:
# Use time-based splitting (strictly avoid data leakage)
is_train = all_features.index.get_level_values('datetime') < '2023-01-01'
# Keep rows where every feature and the label are known
complete = all_features.notna().all(axis=1) & label.notna()
X_train = all_features[complete & is_train]
y_train = label[complete & is_train]
X_test = all_features[~is_train].dropna()
print(
f"Training set: {len(X_train)} rows ({X_train.index.get_level_values(0).min()} ~ {X_train.index.get_level_values(0).max()})"
)
print(
f"Test set: {len(X_test)} rows ({X_test.index.get_level_values(0).min()} ~ {X_test.index.get_level_values(0).max()})"
)
print(f"Training feature shape: {X_train.shape}")4. Train a model
LightGBM
finlab.ml.qlib (imported as q) wraps each model in two calls, fit() and predict():
import finlab.ml.qlib as q
# Create and train LightGBM model
model = q.LGBModel()
model.fit(X_train, y_train)
print("Training complete!")Other models
They all work the same way. The linear model is fast and less prone to overfitting, which makes it a good baseline:
# XGBoost
model_xgb = q.XGBModel()
model_xgb.fit(X_train, y_train)
# CatBoost
model_cat = q.CatBoostModel()
model_cat.fit(X_train, y_train)
# Linear model (fast validation, less prone to overfitting)
model_linear = q.LinearModel()
model_linear.fit(X_train, y_train)
# Deep learning
model_dnn = q.DNNModel()
model_dnn.fit(X_train, y_train)
# List all available models
models = q.get_models()
print(list(models.keys()))Compare several models at once
Each model backtests its 30 highest predictions; compare annualized return and Sharpe ratio:
import finlab.ml.qlib as q
from finlab.backtest import sim
# Quick comparison of multiple models
results = {}
for name, ModelClass in [
('LightGBM', q.LGBModel),
('XGBoost', q.XGBModel),
('Linear', q.LinearModel),
]:
model = ModelClass()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
position = y_pred.is_largest(30)
report = sim(position, resample='W', name=f"ML {name}", upload=False)
results[name] = report
# Compare performance
for name, report in results.items():
stats = report.get_stats()
print(
f"{name}: annualized return {stats['cagr']:.2%}, Sharpe ratio {stats['daily_sharpe']:.2f}"
)5. Turn predictions into holdings
model.predict() returns a FinlabDataFrame (index = dates, columns = stock symbols), so FinlabDataFrame methods turn it straight into positions.
Generate predictions
# Predict on test set
y_pred = model.predict(X_test)
print(
y_pred.head()
) # FinlabDataFrame: index = dates, columns = tickers such as AAPL, MSFT
# Check prediction distribution
print(y_pred.stack().describe())Convert to positions
Three common approaches:
from finlab.backtest import sim
# Method 1: Top N stock selection (buy the N stocks with highest predictions)
position_topn = y_pred.is_largest(30)
# Method 2: Top 20% selection
position_quantile = y_pred > y_pred.quantile(0.8)
# Method 3: Weight the top 30 by their prediction rank (weights sum to 1 each date)
scores = y_pred.rank(axis=1, pct=True)[position_topn]
position_weighted = scores.div(scores.sum(axis=1), axis=0).fillna(0)
print(f"Top 30 strategy average holdings: {position_topn.sum(axis=1).mean():.1f}")
print(f"Top 20% strategy average holdings: {position_quantile.sum(axis=1).mean():.1f}")6. Backtest
Run the backtests
from finlab.backtest import sim
# Backtest Top 30 strategy
report_topn = sim(position_topn, resample='W', name="ML Top 30 Strategy", upload=False)
# Backtest weighted strategy
report_weighted = sim(
position_weighted, resample='W', name="ML Weighted Strategy", upload=False
)
# Display performance
report_topn.display()Compare the two ways of holding
import pandas as pd
stats_topn = report_topn.get_stats()
stats_weighted = report_weighted.get_stats()
comparison = pd.DataFrame(
{
'Top 30 Strategy': [
stats_topn['cagr'],
stats_topn['daily_sharpe'],
stats_topn['max_drawdown'],
stats_topn['win_ratio'],
],
'Weighted Strategy': [
stats_weighted['cagr'],
stats_weighted['daily_sharpe'],
stats_weighted['max_drawdown'],
stats_weighted['win_ratio'],
],
},
index=['Annualized Return', 'Sharpe Ratio', 'Max Drawdown', 'Win Rate'],
)
print(comparison)In-depth analysis
For how to read each analysis, see Strategy analysis modules:
# MAE/MFE analysis
report_topn.display_mae_mfe_analysis()
# Period stability
report_topn.run_analysis('PeriodStatsAnalysis')
# Alpha/Beta
report_topn.run_analysis('AlphaBetaAnalysis')7. Iterate on features and labels
If the results fall short, adjust in three directions: the number of features, the prediction period and the label type.
Use fewer features
# Strategy 1: Use fewer technical indicators
with data.us_universe(index='SP500'):
few_ta = mlf.ta(mlf.ta_names(n=1)[:20]) # Only take the first 20 indicators
features_small = mlf.combine(
{'pb': pb_ratio, 'pe': pe_ratio, 'roe': roe, 'talib': few_ta},
resample='W',
)
label_small = mll.return_percentage(features_small.index, resample='W', period=1)
is_train_small = features_small.index.get_level_values('datetime') < '2023-01-01'
model_v2 = q.LGBModel()
model_v2.fit(features_small[is_train_small], label_small[is_train_small])
y_pred_v2 = model_v2.predict(features_small[~is_train_small])
position_v2 = y_pred_v2.is_largest(30)
report_v2 = sim(position_v2, resample='W', name="ML V2 Reduced Features", upload=False)
report_v2.display()Change the prediction period
# Test different prediction periods
for period in [1, 2, 4]:
label_n = mll.return_percentage(all_features.index, resample='W', period=period)
model_n = q.LGBModel()
model_n.fit(X_train, label_n.loc[X_train.index])
y_pred_n = model_n.predict(X_test)
position_n = y_pred_n.is_largest(30)
report_n = sim(position_n, resample='W', name=f"ML Predict {period}W", upload=False)
stats = report_n.get_stats()
print(
f"Predict {period}W: annualized return {stats['cagr']:.2%}, Sharpe ratio {stats['daily_sharpe']:.2f}"
)Try a different label
# Compare return vs excess return labels
label_return = mll.return_percentage(all_features.index, resample='W', period=1)
label_excess = mll.excess_over_median(all_features.index, resample='W', period=1)
for label_name, label_data in [
('Return', label_return),
('Excess Return', label_excess),
]:
model_cmp = q.LGBModel()
model_cmp.fit(X_train, label_data.loc[X_train.index])
y_pred_cmp = model_cmp.predict(X_test)
position_cmp = y_pred_cmp.is_largest(30)
report_cmp = sim(position_cmp, resample='W', name=f"ML {label_name}", upload=False)
stats = report_cmp.get_stats()
print(
f"{label_name}: annualized return {stats['cagr']:.2%}, Sharpe ratio {stats['daily_sharpe']:.2f}"
)8. Deploy to live trading
Train on all history and save the model
Before going live, retrain on all historical data, save the model, and use it to predict the latest holdings:
from finlab import data
from finlab.ml import feature as mlf, label as mll
import finlab.ml.qlib as q
import pickle
data.set_market('us')
def build_features():
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')
talib = mlf.ta(mlf.ta_names(n=1)[:20])
ttm_eps = eps.rolling(4).sum()
return mlf.combine(
{
'pb': market_value / equity[equity > 0],
'pe': raw_close / ttm_eps[ttm_eps > 0],
'roe': net_income / equity[equity > 0],
'talib': talib,
},
resample='W',
)
# 1. Train the full model (using all historical data)
features = build_features()
label = mll.return_percentage(features.index, resample='W', period=1)
model = q.LGBModel()
model.fit(features, label)
# 2. Save the model
with open('ml_model.pkl', 'wb') as f:
pickle.dump(model, f)
# 3. Load model and predict latest positions
with open('ml_model.pkl', 'rb') as f:
loaded_model = pickle.load(f)
y_pred = loaded_model.predict(features)
# 4. Get latest positions
position = y_pred.is_largest(30)
latest_position = position.iloc[-1]
latest_position = latest_position[latest_position > 0].sort_values(ascending=False)
print("Latest position recommendations:")
print(latest_position)Rebalance automatically every week
Each week, rebuild the features, load the model and predict, then use sim to make a report, size the positions and place the orders. This example trades through a Charles Schwab account; SchwabAccount() reads SCHWAB_API_KEY, SCHWAB_SECRET and SCHWAB_TOKEN_PATH from the environment. See Live trading for other brokers.
from finlab.backtest import sim
from finlab.online.order_executor import OrderExecutor, Position
from finlab.online.schwab_account import SchwabAccount
# Create a script to run weekly (every Monday)
def weekly_rebalance():
# Recalculate features (build_features() from the previous section)
features = build_features()
# Load model and predict
with open('ml_model.pkl', 'rb') as f:
model = pickle.load(f)
y_pred = model.predict(features)
position = y_pred.is_largest(30)
# Use sim to generate report
report = sim(position, resample='W', upload=False)
# Execute orders
account = SchwabAccount()
target = Position.from_report(report, fund=100000) # USD 100,000
executor = OrderExecutor(target, account=account)
executor.create_orders()
# Use cron or a scheduling tool to run weekly_rebalance() periodicallyOnce live, retrain the model periodically (for example, quarterly) and monitor how live results diverge from the backtest; to compare real fills with the strategy's targets, see Real-trade review.
Complete code
From features to backtest in one run:
# =============================================================================
# Machine Learning Strategy Complete Example
# =============================================================================
from finlab import data
from finlab.ml import feature as mlf
from finlab.ml import label as mll
import finlab.ml.qlib as q
from finlab.backtest import sim
# 1. Feature Engineering (S&P 500 stocks)
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')
revenue = data.get('us_income_statement:revenue')
talib = mlf.ta(mlf.ta_names(n=1)[:20])
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
features = mlf.combine(
{
'pb': pb,
'pe': pe,
'rev_ma2': revenue.average(2),
'rev_ma4': revenue.average(4),
'talib': talib,
},
resample='W',
)
# 2. Label Generation
label = mll.return_percentage(features.index, resample='W', period=1)
# 3. Data Splitting
is_train = features.index.get_level_values('datetime') < '2023-01-01'
X_train = features[is_train]
y_train = label[is_train]
X_test = features[~is_train]
# 4. Model Training
model = q.LGBModel()
model.fit(X_train, y_train)
# 5. Prediction & Positions
y_pred = model.predict(X_test)
position = y_pred.is_largest(30)
# 6. Backtest
report = sim(position, resample='W', name="ML Strategy", upload=False)
report.display()
# 7. Analysis
report.display_mae_mfe_analysis()
report.run_analysis('PeriodStatsAnalysis')
print("Done!")Checklist
| Stage | Check |
|---|---|
| Feature engineering | Use diverse feature sources (fundamental, technical, custom); use mlf.combine() to unify merging, ensuring MultiIndex alignment; check and handle missing values; control feature count (too many leads to overfitting) |
| Label generation | Use mll.return_percentage() and similar functions, passing features.index; resample should match the features; the prediction period (period) should be reasonable (too short = noisy, too long = hard to predict) |
| Model training | Use wrapper classes like q.LGBModel(), with fit() + predict(); split train/test by time (not randomly); start with a simple model (LinearModel) to establish a baseline |
| Backtest validation | predict() returns a FinlabDataFrame; use is_largest() to convert to positions; out-of-sample testing is mandatory; run in-depth analysis (MAE/MFE, period stability) |
| Live deployment | Retrain the model periodically (e.g., quarterly); monitor divergence between live and backtest performance; set up performance alert mechanisms |
Common errors and how to check for them
Add a check at each stage to catch problems early.
Feature engineering: resample mismatch, too many missing values, look-ahead bias
Common errors:
resamplemismatch between features and labels- Too many missing values resulting in insufficient training data
- Look-ahead bias (using future data to predict the past)
try:
# 1. Build features
features = mlf.combine(
{'pb': pb, 'pe': pe, 'rev_ma2': revenue.average(2)}, resample='W'
)
if features.empty:
raise ValueError("❌ Feature DataFrame is empty")
# 2. Check missing value ratio
missing_ratio = features.isna().sum() / len(features)
high_missing_cols = missing_ratio[missing_ratio > 0.3].index.tolist()
if high_missing_cols:
print(
f"⚠️ Warning: the following features have > 30% missing values: {high_missing_cols}"
)
print("Suggestion: drop these features or apply forward fill")
# 3. Check date range
print(
f"Feature date range: {features.index.get_level_values(0).min()} ~ {features.index.get_level_values(0).max()}"
)
# 4. Check feature count
num_features = features.shape[1]
if num_features > 500:
print(f"⚠️ Warning: too many features ({num_features}), may cause overfitting")
print("Suggestion: prefer < 200 features")
print(
f"✅ Feature engineering complete: {num_features} features, {len(features)} rows"
)
except KeyError as e:
print(f"❌ Invalid data table name: {e}")
print(
"Please visit https://studio.finlab.finance/en/database to confirm the correct name"
)
except ValueError as e:
print(f"❌ Feature validation failed: {e}")For data download errors, see "Common errors and solutions" in Load data.
Label generation: resample mismatch, wrong index, unreasonable period
Common errors:
resamplemismatch between labels and features- Passing incorrect
index(should passfeatures.index) - Unreasonable prediction period setting
# Generate labels
label = mll.return_percentage(features.index, resample='W', period=1)
# Check label distribution
print("Label statistics:")
print(label.describe())
# Check label missing values
nan_ratio = label.isna().mean()
if nan_ratio > 0.1:
print(f"⚠️ Warning: label missing ratio {nan_ratio:.1%} > 10%")
print("Likely cause: prediction period too long, recent rows have no label")
print(f"✅ Label generation complete: {len(label)} rows")Model training: too little data, random split, overfitting
Common errors:
- Insufficient training data (< 1000 rows)
- Using random split instead of time-based split
- Overfitting (test set performance much worse than training set)
# Split train/test sets
is_train = features.index.get_level_values('datetime') < '2023-01-01'
X_train = features[is_train]
y_train = label[is_train]
X_test = features[~is_train]
# 1. Check data volume
print(f"Training set: {len(X_train)} rows")
print(f"Test set: {len(X_test)} rows")
if len(X_train) < 1000:
print("⚠️ Warning: insufficient training data (< 1000 rows)")
print("Suggestion: extend the historical range or lower the resample frequency")
if len(X_test) < 100:
print("⚠️ Warning: too few test rows (< 100)")
# 2. Check date ordering
train_last = X_train.index.get_level_values(0).max()
test_first = X_test.index.get_level_values(0).min()
if train_last >= test_first:
raise ValueError(
f"❌ Training and test set dates overlap!\n"
f" Training set last date: {train_last}\n"
f" Test set first date: {test_first}\n"
f" This will cause data leakage"
)
print(f"✅ Dataset split is correct")
# 3. Model training
try:
model = q.LGBModel()
model.fit(X_train, y_train)
print(f"✅ Model training complete")
except Exception as e:
print(f"❌ Model training failed: {e}")
print("Please check:")
print("1. Whether features contain NaN or Inf")
print("2. Whether labels are numeric")
print("3. Whether Qlib dependencies are installed (pip install 'finlab[qlib]')")
raisePrediction and backtest: all-NaN predictions, wrong position format, no trades
Common errors:
- Prediction results are all NaN
- Position DataFrame format is incorrect
- Backtest has no trade records
# 1. Prediction
y_pred = model.predict(X_test)
if y_pred.isna().all().all():
raise ValueError("❌ All predictions are NaN")
# Check prediction distribution
print(f"Prediction range: {y_pred.min().min():.4f} ~ {y_pred.max().max():.4f}")
print(f"Prediction mean: {y_pred.stack().mean():.4f}")
# 2. Generate positions
position = y_pred.is_largest(30)
if position.empty:
raise ValueError("❌ Position DataFrame is empty")
holding_count = position.sum(axis=1).mean()
if holding_count < 10:
print(
f"⚠️ Warning: average holding count {holding_count:.1f} < 10, possibly too few"
)
print(f"✅ Positions generated successfully: average {holding_count:.1f} holdings")
# 3. Backtest
try:
report = sim(position, resample='W', name="ML Strategy", upload=False)
print(f"✅ Backtest succeeded")
stats = report.get_stats()
print(f" Annualized return: {stats['cagr']:.2%}")
print(f" Sharpe ratio: {stats['daily_sharpe']:.2f}")
except Exception as e:
print(f"❌ Backtest failed: {e}")
print("Please check:")
print("1. Whether position.index is a DatetimeIndex")
print("2. Whether position.columns are stock symbols")
raiseNext steps
- Machine learning The shorter
finlab.mltutorial: install, features, labels and models. - finlab.ml API Full parameters for the feature, label and model functions.
- From research to live trading The full path for a rule-based strategy, from idea to deployment.
- Risk management Position caps, stops and drawdown control.
Related pages: Strategy analysis modules, Live trading, FAQ.
Was this page helpful?