Particle Filters in Finance
The Kalman filter is exact only when transitions and observations are linear with Gaussian noise. Financial latent-state problems routinely violate both assumptions: volatility is positive and persistent, returns have jumps and heavy tails, and option quotes are nonlinear functions of state. A particle filter, also called sequential Monte Carlo (SMC), represents the posterior with weighted simulated states instead of a mean and covariance.
It is not a universal upgrade. Particle filters introduce Monte Carlo noise, tuning burden, and substantial compute. They earn their place when nonlinear dynamics or a non-Gaussian likelihood materially affect the decision—particularly in intraday volatility estimation, jump-aware risk, and joint use of returns and option information.
Posterior as a weighted cloud
Suppose xt is latent variance and yt is a return. A stochastic-volatility model can be written:
log(v_t) = mu + phi * (log(v_(t-1)) - mu) + eta_t
r_t | v_t ~ Student-t(nu, 0, v_t)
After seeing observations through t, the filter approximates p(xt | y1:t) with particles:
p(x_t | y_1:t) ≈ sum_i w_t^(i) delta(x_t - x_t^(i))
Each particle is a plausible current state; its normalized weight is proportional to how well it explains the latest observation. Unlike a Gaussian summary, the cloud can be skewed, multimodal, bounded, and discontinuous after a jump.
| Component | Financial interpretation | Design decision |
|---|---|---|
| transition | persistence and regime motion | how volatility evolves |
| proposal | where to simulate candidates | efficiency after large returns |
| likelihood | observation plausibility | tails, jumps, quote errors |
| resampling | remove negligible particles | manage weight collapse |
Bootstrap filter mechanics
The bootstrap filter proposes new particles from the transition prior. For each step: propagate, weight by the observation density, normalize, and resample if the weights are concentrated.
import numpy as np
from scipy.special import logsumexp
def effective_sample_size(logw):
w = np.exp(logw - logsumexp(logw))
return 1.0 / np.sum(w**2)
log_v = rng.normal(np.log(0.02**2), 0.5, size=20_000)
logw = np.full(log_v.size, -np.log(log_v.size))
for ret in returns:
log_v = mu + phi * (log_v - mu) + sigma_eta * rng.standard_normal(log_v.size)
v = np.exp(log_v)
# Gaussian example; use a t or jump mixture in real applications
logw += -0.5 * (np.log(2 * np.pi * v) + ret**2 / v)
logw -= logsumexp(logw)
if effective_sample_size(logw) < 0.5 * log_v.size:
idx = systematic_resample(np.exp(logw))
log_v, logw = log_v[idx], np.full(log_v.size, -np.log(log_v.size))
The estimate can be the weighted mean of v, but risk applications should retain posterior quantiles. A mean that is calm while the upper tail is wide is materially different from a confidently calm estimate.
Degeneracy is the central failure mode
Large returns make almost every prior-proposed particle implausible. One or two particles receive nearly all weight, reducing effective sample size (ESS) and creating a noisy, biased posterior after resampling. Resampling every time step prevents weight concentration but creates genealogical collapse: far back in the history, all particles share one ancestor.
| Problem | Observable diagnostic | Better approach |
|---|---|---|
| Weight collapse | ESS near one | resample at ESS threshold |
| Missed volatility jump | likelihood dominated by one particle | heavy-tailed or jump mixture |
| Particle impoverishment | duplicate states after resampling | jitter, rejuvenation MCMC |
| Slow recovery | posterior lags realized moves | observation-informed proposal |
An auxiliary particle filter preweights candidates using the next observation; an unscented or extended-Kalman proposal locally approximates the conditional state. These methods reduce variance but must be tested against the simple bootstrap baseline. Complexity that only improves an in-sample likelihood is not a trading improvement.
Returns, options, and timing
Adding option-implied volatility to the observation vector can identify the forward-looking volatility state more quickly than returns alone:
y_t = [return_t, IV_t(K_1,T_1), ..., IV_t(K_m,T_m)]
However, observed IV includes bid-ask noise, stale quotes, supply-demand pressure, and model inversion error. Build a likelihood with quote-quality filters and bid-ask-scaled errors. Do not treat every strike as independent evidence; the surface has correlated microstructure noise.
The timestamp question is equally important. An end-of-day option surface may incorporate information unavailable to a close-to-close return strategy. Establish the exact signal cut, exchange calendars, and next executable price before interpreting filter forecasts.
Parameter learning and evaluation
Most filters condition on fixed parameters. Estimating persistence, tail degrees of freedom, and jump intensity online is harder because static parameter particles also degenerate. Particle MCMC, SMC2, or periodic expanding-window maximum likelihood are practical alternatives. Start by refitting parameters on a slow schedule; compare it to a fixed parameter set and document the information set.
Evaluate forecast distributions, not merely point estimates. Check probability integral transforms, VaR exception clustering, log predictive score, and realized-volatility calibration. If the output drives a volatility-targeted portfolio, include the cost of leverage changes and stress an abrupt volatility spike. For a lower-complexity baseline, compare with GARCH volatility modeling and a Kalman filter.
Key takeaways
- Particle filters approximate nonlinear, non-Gaussian posteriors with weighted state simulations.
- Monitor ESS; resampling solves weight collapse but can reduce state diversity.
- Heavy-tailed likelihoods and observation-informed proposals matter most in stressed markets.
- Validate predictive distributions and executable portfolio behavior, not just latent-state plots.
