Machine Learning Techniques for Trading › Supervised Learning Algorithms

Michael BrenndoerferJanuary 2, 202618 min read
Quantitative FinanceAll chapters
Part 13 of 15Machine Learning and Alternative Data
Chapter 69Machine Learning Techniques for Trading › Supervised Learning Algorithms
Reading section 3 of 10 · Supervised Learning Algorithms · 18 minSection 3 of 10 · 18 min

Book contents

Section 3 of 10 · 18 min read

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:

y=β0+β1x1+β2x2+⋯+βpxp+ϵy = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_p x_p + \epsilon

where:

  • yy: dependent variable (target)
  • β0\beta_0: y-intercept
  • βi\beta_i: regression coefficient for feature ii
  • xix_i: independent variable (feature) ii
  • pp: total number of features
  • ϵ\epsilon: error term (residual)

To understand what this formula truly represents, consider each coefficient βi\beta_i as quantifying the expected change in the target variable when feature xix_i increases by one unit, holding all other features constant. The intercept β0\beta_0 captures the baseline prediction when all features equal zero. The error term ϵ\epsilon 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:

P(y=1∣x)=11+e−(β0+βTx)P(y = 1 | \mathbf{x}) = \frac{1}{1 + e^{-(\beta_0 + \boldsymbol{\beta}^T \mathbf{x})}}

where:

  • P(y=1∣x)P(y = 1 | \mathbf{x}): probability of the positive class given features x\mathbf{x}
  • β0+βTx\beta_0 + \boldsymbol{\beta}^T \mathbf{x}: the log-odds (linear combination) of the features
  • β0\beta_0: bias term
  • β\boldsymbol{\beta}: vector of weights
  • x\mathbf{x}: 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 (0,1)(0, 1) 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.

Out[2]:
Visualization
Line chart of the sigmoid function from z = -6 to z = 6, showing the S-shaped curve approaching 0 on the left, crossing 0.5 at z = 0, and approaching 1 on the right.
The logistic (sigmoid) function across the range [-6, 6]. The curve squashes real-valued inputs into the (0, 1) interval, with the steepest gradient and decision boundary occurring at z = 0.
In[3]:
Code
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"]
In[4]:
Code
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}")
Out[4]:
Console
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

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 p1,p2,…,pKp_1, p_2, \ldots, p_K, the Gini impurity is:

G=1−∑k=1Kpk2G = 1 - \sum_{k=1}^{K} p_k^2

where:

  • GG: Gini impurity measure
  • pkp_k: probability of class kk
  • KK: total number of classes

Pure nodes (all one class) have G=0G = 0.

To see why this formula makes sense, consider a binary classification problem. If a node contains only positive examples (p1=1,p2=0p_1 = 1, p_2 = 0), then G=1−12−02=0G = 1 - 1^2 - 0^2 = 0, indicating perfect purity. Conversely, if the node contains an equal mix (p1=0.5,p2=0.5p_1 = 0.5, p_2 = 0.5), then G=1−0.25−0.25=0.5G = 1 - 0.25 - 0.25 = 0.5, 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:

Best Split=arg⁡min⁡j,t[nLn⋅Impurity(L)+nRn⋅Impurity(R)]\text{Best Split} = \arg\min_{j, t} \left[ \frac{n_L}{n} \cdot \text{Impurity}(L) + \frac{n_R}{n} \cdot \text{Impurity}(R) \right]

where:

  • jj: feature index selected for the split
  • tt: threshold value for feature jj
  • L,RL, R: left and right child nodes
  • nL,nRn_L, n_R: number of samples in the left and right child nodes
  • nn: total number of samples in the current node
  • Impurity(⋅)\text{Impurity}(\cdot): 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 jj and all possible thresholds tt to find the combination that minimizes this weighted impurity.

In[5]:
Code
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)
Out[6]:
Console
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 BB independent predictions, each with variance σ2\sigma^2, their average has variance σ2/B\sigma^2 / B. 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:

y^=1B∑b=1BTb(x)\hat{y} = \frac{1}{B} \sum_{b=1}^{B} T_b(\mathbf{x})

where:

  • y^\hat{y}: aggregated ensemble prediction
  • BB: total number of trees in the forest
  • Tb(x)T_b(\mathbf{x}): prediction generated by the bb-th tree
  • x\mathbf{x}: 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.

