1. Learning Objectives

By the end of this lesson, you will be able to:

  • Understand the principles of online learning and its application to financial time series.

  • Implement Stochastic Gradient Descent (SGD) and its variants for online learning.

  • Apply online learning to portfolio optimisation and high-frequency trading.

  • Understand the mathematical foundations of transfer learning and domain adaptation.

  • Apply fine-tuning to pre-trained models for financial applications.

  • Implement domain adaptation techniques for regime changes.

  • Understand the limitations and pitfalls of transfer learning in finance.

  • Apply meta-learning (learning to learn) for rapid adaptation to new markets.


2. Online Learning – Learning from Streaming Data

2.1 The Online Learning Framework

In online learning, data arrives sequentially. The model makes predictions and receives feedback immediately.

Algorithm:

text
For t = 1, 2, ..., T:
    1. Receive input x_t
    2. Make prediction ŷ_t = f_t(x_t)
    3. Receive true label y_t
    4. Observe loss l_t(ŷ_t, y_t)
    5. Update model to f_{t+1}

Key Difference from Batch Learning: Online learning must adapt to changing data distributions (non-stationarity).

2.2 Mathematical Formulation

The goal is to minimise cumulative regret:
Regret_T = Σ_{t=1}^{T} l_t(w_t, x_t) - min_{w ∈ W} Σ_{t=1}^{T} l_t(w, x_t)

2.3 Online Gradient Descent (OGD)
w_{t+1} = w_t - η_t ∇l_t(w_t)

Regret Bound: For convex losses with Lipschitz gradients:
Regret_T ≤ O(sqrt(T))

2.4 Online SGD with Financial Data

text
def online_sgd_portfolio(X, y, learning_rate=0.1, epochs=1):
    """
    Online SGD for portfolio optimisation.
    """
    # Initialise weights
    w = np.ones(X.shape[1]) / X.shape[1]

    for t in range(len(X)):
        # Get current data point
        x_t = X.iloc[t].values
        y_t = y.iloc[t]

        # Make prediction
        y_pred = np.dot(w, x_t)

        # Compute loss (MSE)
        loss = (y_t - y_pred)**2

        # Compute gradient
        gradient = -2 * x_t * (y_t - y_pred)

        # Update weights
        w = w - learning_rate * gradient

        # Project onto simplex (weights sum to 1, w_i >= 0)
        w = project_to_simplex(w)

    return w

2.5 Online Portfolio Optimisation

text
class OnlinePortfolioOptimiser:
    def __init__(self, n_assets, learning_rate=0.01, method='sd'):
        """
        method: 'sd' (semi-definite), 'eg' (exponentiated gradient)
        """
        self.n_assets = n_assets
        self.lr = learning_rate
        self.method = method

        # Initialise weights
        self.weights = np.ones(n_assets) / n_assets

    def update(self, returns):
        """
        Update portfolio weights using online learning.
        """
        # Current return
        portfolio_return = np.dot(self.weights, returns)

        if self.method == 'sd':
            # Semi-definite online learning
            gradient = -returns / (portfolio_return + 1e-8)
            self.weights = self.weights - self.lr * gradient
            self.weights = self._project_simplex(self.weights)

        elif self.method == 'eg':
            # Exponentiated gradient (for non-negative weights)
            gradient = -returns / (portfolio_return + 1e-8)
            self.weights = self.weights * np.exp(-self.lr * gradient)
            self.weights = self.weights / self.weights.sum()

        # Clip weights to prevent extreme values
        self.weights = np.clip(self.weights, 0, 1)
        self.weights = self.weights / self.weights.sum()

        return self.weights

    def _project_simplex(self, w):
        """
        Project onto the simplex (sum = 1, w_i >= 0).
        """
        w = np.maximum(w, 0)
        w = w / w.sum()
        return w

    def get_weights(self):
        return self.weights

2.6 Online Learning for Market Making

