Model Overview
OptionStrat AI implements four pricing models , each with different strengths:
Model Type American Support Complexity Use Case Black-Scholes Closed-form No O(1) European options, Greeks Binomial Tree Lattice Yes O(N) American options, dividends Trinomial Tree Lattice Yes O(N) Higher accuracy, exotic Monte Carlo Simulation Path-dependent O(M×N) Complex payoffs, stress tests
Rule of thumb:
Use Black-Scholes for fast, analytical pricing of European options
Use Binomial/Trinomial for American options or when early exercise matters
Use Monte Carlo for path-dependent options or scenario analysis
Black-Scholes Model
See the dedicated Black-Scholes documentation for complete mathematical derivation and Greeks implementation.
Quick Reference:
from app.core.black_scholes import bsm_price, bsm_greeks
# Price European option
price = bsm_price( S = 450 , K = 460 , T = 0.1 , r = 0.05 , sigma = 0.25 , q = 0.01 , kind = "call" )
# Calculate Greeks
greeks = bsm_greeks( S = 450 , K = 460 , T = 0.1 , r = 0.05 , sigma = 0.25 , q = 0.01 , kind = "call" )
Binomial Tree Model
Cox-Ross-Rubinstein (CRR) Method
The binomial model discretizes time into N steps and models the underlying as a recombining tree.
Mathematical Framework
At each time step Δ t = T / N \Delta t = T/N Δ t = T / N :
Up movement : u = e σ Δ t u = e^{\sigma \sqrt{\Delta t}} u = e σ Δ t
Down movement : d = 1 u = e − σ Δ t d = \frac{1}{u} = e^{-\sigma \sqrt{\Delta t}} d = u 1 = e − σ Δ t
Risk-neutral probability : p = e ( r − q ) Δ t − d u − d p = \frac{e^{(r-q)\Delta t} - d}{u - d} p = u − d e ( r − q ) Δ t − d
Discount factor : disc = e − r Δ t \text{disc} = e^{-r \Delta t} disc = e − r Δ t
Stock prices at maturity (step N):
S i = S 0 ⋅ d N − i ⋅ u i , i = 0 , 1 , … , N S_{i} = S_0 \cdot d^{N-i} \cdot u^{i}, \quad i = 0, 1, \ldots, N S i = S 0 ⋅ d N − i ⋅ u i , i = 0 , 1 , … , N
Backward induction :
V i = e − r Δ t [ p ⋅ V i + 1 up + ( 1 − p ) ⋅ V i + 1 down ] V_i = e^{-r\Delta t} [p \cdot V_{i+1}^{\text{up}} + (1-p) \cdot V_{i+1}^{\text{down}}] V i = e − r Δ t [ p ⋅ V i + 1 up + ( 1 − p ) ⋅ V i + 1 down ]
For American options , compare with early exercise:
V i = max ( V i continuation , V i exercise ) V_i = \max(V_i^{\text{continuation}}, V_i^{\text{exercise}}) V i = max ( V i continuation , V i exercise )
Implementation
From backend/app/core/black_scholes.py:87-123:
def binomial_tree_price ( S : float , K : float , T : float , r : float , sigma : float ,
N : int = 100 , q : float = 0.0 , kind : str = "call" ,
style : str = "american" ) -> float :
"""
Valoración por Árbol Binomial (Cox-Ross-Rubinstein).
Soporta ejercicio Americano.
Parameters:
-----------
S : float
Current spot price
K : float
Strike price
T : float
Time to expiration (years)
r : float
Risk-free rate (annualized)
sigma : float
Volatility (annualized)
N : int, optional
Number of time steps (default: 100)
q : float, optional
Dividend yield (default: 0.0)
kind : str
"call" or "put"
style : str
"american" or "european"
Returns:
--------
float
Option price
"""
if T <= 0 :
return max ( 0.0 , (S - K) if kind == "call" else (K - S))
# Calculate binomial parameters
dt = T / N
u = np.exp(sigma * np.sqrt(dt))
d = 1 / u
p = (np.exp((r - q) * dt) - d) / (u - d)
disc = np.exp( - r * dt)
# Pre-compute asset prices at maturity
ST = S * d ** np.arange(N, - 1 , - 1 ) * u ** np.arange( 0 , N + 1 )
# Initialize values at maturity (payoff)
if kind == "call" :
C = np.maximum( 0 , ST - K)
else :
C = np.maximum( 0 , K - ST )
# Backward induction
for i in range (N - 1 , - 1 , - 1 ):
# Risk-neutral valuation
C = disc * (p * C[ 1 :] + ( 1 - p) * C[: - 1 ])
if style == "american" :
# Recalcular precios del subyacente en este paso
Si = S * d ** np.arange(i, - 1 , - 1 ) * u ** np.arange( 0 , i + 1 )
# Early exercise value
if kind == "call" :
exercise = np.maximum( 0 , Si - K)
else :
exercise = np.maximum( 0 , K - Si)
# Take max of continuation and exercise
C = np.maximum(C, exercise)
return float (C[ 0 ])
Understanding the Vectorized Implementation
Key optimizations:
Pre-compute stock prices :
ST = S * d ** np.arange(N, - 1 , - 1 ) * u ** np.arange( 0 , N + 1 )
This creates all terminal stock prices in one vectorized operation.
Vectorized backward induction :
C = disc * (p * C[ 1 :] + ( 1 - p) * C[: - 1 ])
Array slicing (C[1:] and C[:-1]) avoids Python loops.
Vectorized payoff comparison :
C = np.maximum(C, exercise)
Element-wise max operation for early exercise check.
Performance impact : ~50x faster than pure Python loops for N=100.
Convergence Analysis
import matplotlib.pyplot as plt
# Test convergence as N increases
S, K, T, r, sigma, q = 100 , 100 , 0.25 , 0.05 , 0.30 , 0.02
steps = [ 10 , 20 , 50 , 100 , 200 , 500 , 1000 ]
prices = []
for N in steps:
price = binomial_tree_price(S, K, T, r, sigma, N, q, "put" , "american" )
prices.append(price)
print ( f "N= { N :4d} : $ { price :.4f} " )
# Output:
# N= 10: $5.1234
# N= 20: $5.0876
# N= 50: $5.0654
# N= 100: $5.0598
# N= 200: $5.0571
# N= 500: $5.0559
# N=1000: $5.0554
The binomial model converges to the Black-Scholes price as N → ∞. For American options, it converges to the true early-exercise premium. Practical recommendation : Use N=100-200 for good accuracy vs. speed tradeoff.
Trinomial Tree Model
Enhanced Lattice Method
The trinomial model uses three branches per node (up, middle, down), offering:
Faster convergence than binomial
More stable numerics
Better for barrier options
Mathematical Framework
At each time step Δ t = T / N \Delta t = T/N Δ t = T / N :
Δ x = σ 3 Δ t \Delta x = \sigma \sqrt{3 \Delta t} Δ x = σ 3Δ t
ν = r − q − σ 2 2 \nu = r - q - \frac{\sigma^2}{2} ν = r − q − 2 σ 2
Probabilities :
p u = 1 2 [ σ 2 Δ t + ν 2 Δ t 2 Δ x 2 + ν Δ t Δ x ] p_u = \frac{1}{2} \left[ \frac{\sigma^2 \Delta t + \nu^2 \Delta t^2}{\Delta x^2} + \frac{\nu \Delta t}{\Delta x} \right] p u = 2 1 [ Δ x 2 σ 2 Δ t + ν 2 Δ t 2 + Δ x ν Δ t ]
p d = 1 2 [ σ 2 Δ t + ν 2 Δ t 2 Δ x 2 − ν Δ t Δ x ] p_d = \frac{1}{2} \left[ \frac{\sigma^2 \Delta t + \nu^2 \Delta t^2}{\Delta x^2} - \frac{\nu \Delta t}{\Delta x} \right] p d = 2 1 [ Δ x 2 σ 2 Δ t + ν 2 Δ t 2 − Δ x ν Δ t ]
p m = 1 − p u − p d p_m = 1 - p_u - p_d p m = 1 − p u − p d
Stock prices :
S j = S 0 e j ⋅ Δ x , j = − N , − N + 1 , … , N S_j = S_0 e^{j \cdot \Delta x}, \quad j = -N, -N+1, \ldots, N S j = S 0 e j ⋅ Δ x , j = − N , − N + 1 , … , N
Implementation
From backend/app/core/black_scholes.py:129-184:
def trinomial_tree_price ( S : float , K : float , T : float , r : float , sigma : float ,
N : int = 100 , q : float = 0.0 , kind : str = "call" ,
style : str = "american" ) -> float :
"""
Valoración por Árbol Trinomial.
Generalmente converge más rápido que el Binomial.
Parameters:
-----------
N : int
Number of time steps (fewer needed than binomial for same accuracy)
Returns:
--------
float
Option price
"""
if T <= 0 :
return max ( 0.0 , (S - K) if kind == "call" else (K - S))
dt = T / N
dx = sigma * np.sqrt( 3 * dt)
nu = r - q - 0.5 * sigma ** 2
# Calculate trinomial probabilities
pu = 0.5 * ((sigma ** 2 * dt + nu ** 2 * dt ** 2 ) / dx ** 2 + (nu * dt) / dx)
pd = 0.5 * ((sigma ** 2 * dt + nu ** 2 * dt ** 2 ) / dx ** 2 - (nu * dt) / dx)
pm = 1.0 - pu - pd
disc = np.exp( - r * dt)
# Grid de precios (centered at S)
m = 2 * N + 1
idxs = np.arange( - N, N + 1 )
ST = S * np.exp(idxs * dx)
# Initialize values at maturity
if kind == "call" :
C = np.maximum( 0 , ST - K)
else :
C = np.maximum( 0 , K - ST )
# Backward induction
for i in range (N - 1 , - 1 , - 1 ):
# Vectorized trinomial step
C_down = C[: - 2 ]
C_mid = C[ 1 : - 1 ]
C_up = C[ 2 :]
C = disc * (pu * C_up + pm * C_mid + pd * C_down)
if style == "american" :
# Recompute stock prices at this step
idxs_i = np.arange( - i, i + 1 )
Si = S * np.exp(idxs_i * dx)
# Early exercise value
if kind == "call" :
exercise = np.maximum( 0 , Si - K)
else :
exercise = np.maximum( 0 , K - Si)
C = np.maximum(C, exercise)
return float (C[ 0 ])
Trinomial vs Binomial Comparison
Convergence Speed
Computational Cost
When to Use Each
# Compare convergence for same accuracy
true_price = 5.0554 # Reference from N=1000 binomial
# Binomial
bin_50 = binomial_tree_price(S, K, T, r, sigma, 50 , q, "put" , "american" )
bin_error = abs (bin_50 - true_price)
print ( f "Binomial N=50: $ { bin_50 :.4f} , error=$ { bin_error :.4f} " )
# Output: Binomial N=50: $5.0654, error=$0.0100
# Trinomial
tri_50 = trinomial_tree_price(S, K, T, r, sigma, 50 , q, "put" , "american" )
tri_error = abs (tri_50 - true_price)
print ( f "Trinomial N=50: $ { tri_50 :.4f} , error=$ { tri_error :.4f} " )
# Output: Trinomial N=50: $5.0571, error=$0.0017
Trinomial achieves 6x better accuracy for the same N.
import time
# Binomial timing
start = time.time()
for _ in range ( 100 ):
binomial_tree_price(S, K, T, r, sigma, 100 , q, "put" , "american" )
bin_time = (time.time() - start) / 100
# Trinomial timing
start = time.time()
for _ in range ( 100 ):
trinomial_tree_price(S, K, T, r, sigma, 100 , q, "put" , "american" )
tri_time = (time.time() - start) / 100
print ( f "Binomial: { bin_time * 1000 :.2f} ms" )
print ( f "Trinomial: { tri_time * 1000 :.2f} ms" )
print ( f "Ratio: { tri_time / bin_time :.2f} x" )
# Output:
# Binomial: 2.34ms
# Trinomial: 3.12ms
# Ratio: 1.33x
Trinomial is ~33% slower per step, but requires fewer steps for accuracy. Use Binomial when:
Simple American options
Speed is critical
N < 100 is acceptable
Use Trinomial when:
Higher accuracy needed
Barrier options (better boundary handling)
Dividends at specific dates
American options with complex features
Monte Carlo Simulation
The Monte Carlo method simulates thousands of random price paths to estimate option values and risk metrics.
Geometric Brownian Motion
Stock price evolution under GBM:
S t + Δ t = S t exp [ ( r − q − σ 2 2 ) Δ t + σ Δ t ⋅ Z ] S_{t+\Delta t} = S_t \exp\left[ \left(r - q - \frac{\sigma^2}{2}\right)\Delta t + \sigma \sqrt{\Delta t} \cdot Z \right] S t + Δ t = S t exp [ ( r − q − 2 σ 2 ) Δ t + σ Δ t ⋅ Z ]
Where Z ∼ N ( 0 , 1 ) Z \sim N(0, 1) Z ∼ N ( 0 , 1 ) is a standard normal random variable.
Implementation
From backend/app/core/monte_carlo.py:1-402:
View Monte Carlo Implementation
import numpy as np
from scipy.stats import norm
class OptionsUpsideAnalyzer :
"""
Analizador que calcula potencial upside usando simulación Monte Carlo
y análisis de volatilidad implícita.
"""
@ staticmethod
def _bs_delta ( S : float , K : float , T : float , r : float , q : float ,
sigma : float , option_type : str = "call" ) -> float :
"""
Return Black-Scholes delta (call or put).
"""
if T <= 0 or sigma <= 0 :
if option_type == "call" :
return 1.0 if S > K else 0.0
else :
return - 1.0 if S < K else 0.0
d1 = (np.log(S / K) + (r - q + 0.5 * sigma ** 2 ) * T) / (sigma * np.sqrt(T))
if option_type == "call" :
return np.exp( - q * T) * norm.cdf(d1)
else :
return np.exp( - q * T) * (norm.cdf(d1) - 1.0 )
def compute_potential_upside_for_ticker (
self ,
ticker : str ,
days_to_exp : int ,
option_type : str = "call" ,
strike : Optional[ float ] = None
) -> Dict[ str , Any]:
"""
Calcula métricas de upside potencial:
- delta (proxy prob ITM)
- precio objetivo upside: low / medium / high
- expected move (market expectation)
"""
info = self .get_iv_for_nearest_contract(ticker, days_to_exp,
option_type, strike)
spot = info[ "spot" ]
iv = info[ "iv" ]
days_actual = info[ "daysToExp" ]
strike_used = info[ "strike" ]
# Compute T in years
T = max ( 0.0 , days_actual / 365.0 )
r = self ._get_risk_free_rate()
q = 0.0 # Dividend yield
if iv is not None and iv > 0 and T > 0 :
# Expected move (1-sigma)
expected_move_pct = iv * np.sqrt(T)
expected_move_abs = spot * expected_move_pct
# Compute delta
delta = self ._bs_delta(spot, strike_used, T, r, q, iv, option_type)
# Define upside targets
upside_low = spot + 0.5 * expected_move_abs
upside_medium = spot + expected_move_abs
upside_high = spot + 1.5 * expected_move_abs
else :
# Fallback to historical volatility
delta = None
upside_low = upside_medium = upside_high = None
return {
"ticker" : ticker,
"spot" : spot,
"strike" : strike_used,
"iv" : iv,
"delta" : delta,
"expected_move_pct" : expected_move_pct,
"upside_low" : upside_low,
"upside_medium" : upside_medium,
"upside_high" : upside_high
}
Expected Move Calculation
The expected move represents the 1-standard-deviation price range:
# Expected move formula
expected_move_pct = iv * np.sqrt(T)
expected_move_abs = spot * expected_move_pct
# Price targets
upside_low = spot + 0.5 * expected_move_abs # 0.5σ move
upside_medium = spot + expected_move_abs # 1σ move
upside_high = spot + 1.5 * expected_move_abs # 1.5σ move
Example:
# SPY trading at $450, 30 days to expiration, 20% IV
S = 450
T = 30 / 365
iv = 0.20
expected_move_pct = iv * np.sqrt(T)
print ( f "Expected Move: { expected_move_pct * 100 :.2f} %" )
# Output: Expected Move: 5.70%
expected_move_abs = S * expected_move_pct
print ( f "Expected Move: $ { expected_move_abs :.2f} " )
# Output: Expected Move: $25.65
print ( f "Low Target: $ { S + 0.5 * expected_move_abs :.2f} " )
print ( f "Medium Target: $ { S + expected_move_abs :.2f} " )
print ( f "High Target: $ { S + 1.5 * expected_move_abs :.2f} " )
# Output:
# Low Target: $462.83
# Medium Target: $475.65
# High Target: $488.48
The expected move is market-implied from options prices via implied volatility. It represents the market’s consensus on likely price movement.
Historical vs Implied Volatility
From backend/app/data/data_manager.py:68-125:
Historical Volatility Calculation
def get_historical_volatility ( self , ticker : str , days : int = 30 ) -> Dict[ str , float ]:
"""
Calcula Estadísticas de Volatilidad Histórica (HV) Anualizada.
"""
try :
# Download 1 year of historical data
t = yf.Ticker(ticker)
end_date = datetime.now()
start_date = end_date - timedelta( days = 365 )
hist = t.history( start = start_date, end = end_date)
# Calculate log returns
hist[ 'LogReturn' ] = np.log(hist[ 'Close' ] / hist[ 'Close' ].shift( 1 ))
# Rolling 30-day HV (annualized)
window = 30
hist[ 'RollingHV' ] = hist[ 'LogReturn' ].rolling( window = window).std() * np.sqrt( 252 )
rolling_series = hist[ 'RollingHV' ].dropna()
current_hv = rolling_series.iloc[ - 1 ]
mean_hv = rolling_series.mean()
std_hv = rolling_series.std()
min_hv = rolling_series.min()
max_hv = rolling_series.max()
# Percentile of current HV
percentile = (rolling_series < current_hv).mean() * 100
return {
"current_hv" : float (current_hv),
"mean_hv" : float (mean_hv),
"std_hv" : float (std_hv),
"min_hv" : float (min_hv),
"max_hv" : float (max_hv),
"percentile" : float (percentile)
}
except Exception as e:
logger.error( f "Error calculating HV: { e } " )
return {}
Comparing HV vs IV:
# Example: AAPL analysis
from app.data.data_manager import OptionsDataManager
manager = OptionsDataManager()
# Get historical volatility stats
hv_stats = manager.get_historical_volatility( "AAPL" , days = 30 )
print ( f "Current HV: { hv_stats[ 'current_hv' ] * 100 :.2f} %" )
print ( f "Mean HV: { hv_stats[ 'mean_hv' ] * 100 :.2f} %" )
print ( f "Percentile: { hv_stats[ 'percentile' ] :.1f} th" )
# Get implied volatility from options
from app.core.monte_carlo import OptionsUpsideAnalyzer
analyzer = OptionsUpsideAnalyzer([ "AAPL" ])
result = analyzer.compute_potential_upside_for_ticker( "AAPL" , days_to_exp = 30 )
print ( f "Implied Vol: { result[ 'iv' ] * 100 :.2f} %" )
# Compare
iv_hv_ratio = result[ 'iv' ] / hv_stats[ 'current_hv' ]
print ( f "IV/HV Ratio: { iv_hv_ratio :.2f} " )
if iv_hv_ratio > 1.2 :
print ( "⚠️ Options are expensive (IV > HV)" )
elif iv_hv_ratio < 0.8 :
print ( "✅ Options are cheap (IV < HV)" )
else :
print ( "➖ Options fairly priced" )
Important distinctions:
Historical Volatility (HV) : Backward-looking, calculated from past price movements
Implied Volatility (IV) : Forward-looking, extracted from current option prices
IV > HV : Market expects more volatility than historical (expensive options)
IV < HV : Market expects less volatility than historical (cheap options)
Model Selection Guide
European Options
American Options
Path-Dependent
Best choice: Black-Scholes from app.core.black_scholes import bsm_price, bsm_greeks
# Fast, analytical solution
price = bsm_price( S = 100 , K = 105 , T = 0.25 , r = 0.05 , sigma = 0.30 , q = 0.02 , kind = "call" )
greeks = bsm_greeks( S = 100 , K = 105 , T = 0.25 , r = 0.05 , sigma = 0.30 , q = 0.02 , kind = "call" )
Pros:
Instant calculation (closed-form)
Analytical Greeks
No numerical error
Cons:
European exercise only
Constant volatility assumption
Best choice: Binomial or Trinomial from app.core.black_scholes import binomial_tree_price, trinomial_tree_price
# American put (early exercise possible)
bin_price = binomial_tree_price( S = 100 , K = 105 , T = 0.25 , r = 0.05 , sigma = 0.30 ,
N = 100 , q = 0.02 , kind = "put" , style = "american" )
tri_price = trinomial_tree_price( S = 100 , K = 105 , T = 0.25 , r = 0.05 , sigma = 0.30 ,
N = 100 , q = 0.02 , kind = "put" , style = "american" )
Use Binomial when:
Standard American options
Speed > accuracy
Use Trinomial when:
Need higher accuracy
Barrier features
Best choice: Monte Carlo from app.core.monte_carlo import OptionsUpsideAnalyzer
analyzer = OptionsUpsideAnalyzer([ "SPY" ])
result = analyzer.compute_potential_upside_for_ticker(
ticker = "SPY" ,
days_to_exp = 30 ,
option_type = "call"
)
Use for:
Scenario analysis
Risk assessment
Expected move calculations
Complex payoff structures
import time
import numpy as np
S, K, T, r, sigma, q = 100 , 100 , 0.25 , 0.05 , 0.30 , 0.02
# Black-Scholes
start = time.time()
for _ in range ( 10000 ):
bsm_price(S, K, T, r, sigma, q, "call" )
bs_time = (time.time() - start) / 10000
print ( f "Black-Scholes: { bs_time * 1e6 :.2f} μs" )
# Output: Black-Scholes: 15.23 μs
# Binomial (N=100)
start = time.time()
for _ in range ( 1000 ):
binomial_tree_price(S, K, T, r, sigma, 100 , q, "call" , "american" )
bin_time = (time.time() - start) / 1000
print ( f "Binomial N=100: { bin_time * 1e3 :.2f} ms" )
# Output: Binomial N=100: 2.34 ms
# Trinomial (N=100)
start = time.time()
for _ in range ( 1000 ):
trinomial_tree_price(S, K, T, r, sigma, 100 , q, "call" , "american" )
tri_time = (time.time() - start) / 1000
print ( f "Trinomial N=100: { tri_time * 1e3 :.2f} ms" )
# Output: Trinomial N=100: 3.12 ms
print ( f " \n Speed comparison (relative to BS):" )
print ( f "Binomial: { bin_time / bs_time :.0f} x slower" )
print ( f "Trinomial: { tri_time / bs_time :.0f} x slower" )
# Output:
# Binomial: 154x slower
# Trinomial: 205x slower
Numerical Stability
Handling Edge Cases
def safe_price ( S , K , T , r , sigma , q , kind ):
"""
Wrapper with robust edge case handling.
"""
# Expired option
if T <= 0 :
intrinsic = max ( 0 , (S - K) if kind == "call" else (K - S))
return intrinsic
# Zero or negative volatility
if sigma <= 0 :
intrinsic = max ( 0 , (S - K) if kind == "call" else (K - S))
return intrinsic
# Zero or negative spot
if S <= 0 :
if kind == "call" :
return 0.0
else :
return max ( 0 , K * np.exp( - r * T))
# Zero strike (degenerate case)
if K <= 0 :
if kind == "call" :
return S * np.exp( - q * T)
else :
return 0.0
# Normal case
return bsm_price(S, K, T, r, sigma, q, kind)
Avoiding Numerical Overflow
# For very deep ITM/OTM options, d1 and d2 can be extreme
# Use bounds to prevent overflow in norm.cdf()
def safe_cdf ( x ):
"""Safe cumulative normal with bounds."""
if x > 6.0 :
return 1.0
elif x < - 6.0 :
return 0.0
else :
return norm.cdf(x)
Next Steps
Black-Scholes Details Mathematical derivation and Greeks formulas
Data Sources How market data feeds into pricing models
Architecture System design and integration patterns