Machine Learning Techniques for Trading › Supervised Learning Algorithms
Quantitative FinanceAll chapters Machine Learning Techniques for Trading › Supervised Learning AlgorithmsOne section · Sections in this chapter3 / 10 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Let's examine the supervised learning algorithms most commonly employed in quantitative finance, building from simple linear models to more complex ensemble methods and neural networks.
Linear and Logistic Regression Revisited
Linear regression, which we covered extensively in Part III's chapter on regression analysis, remains a foundational tool. The intuition behind linear regression is straightforward: we assume that the target variable can be expressed as a weighted sum of the input features, plus some random noise that captures everything our model cannot explain. This assumption, while restrictive, proves remarkably useful in many financial applications where relationships between variables are approximately linear over relevant ranges.
For predicting continuous targets like returns, we model:
where:
- : dependent variable (target)
- : y-intercept
- : regression coefficient for feature
- : independent variable (feature)
- : total number of features
- : error term (residual)
To understand what this formula truly represents, consider each coefficient as quantifying the expected change in the target variable when feature increases by one unit, holding all other features constant. The intercept captures the baseline prediction when all features equal zero. The error term acknowledges that our linear model cannot perfectly explain every observation; it represents the inherent randomness in financial markets and the influence of factors we have not included.
Despite its simplicity, linear regression offers important advantages: interpretability, computational efficiency, and well-understood statistical properties. Its coefficients tell us exactly how each feature influences the prediction. When a portfolio manager asks why the model is bullish on a particular stock, we can point to specific features and their associated weights, and gives a clear narrative that connects inputs to outputs.
For binary classification tasks, such as predicting whether returns will be positive or negative, we need a different approach. The challenge is that linear regression produces unbounded outputs, yet we need probabilities that fall between zero and one. Logistic regression solves this elegantly by wrapping the linear combination inside a special transformation called the logistic (or sigmoid) function. Logistic regression models the probability of the positive class:
where:
- : probability of the positive class given features
- : the log-odds (linear combination) of the features
- : bias term
- : vector of weights
- : vector of input features
The logistic function converts a linear score into a probability. Inside the exponential, we still have our familiar linear combination of features. However, the logistic function squashes this linear combination into the interval, giving us probability estimates rather than unbounded predictions. When the linear combination is large and positive, the probability approaches 1; when it is large and negative, the probability approaches 0; and when the linear combination equals zero, the probability equals exactly 0.5.

