The Feller condition is a single inequality that tells you whether a CIR process can reach zero:

    \[2\kappa\theta \geq \xi^2\]

Satisfy it and the process stays strictly positive forever. Violate it and it touches zero with positive probability. That matters because CIR is the standard model for two things that cannot go negative — short rates and variance — and a simulation that produces negative values is not a modelling nuance, it is broken output feeding into prices.

The process

    \[dr_t = \kappa(\theta - r_t)\,dt + \xi\sqrt{r_t}\,dW_t\]

with \kappa the mean-reversion speed, \theta the long-run level and \xi the volatility of the rate. The \sqrt{r_t} is what makes it interesting: as r approaches zero the diffusion term shrinks, so the process gets quieter near the boundary while the drift \kappa\theta keeps pushing it up.

Feller’s condition is the statement of who wins that race. The upward push at zero is \kappa\theta; the noise trying to drag it through is governed by \xi^2. When 2\kappa\theta \geq \xi^2 the drift dominates and zero is unreachable.

What it looks like in practice

Four parameter sets, 200,000 paths each, daily steps over a year, counting how many paths touch zero at any point:

\kappa \theta \xi 2\kappa\theta \xi^2 Feller Paths hitting zero
2.0 0.04 0.10 0.1600 0.0100 satisfied 0.00%
2.0 0.04 0.30 0.1600 0.0900 satisfied 1.67%
1.0 0.04 0.40 0.0800 0.1600 violated 45.94%
0.5 0.04 0.55 0.0400 0.3025 violated 75.46%

The pattern is stark. Comfortably inside the condition, no path goes near zero. Comfortably outside, three quarters of them do.

The middle row deserves attention. Feller is technically satisfied — 0.16 against 0.09 — and 1.67% of paths still register a zero. That is not the theory failing. It is the **discretisation** failing: an Euler step can jump below zero even when the continuous process never would, because the step is finite and the square root is evaluated at the start of it. Feller is a statement about the continuous process, not about your code.

Why this bites in real calibrations

The awkward fact: **market calibrations routinely violate Feller.** Fitting Heston to an equity index option surface very often produces parameters where 2\kappa\theta < \xi^2, because the market’s steep short-dated smile demands a high vol-of-vol that the condition will not accommodate.

So you cannot simply refuse to work outside the condition. The realistic positions are:

**Constrain the calibration** to respect Feller. Clean, and it fits the observed smile worse. Some desks do it anyway for the stability.

**Let it violate and handle the boundary numerically.** More common. The condition tells you which regime you are in, and then you choose a scheme that survives it.

Either way, the value of the condition is diagnostic: compute 2\kappa\theta - \xi^2 every time you calibrate, and you know immediately whether the boundary is going to be an issue before you see a single simulated path.

What to do when it is violated

Not a higher-order scheme. Milstein does not fix this — it improves pathwise accuracy, which was never the problem, and produces marginally *more* negative values than Euler on CIR.

What works:

**Full truncation.** Use \max(r, 0) inside both the drift and the diffusion, and allow the state itself to go slightly negative between steps. Simple, and the standard first choice.

**Reflection.** Take |r| instead. Easy, but it introduces a bias by adding mass near the boundary rather than absorbing it.

**The Andersen QE scheme.** Matches the first two moments of the exact transition using a quadratic-exponential approximation. More work to implement and much better behaved when Feller is badly violated — the standard choice in production Heston engines.

**Exact simulation.** CIR has a known non-central chi-square transition density, so you can sample it directly with no discretisation error at all. Slower per step, exact by construction, and the right benchmark to validate any of the above against.

Code

import numpy as np

def cir_hits_zero(kap, th, xi, r0=0.04, T=1.0, n=252, M=200_000, seed=3):
    rng = np.random.default_rng(seed); dt = T/n
    r = np.full(M, r0); hit = np.zeros(M, bool)
    for _ in range(n):
        p = np.maximum(r, 0.)                      # full truncation
        r = r + kap*(th - p)*dt + xi*np.sqrt(p*dt)*rng.standard_normal(M)
        hit |= (r <= 0)
    return hit.mean()

for kap, th, xi in [(2.0, 0.04, 0.10), (2.0, 0.04, 0.30),
                    (1.0, 0.04, 0.40), (0.5, 0.04, 0.55)]:
    feller = 2*kap*th - xi**2
    print(f"2*k*th - xi^2 = {feller:+.4f}  "
          f"{'OK      ' if feller >= 0 else 'VIOLATED'}  "
          f"hit zero: {100*cir_hits_zero(kap, th, xi):.2f}%")

Make that first line part of your calibration output. A single number that tells you whether the parameters you just fitted are going to fight your simulation scheme.

The takeaway

Feller’s condition is 2\kappa\theta \geq \xi^2, and it decides whether a CIR process can reach zero. Satisfied, the boundary is unreachable and any reasonable scheme works. Violated — which happens routinely in real equity calibrations — a large fraction of paths visit zero and you need a scheme built for the boundary rather than a more accurate one.

Compute it at calibration time, not after you have found negative rates in your output.


Related: Euler vs Milstein, including why the extra Milstein term does not help here, and fixing negative rates in a CIR simulation.


SDE discretisation, boundary-preserving schemes and the numerical checks that reveal a broken implementation are worked through from the ground up in the course modules.


Get the Quant Reference Card — four pages covering stochastic calculus, measure change, the models you’ll meet, discretisation, rates, credit and XVA. Free.