In[7]:
Code
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]
Out[8]:
Console
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:

  1. Initialize with the mean value:
F0(x)=yˉF_0(\mathbf{x}) = \bar{y}

where:

  • F0(x)F_0(\mathbf{x}): initial prediction (mean value)
  • yˉ\bar{y}: mean of the target values
  • x\mathbf{x}: 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.

  1. Iterate for m=1,2,…,Mm = 1, 2, \ldots, M:

    First, compute the pseudo-residuals, which represent the errors not yet explained by the current ensemble:

ri(m)=yi−Fm−1(xi)r_i^{(m)} = y_i - F_{m-1}(\mathbf{x}_i)

where:

  • ri(m)r_i^{(m)}: pseudo-residual for sample ii, representing the unexplained part of the target
  • yiy_i: target value for sample ii
  • Fm−1(xi)F_{m-1}(\mathbf{x}_i) or Fm−1(x)F_{m-1}(\mathbf{x}): prediction from the previous iteration (the baseline we are improving)
  • xi\mathbf{x}_i: feature vector for sample ii

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) hm(x)h_m(\mathbf{x}) 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:

Fm(x)=Fm−1(x)+ν⋅hm(x)F_m(\mathbf{x}) = F_{m-1}(\mathbf{x}) + \nu \cdot h_m(\mathbf{x})

where:

  • Fm(x)F_m(\mathbf{x}): ensemble prediction after iteration mm
  • Fm−1(x)F_{m-1}(\mathbf{x}): prediction from the previous iteration
  • ν\nu: learning rate parameter (scales the contribution of each tree to prevent overfitting)
  • hm(x)h_m(\mathbf{x}): weak learner (tree) fitted to the pseudo-residuals
  • x\mathbf{x}: input feature vector

The learning rate ν\nu (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.

In[9]:
Code
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]
Out[10]:
Console
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.
Out[11]:
Visualization
Line chart showing training log loss decreasing from 0.66 at the first boosting iteration to 0.35 at iteration 100, with the endpoints annotated.
In-sample log loss as trees are added to the gradient boosting ensemble. Loss falls from about 0.66 after the first tree to about 0.35 after 100 trees; the continued decline reflects training fit, not out-of-sample performance.

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:

y^=g2(W2g1(W1x+b1)+b2)\hat{y} = g_2\left( \mathbf{W}_2 g_1(\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1) + \mathbf{b}_2 \right)

where:

  • W1x+b1\mathbf{W}_1 \mathbf{x} + \mathbf{b}_1: linear transformation of the input
  • g1(… )g_1(\dots): activation of the hidden layer, creating nonlinear features
  • W1,W2\mathbf{W}_1, \mathbf{W}_2: weight matrices for the hidden and output layers
  • b1,b2\mathbf{b}_1, \mathbf{b}_2: bias vectors
  • g1,g2g_1, g_2: activation functions (e.g., ReLU, sigmoid)
  • y^\hat{y}: network output (prediction)
  • x\mathbf{x}: input feature vector

To understand this formula, trace the flow of information from input to output. The input vector x\mathbf{x} first undergoes a linear transformation by the weight matrix W1\mathbf{W}_1, which projects it into a higher-dimensional space, and bias b1\mathbf{b}_1 shifts this projection. The activation function g1g_1 then introduces nonlinearity, allowing the network to learn complex patterns that linear models cannot capture. This transformed representation passes through another linear transformation (W2\mathbf{W}_2, b2\mathbf{b}_2) and final activation g2g_2 to produce the output.

The hidden layer activation, typically ReLU defined as g(z)=max⁡(0,z)g(z) = \max(0, z), 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.

Out[12]:
Visualization
Line chart of the ReLU function from z = -4 to z = 4, flat at zero for negative inputs and linearly rising for positive inputs.
The Rectified Linear Unit (ReLU) activation function, which outputs zero for negative inputs and passes positive values unchanged. This piecewise linear nature allows efficient computation while providing the non-linearity required for deep learning.
Line chart of the sigmoid function from z = -4 to z = 4, with an S-shaped curve rising from near 0 to near 1 and crossing 0.5 at z = 0.
The sigmoid activation function mapping inputs to a probabilistic range. The function is centered at 0.5 when the input is zero, and gives the smooth transition required for binary classification output layers.
In[13]:
Code
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]
Out[14]:
Console
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.