import warnings
import numpy as np
warnings.filterwarnings("ignore")
# Generate synthetic financial data
np.random.seed(42)
n_samples = 500
# Features: momentum, mean reversion signal, volatility
momentum = np.random.randn(n_samples)
mean_rev = np.random.randn(n_samples)
volatility = np.abs(np.random.randn(n_samples)) + 0.5
# Target: next-day return (continuous)
returns = (
0.01 * momentum
- 0.005 * mean_rev
+ 0.002 * volatility
+ 0.02 * np.random.randn(n_samples)
)
# Target: direction (binary)
direction = (returns > 0).astype(int)
# Create feature matrix
X = np.column_stack([momentum, mean_rev, volatility])
feature_names = ["Momentum", "Mean_Reversion", "Volatility"]from sklearn.linear_model import LinearRegression, LogisticRegression
# Fit linear regression for return prediction
lin_reg = LinearRegression()
lin_reg.fit(X, returns)
# Fit logistic regression for direction prediction
log_reg = LogisticRegression()
log_reg.fit(X, direction)
print("Linear Regression Coefficients:")
for name, coef in zip(feature_names, lin_reg.coef_):
print(f" {name}: {coef:.6f}")
print("\nLogistic Regression Coefficients:")
for name, coef in zip(feature_names, log_reg.coef_[0]):
print(f" {name}: {coef:.4f}")Linear Regression Coefficients: Momentum: 0.011258 Mean_Reversion: -0.005339 Volatility: 0.002418 Logistic Regression Coefficients: Momentum: 0.9647 Mean_Reversion: -0.4816 Volatility: 0.1845
The coefficients reveal the relationship between features and targets. Momentum shows a positive relationship with returns (as expected from our data generation), while the mean reversion signal has a negative coefficient, consistent with the contrarian nature of mean reversion strategies.
Key Parameters
- fit_intercept: Whether to calculate the intercept (bias) term. In finance, this represents the alpha or base probability.
- C: Inverse of regularization strength for logistic regression (default 1.0). Smaller values specify stronger regularization to prevent overfitting.
- penalty: The norm used in the penalization (e.g., 'l2').
Decision Trees
Decision trees partition the feature space through a sequence of binary splits, creating a hierarchical structure that naturally captures nonlinear relationships and feature interactions. The fundamental idea is intuitive: at each step, the algorithm asks a yes-or-no question about one of the features (such as "Is momentum greater than 0.5?"), and based on the answer, it routes the observation down one of two branches. This process continues recursively until the observations reach terminal nodes, called leaves, where the final prediction is made.
At each internal node, the tree selects a feature and threshold that best separates the data according to some criterion. For classification tasks, this criterion is typically Gini impurity while for regression tasks, mean squared error is commonly used. This approach automatically discovers which features matter most and identifies the thresholds that create the most informative splits.
Gini impurity provides a measure of how often a randomly chosen element would be incorrectly classified if we assigned labels according to the distribution of classes in that node. Intuitively, a node where all observations belong to a single class is "pure" and has zero impurity, while a node with an even mix of classes has maximum impurity.
For a node with class probabilities , the Gini impurity is:
where:
- : Gini impurity measure
- : probability of class
- : total number of classes
Pure nodes (all one class) have .
To see why this formula makes sense, consider a binary classification problem. If a node contains only positive examples (), then , indicating perfect purity. Conversely, if the node contains an equal mix (), then , the maximum possible impurity for two classes.
The tree-building process recursively finds the optimal split at each node by evaluating all possible features and thresholds, selecting the combination that most reduces the weighted impurity of the resulting child nodes:
where:
- : feature index selected for the split
- : threshold value for feature
- : left and right child nodes
- : number of samples in the left and right child nodes
- : total number of samples in the current node
- : function measuring node impurity (e.g., Gini)
This formula captures the essence of the splitting decision. We weight each child node's impurity by the proportion of samples it receives, so we prioritize splits that create large, pure groups rather than small pockets of purity. The algorithm searches over all features and all possible thresholds to find the combination that minimizes this weighted impurity.
from sklearn.tree import DecisionTreeClassifier
# Fit a decision tree
tree = DecisionTreeClassifier(max_depth=3, random_state=42)
tree.fit(X, direction)
# Make predictions
tree_predictions = tree.predict(X)Decision Tree Training Accuracy: 0.7320 Feature Importances: Momentum: 0.6768 Mean_Reversion: 0.2848 Volatility: 0.0384
Random forests improve on individual decision trees by training an ensemble of trees, each on a bootstrapped sample of the data with a random subset of features considered at each split. The core insight draws from a fundamental principle in statistics: while individual estimates may be noisy, averaging many independent estimates reduces that noise.
Consider the variance reduction that comes from averaging. If we have independent predictions, each with variance , their average has variance . Of course, trees trained on the same data are not truly independent. This is where the clever design of random forests comes into play. By introducing randomness in both the data (bootstrap sampling) and features (random feature subsets at each split), the individual trees become decorrelated. Even though each tree may be a somewhat biased estimator of the true function, their average prediction is more stable than any single tree could achieve.
The final prediction aggregates across all trees:
where:
- : aggregated ensemble prediction
- : total number of trees in the forest
- : prediction generated by the -th tree
- : input feature vector
For classification tasks, this aggregation typically takes the form of majority voting: each tree casts a vote for its predicted class, and the class receiving the most votes becomes the ensemble prediction. For regression, we simply average the numerical predictions. The key insight is that averaging reduces variance without increasing bias. By introducing randomness in both the data (bootstrap sampling) and features (random feature subsets), the individual trees become decorrelated, and their average prediction is more stable than any single tree.
from sklearn.ensemble import RandomForestClassifier
# Fit a random forest
rf = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42)
rf.fit(X, direction)
# Make predictions
rf_predictions = rf.predict(X)
rf_probabilities = rf.predict_proba(X)[:, 1]Random Forest Training Accuracy: 0.7980 Feature Importances: Momentum: 0.4882 Mean_Reversion: 0.3078 Volatility: 0.2039
Random forests often perform well with relatively little hyperparameter tuning. They handle mixed feature types, provide probability estimates through voting, and offer feature importance measures. Their main limitations are computational cost (training many trees) and reduced interpretability compared to single trees.
Key Parameters
- n_estimators: The number of trees in the forest. More trees increase stability but also computational cost.
- max_depth: The maximum depth of each tree.
- max_features: The number of features to consider when looking for the best split.
Gradient Boosting
While random forests build trees independently and average their predictions, gradient boosting takes a fundamentally different approach by building trees sequentially, with each tree correcting the errors of the previous ensemble. The conceptual difference is significant: random forests reduce variance through averaging, while gradient boosting reduces bias through iterative refinement.
The method works by fitting each new tree to the negative gradient of the loss function. To understand this intuitively, think of the gradient as pointing in the direction of steepest ascent. By moving in the opposite direction (the negative gradient), we descend toward lower loss values. In the case of squared error loss, this negative gradient equals the residuals, the differences between actual values and current predictions. In essence, each new tree learns to predict the mistakes of the ensemble so far.
For regression with squared error loss, the algorithm proceeds in steps:
- Initialize with the mean value:
where:
- : initial prediction (mean value)
- : mean of the target values
- : input feature vector
Starting with the mean is a natural choice because, in the absence of any other information, the mean minimizes squared error. This provides a sensible baseline from which the algorithm can begin its iterative improvement.
-
Iterate for :
First, compute the pseudo-residuals, which represent the errors not yet explained by the current ensemble:
where:
- : pseudo-residual for sample , representing the unexplained part of the target
- : target value for sample
- or : prediction from the previous iteration (the baseline we are improving)
- : feature vector for sample
These pseudo-residuals tell us how much each observation's actual value differs from our current prediction. A large positive residual means we are underestimating; a large negative residual means we are overestimating. The next tree will focus on learning these patterns.
Next, fit a weak learner (tree) to these residuals. This tree learns to predict where the current ensemble is making mistakes.
Finally, update the ensemble prediction by adding the new tree's contribution:
where:
- : ensemble prediction after iteration
- : prediction from the previous iteration
- : learning rate parameter (scales the contribution of each tree to prevent overfitting)
- : weak learner (tree) fitted to the pseudo-residuals
- : input feature vector
The learning rate (typically 0.01 to 0.1) controls how much each tree contributes. This parameter represents a deliberate choice to learn slowly: rather than fully correcting all residuals at once, we make small, incremental adjustments. This regularization technique, known as shrinkage, helps prevent overfitting. Smaller values require more trees but generally achieve better performance by allowing for finer-grained corrections. Modern implementations like XGBoost and LightGBM add sophisticated optimizations including regularization, efficient handling of sparse data, and parallel processing.
from sklearn.ensemble import GradientBoostingClassifier
# Fit gradient boosting
gb = GradientBoostingClassifier(
n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42
)
gb.fit(X, direction)
# Make predictions
gb_predictions = gb.predict(X)
gb_probabilities = gb.predict_proba(X)[:, 1]Gradient Boosting Training Accuracy: 0.8900 Feature Importances: Momentum: 0.4881 Mean_Reversion: 0.2898 Volatility: 0.2221
Gradient boosting often achieves the best predictive performance among tree-based methods, but it requires more careful tuning and is more prone to overfitting. The sequential nature also makes training slower than random forests, though prediction remains fast.
Key Parameters
- n_estimators: The number of boosting stages to perform.
- learning_rate: Shrinks the contribution of each tree. Lower values require more trees.
- max_depth: Maximum depth of the individual regression estimators.