text
class OnlineMarketMaker:
    def __init__(self, spread=0.001, inventory_limit=1000):
        self.spread = spread
        self.inventory_limit = inventory_limit
        self.inventory = 0
        self.bid = 0
        self.ask = 0

    def update(self, mid_price, order_flow):
        """
        Update quotes based on order flow.
        """
        # Adjust quotes based on inventory
        inventory_signal = -self.inventory / self.inventory_limit

        # Online learning: adapt spread based on volatility
        volatility = np.std(order_flow[-20:])
        dynamic_spread = self.spread + 0.5 * volatility

        # Set quotes
        self.bid = mid_price - dynamic_spread * (1 + inventory_signal * 0.5)
        self.ask = mid_price + dynamic_spread * (1 - inventory_signal * 0.5)

        # Update inventory based on filled orders
        # (simplified)
        filled_orders = self._simulate_fills(order_flow)
        self.inventory += filled_orders

        return self.bid, self.ask

3. Transfer Learning – Leveraging Pre-Trained Models

3.1 What is Transfer Learning?

Transfer learning applies knowledge learned from one task to a different but related task.

Types:

  1. Inductive Transfer: Labels are available in the source domain.

  2. Transductive Transfer: Unlabelled target data is available.

  3. Unsupervised Transfer: No labels in either domain.

3.2 Mathematical Formulation

Given source domain D_S = (X_S, y_S) and target domain D_T = (X_T, y_T), the goal is to learn f: X_T → y_T using knowledge from D_S.

3.3 Transfer Learning in Finance – Scenarios

 
 
Source Target Application
S&P 500 returns European equity returns Cross-market prediction
FX data Commodity data Similar market structure
Pre-trained NLP model Financial text Sentiment analysis
Pre-trained CNN Satellite imagery Alternative data

3.4 Fine-Tuning – The Most Common Approach

text
import torch
import torch.nn as nn

def fine_tune_model(pretrained_model, target_data, target_labels, num_epochs=10):
    """
    Fine-tune a pre-trained model on target data.
    """
    # Freeze all layers except the last few
    for param in pretrained_model.parameters():
        param.requires_grad = False

    # Replace the last layer for the target task
    pretrained_model.fc = nn.Linear(
        pretrained_model.fc.in_features,
        target_labels.shape[1] if len(target_labels.shape) > 1 else 1
    )

    # Unfreeze the last few layers
    for param in list(pretrained_model.parameters())[-5:]:
        param.requires_grad = True

    # Train on target data
    optimizer = torch.optim.Adam(
        filter(lambda p: p.requires_grad, pretrained_model.parameters()),
        lr=1e-4
    )

    for epoch in range(num_epochs):
        pretrained_model.train()
        for X_batch, y_batch in target_data:
            optimizer.zero_grad()
            y_pred = pretrained_model(X_batch)
            loss = nn.MSELoss()(y_pred, y_batch)
            loss.backward()
            optimizer.step()

    return pretrained_model

3.5 Domain Adaptation – When Distributions Differ

Domain adaptation handles the case where the source and target distributions are different:
P(X_S) ≠ P(X_T) (covariate shift) or P(y_S | X_S) ≠ P(y_T | X_T).

Adversarial Domain Adaptation:

text
class DomainAdversarialNetwork(nn.Module):
    """
    Domain Adversarial Neural Network (DANN) for domain adaptation.
    """
    def __init__(self, input_dim, hidden_dim, output_dim):
        super(DomainAdversarialNetwork, self).__init__()

        # Feature extractor
        self.feature_extractor = nn.Sequential(
            nn.Linear(input_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU()
        )

        # Label predictor
        self.label_predictor = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, output_dim)
        )

        # Domain classifier (adversarial)
        self.domain_classifier = nn.Sequential(
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, 2)  # Binary: source or target
        )

    def forward(self, x, alpha=1.0):
        """
        alpha: gradient reversal factor (larger = stronger adaptation)
        """
        features = self.feature_extractor(x)

        # Label prediction
        label = self.label_predictor(features)

        # Domain classification with gradient reversal
        # (Implementation requires custom GradientReversalLayer)
        reversed_features = gradient_reverse(features, alpha)
        domain = self.domain_classifier(reversed_features)

        return label, domain

