Common Probability Distributions in Finance › Discrete Distributions: Binomial and Poisson
Quantitative FinanceAll chapters Common Probability Distributions in Finance › Discrete Distributions: Binomial and PoissonOne section · Sections in this chapter4 / 8 ↓
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
Not all financial phenomena are continuous. Events like defaults, dividend announcements, or large jumps in prices are discrete occurrences. The binomial and Poisson distributions model these situations and give a framework for counting how many times something happens rather than measuring continuous quantities. These distributions are particularly important in credit risk, where we count defaults in a portfolio, and in derivatives pricing, where discrete lattice models approximate continuous price processes.
The Binomial Distribution
The binomial distribution models the number of successes in a fixed number of independent trials, each with the same probability of success. The classic example is flipping a coin multiple times and counting heads, but the same mathematics applies to questions such as: How many stocks in a portfolio will outperform? How many corporate bonds will default? How many days this month will have positive returns?
The probability distribution of the number of successes in independent Bernoulli trials, where each trial has success probability . Used in discrete option pricing models and credit risk modeling.
If , the probability mass function is:
This formula answers the question: if we have independent trials, each with success probability , what is the probability of getting exactly successes? The answer decomposes into two multiplicative components: the number of ways the successes can be arranged, and the probability of any particular arrangement.
Each component of this formula serves a specific purpose:
- : the number of successes we're computing the probability for
- : total number of independent trials
- : probability of success on each trial
- : the binomial coefficient, representing the number of ways to choose successes among trials
- : success-probability factor for specified success positions
- : failure-probability factor for specified failure positions
The formula multiplies the number of arrangements by the probability of each specific arrangement. Intuitively, if you flip a coin times with probability of heads, the PMF tells you the probability of getting exactly heads: you need successes (probability ), failures (probability ), and the binomial coefficient counts how many orderings produce that outcome. The key insight is that because the trials are independent, the probability of any specific sequence of outcomes is simply the product of the individual probabilities, and the binomial coefficient tells us how many such sequences lead to exactly successes.
The mean and variance have elegant formulas that reveal the distribution's structure:
These formulas encode intuitive relationships:
- : the expected number of successes
- : the variance in the number of successes
- : total number of trials
- : probability of success on each trial
- : probability of failure; multiplying by gives each Bernoulli trial's variance
The expected number of successes, , is simply the number of trials times the success probability, exactly what intuition suggests. The variance formula is more subtle: it shows that variance is maximized when (complete uncertainty about each trial's outcome) and decreases toward zero as approaches 0 or 1 (near certainty). This makes sense because if is very close to 1, almost all trials succeed, leaving little room for variability in the total count.


Binomial Option Pricing Model
The Cox-Ross-Rubinstein (CRR) binomial model prices options by assuming that at each time step, the stock price either moves up by a factor or down by a factor . This turns a continuous-time model into a tree of possible outcomes that can be analyzed one step at a time. Under shared assumptions for a non-dividend-paying stock and a vanilla European option, its price converges to the Black-Scholes price as the number of time steps increases.
After steps, the stock price has experienced up moves and down moves, giving:
This formula shows how the final price depends entirely on the number of up moves, not their specific timing. The components are:
- : stock price at maturity after steps
- : initial stock price
- : up factor (price multiplier for an up move)
- : down factor (price multiplier for a down move)
- : number of up moves
- : number of down moves
Whether the stock rises first and then falls, or falls first and then rises, the final price is the same as long as the total number of up and down moves matches. This recombination supports efficient pricing for vanilla European calls and puts, whose payoff depends only on the final price. European exercise alone does not rule out path dependence. Claims such as Asian options require the tree state to be augmented with path information; the simple terminal-price state is not sufficient.
The number of up moves follows a binomial distribution under the risk-neutral measure, with probability of an up move:
This risk-neutral probability is derived from the pricing model, not estimated as the physical probability of an up move. It need not equal that physical probability, although the two can coincide. It makes the modeled expected stock growth equal to risk-free growth. The components are:
- : risk-neutral probability of an up move
- : continuously compounded annual risk-free rate
- : length of each time step in years
- : up factor (stock price multiplier for an up move)
- : down factor (stock price multiplier for a down move)
- : the growth factor for a risk-free investment over one time step
This formula is derived from the no-arbitrage condition: the expected stock return under the risk-neutral measure must equal the risk-free rate. Setting and solving for yields this result. In this complete one-period binomial model, a replicable option is priced from the risk-neutral probability rather than the physical up-move probability. This conclusion depends on no arbitrage, market completeness, and the ability to form the replicating portfolio; it is not a universal statement about every derivative market.
Assuming , this is a strict probability and the one-period stock-and-bond model is arbitrage-free only when
which is equivalent to . Inputs outside this range do not define valid CRR risk-neutral probabilities.

import numpy as np
from scipy import stats
def binomial_option_price(S0, K, T, r, sigma, n_steps, option_type="call"):
"""
Price a vanilla European option on a non-dividend-paying stock using CRR.
Parameters:
S0: Initial stock price
K: Strike price
T: Time to expiration (years)
r: Continuously compounded annual risk-free rate
sigma: Volatility (annualized)
n_steps: Number of time steps
option_type: 'call' or 'put'
Returns:
Option price
This instructional implementation uses double precision. Finite parameters
and the no-arbitrage condition do not guarantee representable terminal
prices or accurate tiny residuals. The scaled branches avoid selected
intermediate failures; they are not an arbitrary-range accuracy guarantee.
"""
if option_type not in {"call", "put"}:
raise ValueError("option_type must be 'call' or 'put'")
if not all(np.isfinite(value) for value in (S0, K, T, r, sigma)):
raise ValueError("Pricing parameters must be finite")
if S0 <= 0 or K <= 0 or T <= 0 or sigma <= 0:
raise ValueError("Require S0, K, T and sigma to be positive")
if (
isinstance(n_steps, (bool, np.bool_))
or not isinstance(n_steps, (int, np.integer))
or n_steps <= 0
):
raise ValueError("n_steps must be a positive integer")
dt = T / n_steps
# Keep the ordinary path, with time scaling if T/n rounds to zero.
log_up = (
sigma * np.sqrt(dt)
if dt > 0
else _scaled_product(sigma, np.sqrt(T), np.sqrt(n_steps))
)
rate_step = r * dt if dt > 0 else _scaled_product(r, T, n_steps)
if not np.isfinite(log_up) or log_up <= 0 or not np.isfinite(rate_step):
raise FloatingPointError(
"CRR step coefficients are outside double-precision range"
)
if not -log_up < rate_step < log_up:
raise ValueError("CRR no-arbitrage condition requires d < exp(r*dt) < u")
if log_up > 350 or min(rate_step + log_up, log_up - rate_step) < 1e-8 * log_up:
# Algebraically scale by exp(log_up); retain the small probability
# independently instead of subtracting it from a rounded one.
denominator = -np.expm1(-2 * log_up)
log_q = (
rate_step
- log_up
+ np.log(-np.expm1(-rate_step - log_up))
- np.log(denominator)
)
q = np.exp(log_q)
down_probability = -np.expm1(rate_step - log_up) / denominator
log_down_probability = np.log(-np.expm1(rate_step - log_up)) - np.log(
denominator
)
else:
q = (np.expm1(rate_step) - np.expm1(-log_up)) / (2 * np.sinh(log_up))
down_probability = 1 - q
log_q = np.log(q)
log_down_probability = np.log1p(-q)
use_down_probability = (
q == 1
or (log_up > 350 or min(rate_step + log_up, log_up - rate_step) < 1e-8 * log_up)
and down_probability < q
)
# Calculate all possible final stock prices
k = np.arange(n_steps + 1)
log_moves = (2 * k - n_steps) * log_up
with np.errstate(over="ignore", under="ignore", invalid="ignore"):
final_prices = S0 * np.exp(log_moves)
if not np.all(np.isfinite(final_prices) & (final_prices > 0)):
final_prices = np.exp(np.log(S0) + log_moves)
if not np.all(np.isfinite(final_prices) & (final_prices > 0)):
raise FloatingPointError("CRR terminal prices exceed floating-point range")
# Calculate payoffs
if option_type == "call":
payoffs = np.maximum(final_prices - K, 0)
else:
payoffs = np.maximum(K - final_prices, 0)
# Near the strike, subtracting rounded price levels can erase the payoff.
relative_spot_gap = (S0 - K) / K if abs(S0 - K) < 0.5 * K else np.inf
log_spot_strike = (
np.log1p(relative_spot_gap)
if abs(relative_spot_gap) < 0.5
else np.log(S0) - np.log(K)
)
log_moneyness = log_spot_strike + log_moves
near_strike = np.abs(log_moneyness) <= 1e-6
if option_type == "call":
payoffs[near_strike] = K * np.maximum(np.expm1(log_moneyness[near_strike]), 0)
else:
payoffs[near_strike] = K * np.maximum(-np.expm1(log_moneyness[near_strike]), 0)
# Risk-neutral probabilities
# A stable PMF avoids overflowing binomial coefficients in large trees.
if use_down_probability:
probs = stats.binom.pmf(n_steps - k, n_steps, down_probability)
else:
probs = stats.binom.pmf(k, n_steps, q)
# Expected payoff under risk-neutral measure, discounted
with np.errstate(over="ignore", under="ignore", invalid="ignore"):
option_price = np.exp(-r * T) * np.sum(probs * payoffs)
positive_payoff = payoffs > 0
if not np.isfinite(option_price) or np.any((probs == 0) & positive_payoff):
# The same terminal expectation, with probability/payoff/discount
# combined before exponentiation if an intermediate loses range.
from scipy.special import gammaln, logsumexp
if not np.any(positive_payoff):
return 0.0
log_probs = (
gammaln(n_steps + 1)
- gammaln(k + 1)
- gammaln(n_steps - k + 1)
+ k * log_q
+ (n_steps - k) * log_down_probability
)
log_expectation = logsumexp(
log_probs[positive_payoff] + np.log(payoffs[positive_payoff]) - r * T
)
with np.errstate(over="ignore", under="ignore"):
option_price = np.exp(log_expectation)
if not np.isfinite(option_price):
raise FloatingPointError("Option price exceeds floating-point representability")
return option_price
# Price a call option
S0 = 100
K = 100
T = 0.5 # 6 months
r = 0.05
sigma = 0.20Binomial Model Call Option Prices ----------------------------------- Steps: 10 | Price: $6.7494 Steps: 50 | Price: $6.8605 Steps: 100 | Price: $6.8746 Steps: 200 | Price: $6.8817 Steps: 500 | Price: $6.8859 ----------------------------------- Black-Scholes: | Price: $6.8887
As the number of steps increases, the CRR binomial price converges to the Black-Scholes price under their shared assumptions. The error can oscillate because the strike sits differently relative to successive lattices. First-order convergence, on the scale of , has been established for the European-call setting; this is not a uniform rate guarantee for arbitrary payoffs and parameter choices. The convergence reflects a mathematical connection between the binomial random walk and Brownian motion, formalized in Donsker's theorem.

The Poisson Distribution
The Poisson distribution models the number of events occurring in a fixed interval when events happen at a constant average rate and independently of each other. Unlike the binomial distribution, which assumes a fixed number of trials, the Poisson distribution allows for any number of events, making it useful for modeling rare occurrences over a continuous interval.
A discrete probability distribution for an event count with expected interval count . For a homogeneous Poisson process with intensity events per unit time, the count over an interval of length has parameter . It provides a baseline for rare independent defaults, jump counts, and arrivals.
If , the probability mass function is:
This formula has a clear structure when we trace its derivation from first principles. Imagine dividing the interval into many tiny subintervals, each with a small probability of containing an event. As the number of subintervals goes to infinity while keeping the expected total number of events fixed at , we obtain the Poisson distribution.
The components of this formula are:
- : the number of events we're computing the probability for
- : expected number of events in the interval, equal to intensity times interval length
- : the probability of zero events, which is a normalization factor
- : the intensity factor for events; together with , it makes largest at for
- : accounts for the indistinguishability of event orderings
The Poisson distribution arises as the limit of a binomial distribution when and while remains constant. More generally, a Poisson approximation requires independent rare-event opportunities whose largest individual probability vanishes while the sum of their probabilities approaches . A finite expected count alone is not enough: dependence or a non-negligible individual probability can produce a different count distribution. Credit portfolios with small independent default probabilities and jump models with independent small-interval arrivals can satisfy the approximation. Dependence-driven clustering requires a richer model; occasional groups of arrivals can also occur by chance under a Poisson process.
Both the mean and variance equal :
This property deserves emphasis:
- : the expected number of events
- : the variance in the number of events
- : expected interval count, not a per-unit-time rate unless the interval is one time unit
The equality of mean and variance is a distinctive property of the Poisson distribution. When analyzing count data, if the observed variance significantly exceeds the mean (overdispersion), this suggests the Poisson model may be inadequate. Dependence is one possible cause, but rate heterogeneity, omitted covariates, and excess zeros can also produce overdispersion. For instance, defaults may cluster because borrowers share economic conditions even when individual default mechanisms differ.


Applications of the Poisson Distribution
The Poisson distribution is particularly useful for modeling:
-
Jump processes in asset prices: The Merton jump-diffusion model adds jumps whose arrivals follow a Poisson process and whose sizes follow a separate distribution to geometric Brownian motion. The stock price follows GBM between jumps and moves discontinuously at jump times. This combination represents continuous diffusion-like fluctuations together with occasional discontinuous price moves.
-
Credit defaults in a portfolio: A large collection of independent, rare default opportunities can admit a Poisson count approximation under the limiting conditions above. Shared economic conditions can make defaults dependent, so a Poisson approximation is not justified by a large portfolio alone.
-
Order arrivals: A homogeneous Poisson process is a baseline arrival model with independent exponential waiting times. Chance clusters can occur, but the model does not encode self-excitation or time-varying intraday intensity. Those features require richer arrival models.
import numpy as np
def simulate_jump_diffusion(
S0, mu, sigma, lambda_jump, mu_jump, sigma_jump, T, n_steps, n_paths, rng=None
):
"""
Simulate Merton jump-diffusion price paths.
Parameters:
S0: Initial stock price
mu: Drift of the diffusion component
sigma: Volatility of the diffusion component
lambda_jump: Average number of jumps per year (Poisson intensity)
mu_jump: Mean of log-jump size
sigma_jump: Std dev of log-jump size
T: Time horizon in years
n_steps: Number of time steps
n_paths: Number of paths to simulate
rng: Optional local NumPy RandomState; defaults to a fresh seed-42 stream
Returns:
Array of simulated price paths
Finite inputs do not guarantee representable coefficients, Poisson counts
or prices. Numerical range failures are not evidence of model invalidity.
The returned time-zero column directly preserves the floating-point
representation of S0, not an arbitrary integer's exact precision.
"""
if not all(
np.isfinite(value)
for value in (S0, mu, sigma, lambda_jump, mu_jump, sigma_jump, T)
):
raise ValueError("Simulation parameters must be finite")
if S0 <= 0 or min(sigma, lambda_jump, sigma_jump, T) < 0:
raise ValueError("Require S0>0 and nonnegative scales, intensity and horizon")
if any(
isinstance(value, (bool, np.bool_))
or not isinstance(value, (int, np.integer))
or value <= 0
for value in (n_steps, n_paths)
):
raise ValueError("n_steps and n_paths must be positive integers")
dt = T / n_steps
rng = np.random.RandomState(42) if rng is None else rng
# Diffusion component
Z_diffusion = rng.standard_normal((n_paths, n_steps))
if T == 0:
diffusion = np.zeros_like(Z_diffusion)
else:
ordinary_step = dt > 0 and sigma <= np.sqrt(np.finfo(float).max)
if ordinary_step:
with np.errstate(over="ignore", invalid="ignore"):
diffusion = (mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(
dt
) * Z_diffusion
ordinary_step = np.all(np.isfinite(diffusion))
if not ordinary_step:
volatility_step = _scaled_product(sigma, np.sqrt(T), np.sqrt(n_steps))
drift_step = _scaled_product(mu, T, n_steps)
variance_step = _scaled_product(volatility_step, volatility_step, 2)
diffusion = drift_step - variance_step + volatility_step * Z_diffusion
# Jump component
# Number of jumps in each interval (Poisson distributed)
interval_intensity = (
lambda_jump * dt
if dt > 0 or T == 0
else _scaled_product(lambda_jump, T, n_steps)
)
n_jumps = rng.poisson(interval_intensity, (n_paths, n_steps))
# Log jump sizes (normal)
jump_sizes = np.zeros((n_paths, n_steps))
for i in range(n_paths):
for j in range(n_steps):
if n_jumps[i, j] > 0:
jumps = rng.normal(mu_jump, sigma_jump, n_jumps[i, j])
jump_sizes[i, j] = np.sum(jumps)
# Combine diffusion and jumps
log_returns = diffusion + jump_sizes
# Calculate prices
log_prices = np.zeros((n_paths, n_steps + 1))
log_prices[:, 0] = np.log(S0)
log_prices[:, 1:] = np.log(S0) + np.cumsum(log_returns, axis=1)
if T == 0:
return np.full((n_paths, n_steps + 1), S0, dtype=float)
with np.errstate(over="raise", under="ignore", invalid="raise"):
prices = np.exp(log_prices)
prices[:, 0] = S0
if not np.all(np.isfinite(prices) & (prices > 0)):
raise FloatingPointError("Price paths exceed floating-point representability")
return prices
# Compare GBM vs Jump-Diffusion
S0 = 100
mu = 0.10
sigma = 0.20
T = 1.0
n_steps = 252
n_paths = 10000
# Pure GBM paths
gbm_paths = simulate_gbm_paths(S0, mu, sigma, T, n_steps, n_paths)
# Jump-diffusion paths
jd_rng = np.random.RandomState(42)
jd_paths = simulate_jump_diffusion(
S0,
mu,
sigma,
lambda_jump=2, # 2 jumps per year on average
mu_jump=-0.05, # Jumps tend to be negative
sigma_jump=0.10,
T=T,
n_steps=n_steps,
n_paths=n_paths,
rng=jd_rng,
)
gbm_returns = np.diff(np.log(gbm_paths), axis=1).flatten()
jd_returns = np.diff(np.log(jd_paths), axis=1).flatten()Return Distribution Comparison ================================================== Statistic GBM Jump-Diffusion -------------------------------------------------- Mean 0.000312 -0.000083 Std Dev 0.012609 0.016096 Skewness -0.0006 -3.1474 Excess Kurtosis -0.0002 55.5761 Min -0.0605 -0.4391 Max 0.0611 0.3572
With the negatively centered jump-size distribution used here, the simulated jump-diffusion returns have a more negative sample skewness and a larger fourth standardized moment than the simulated GBM returns. Changing the jump-size distribution can change the sign of skewness. Likewise, higher kurtosis alone does not determine a unique allocation of probability among the center, shoulders, and tails; the histogram and quantiles must be examined directly.