Neural Networks
Neural networks model complex nonlinear relationships through layers of interconnected nodes. The fundamental building block is the artificial neuron, which computes a weighted sum of its inputs, adds a bias term, and passes the result through a nonlinear activation function. By stacking many such neurons in layers and connecting them together, neural networks can approximate arbitrarily complex functions.
The architecture of a neural network determines its capacity to learn. A feedforward neural network with one hidden layer computes:
where:
- : linear transformation of the input
- : activation of the hidden layer, creating nonlinear features
- : weight matrices for the hidden and output layers
- : bias vectors
- : activation functions (e.g., ReLU, sigmoid)
- : network output (prediction)
- : input feature vector
To understand this formula, trace the flow of information from input to output. The input vector first undergoes a linear transformation by the weight matrix , which projects it into a higher-dimensional space, and bias shifts this projection. The activation function then introduces nonlinearity, allowing the network to learn complex patterns that linear models cannot capture. This transformed representation passes through another linear transformation (, ) and final activation to produce the output.
The hidden layer activation, typically ReLU defined as , introduces nonlinearity. Without these nonlinear activations, stacking multiple layers would be equivalent to a single linear transformation, no matter how many layers we added. The output activation depends on the task: sigmoid for binary classification (squashing outputs to probabilities), linear for regression (allowing unbounded predictions).
Training proceeds by backpropagation, a systematic application of the chain rule to compute gradients of the loss function with respect to all weights. These gradients indicate how to adjust each weight to reduce the prediction error. Optimization algorithms like stochastic gradient descent, Adam, or RMSprop then update the weights iteratively, gradually improving the network's predictions.


from sklearn.neural_network import MLPClassifier
from sklearn.preprocessing import StandardScaler
# Scale features (important for neural networks)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Fit a simple neural network
nn = MLPClassifier(
hidden_layer_sizes=(32, 16),
activation="relu",
max_iter=500,
random_state=42,
)
nn.fit(X_scaled, direction)
# Make predictions
nn_predictions = nn.predict(X_scaled)
nn_probabilities = nn.predict_proba(X_scaled)[:, 1]Neural Network Training Accuracy: 0.7340 Architecture: (32, 16) Number of iterations: 500
The model architecture consists of two hidden layers with 32 and 16 neurons respectively, which converged after the displayed number of iterations.
Neural networks can approximate any continuous function given sufficient capacity, making them extremely flexible. However, this flexibility comes with costs: they require more data, careful hyperparameter tuning, and feature scaling. They're also less interpretable than tree-based methods, acting largely as "black boxes." For tabular financial data with moderate sample sizes, gradient boosting often outperforms neural networks, but neural networks excel when working with alternative data like images or text.
Key Parameters
- hidden_layer_sizes: Tuple defining the number of neurons in each hidden layer. Determines the model's capacity and complexity.
- activation: Activation function for the hidden layers (e.g., 'relu'). 'relu' is standard for deep networks.
- max_iter: Maximum number of iterations (epochs). Ensure this is sufficient for convergence.
- alpha: L2 penalty (regularization term) parameter. Higher values force weights to be smaller, reducing overfitting.