4. Meta-Learning – Learning to Learn

Meta-learning trains models to quickly adapt to new tasks with limited data.

4.1 MAML (Model-Agnostic Meta-Learning)

Objective: Find initial parameters θ such that one gradient step on a new task leads to good performance.

Inner Loop (Task-Specific):
θ'_i = θ - α ∇θ L_{T_i}(θ)

Outer Loop (Meta):
θ ← θ - β ∇θ Σ_{i=1}^{N} L_{T_i}(θ'_i)

Implementation:

text
def maml_update(model, tasks, inner_lr=0.01, outer_lr=0.001, inner_steps=5):
    """
    MAML update step.
    """
    # Copy model parameters
    meta_theta = [p.clone() for p in model.parameters()]

    for task in tasks:
        # Clone model for this task
        task_model = copy.deepcopy(model)

        # Inner loop: adapt to task
        for _ in range(inner_steps):
            X, y = task.sample()
            y_pred = task_model(X)
            loss = nn.MSELoss()(y_pred, y)
            gradients = torch.autograd.grad(loss, task_model.parameters())

            # Update task-specific parameters
            for param, grad in zip(task_model.parameters(), gradients):
                param.data = param.data - inner_lr * grad

        # Compute loss on validation set
        X_val, y_val = task.sample_val()
        y_pred_val = task_model(X_val)
        meta_loss = nn.MSELoss()(y_pred_val, y_val)

        # Meta-gradient (gradients of meta-loss wrt original parameters)
        meta_grads = torch.autograd.grad(meta_loss, meta_theta)

        # Update meta-parameters
        for param, grad in zip(meta_theta, meta_grads):
            param.data = param.data - outer_lr * grad

    return meta_theta

4.2 Financial Application – Rapid Adaptation to New Markets

text
def adapt_to_new_market(pretrained_model, new_market_data, n_epochs=5):
    """
    Use meta-learning to rapidly adapt to a new market.
    """
    # Use MAML or fine-tuning
    # For MAML, we need multiple tasks from the same domain

    # Example: adapt from US to European markets
    # Source tasks: different US market regimes
    # Target task: European market

    # MAML adaptation
    adapted_params = maml_update(pretrained_model, new_market_data)

    return adapted_params

5. Challenges and Pitfalls

 
 
Challenge Description Solution
Catastrophic Forgetting Model forgets previous knowledge when learning new tasks. Elastic Weight Consolidation (EWC), rehearsal.
Negative Transfer Transfer learning harms performance. Careful task selection, domain similarity check.
Overfitting to Target Fine-tuning on small target data leads to overfitting. Regularisation, smaller learning rate.
Distribution Shift Source and target distributions differ. Domain adaptation, adversarial training.
Computational Cost Online learning can be expensive. Mini-batch updates, efficient algorithms.

6. Summary for the AI Practitioner

  1. Online learning is essential for streaming financial data (market data, order flow). Regret bounds ensure convergence.

  2. SGD is the workhorse of online learning. Use it with a projection onto the simplex for portfolio weights.

  3. Online portfolio optimisation (Semi-Definite, Exponentiated Gradient) adapts to changing market conditions.

  4. Transfer learning leverages pre-trained models to accelerate training on new tasks.

  5. Fine-tuning is the simplest approach. Freeze early layers and train the last layers on target data.

  6. Domain adaptation handles cases where the input distributions differ. Use adversarial training (DANN) for robust adaptation.

  7. Meta-learning (MAML) enables rapid adaptation to new tasks with limited data. Useful for entering new markets.

  8. Key pitfalls: Catastrophic forgetting, negative transfer, overfitting to target, distribution shift.

  9. Practical rule: Always validate transfer learning on a hold-out set. Monitor for performance degradation.