Skip to content

Commit a09ebfd

Browse files
committed
Add binomial tree to Risk Analysis
1 parent e972a0f commit a09ebfd

1 file changed

Lines changed: 126 additions & 6 deletions

File tree

streamlit_app/pages/risk_analysis.py

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,88 @@ def fallback_monte_carlo(S, K, T, r, sigma, option_type, q=0.0, num_sim=50000, n
267267
logger.error(f"Monte Carlo fallback failed: {str(e)}")
268268
return 0.0
269269

270+
# --- NEW: Binomial Tree Fallback ---
271+
def fallback_binomial_tree(S, K, T, r, sigma, option_type="call", exercise_style="european", q=0.0, num_steps=500):
272+
"""Fallback implementation replicating the BinomialTree.price logic"""
273+
try:
274+
# --- Replicate validation logic ---
275+
if not (isinstance(S, (int,float)) and isinstance(K, (int,float)) and isinstance(T, (int,float)) and
276+
isinstance(r, (int,float)) and isinstance(sigma, (int,float)) and isinstance(q, (int,float))):
277+
logger.error("Binomial Tree fallback: Inputs must be numeric.")
278+
return 0.0
279+
if S <= 0 or K <= 0:
280+
logger.error("Binomial Tree fallback: Spot/strike must be positive.")
281+
return 0.0
282+
if T < 0 or sigma < 0 or q < 0:
283+
logger.error("Binomial Tree fallback: T/sigma/q must be non-negative.")
284+
return 0.0
285+
if option_type not in {"call", "put"}:
286+
logger.error("Binomial Tree fallback: option_type must be 'call' or 'put'.")
287+
return 0.0
288+
if exercise_style not in {"european", "american"}:
289+
logger.error("Binomial Tree fallback: exercise_style must be 'european' or 'american'.")
290+
return 0.0
291+
if num_steps <= 0:
292+
logger.error("Binomial Tree fallback: num_steps must be positive.")
293+
return 0.0
294+
295+
# Handle edge cases
296+
if T == 0:
297+
if option_type == "call":
298+
return float(max(S - K, 0.0))
299+
else: # put
300+
return float(max(K - S, 0.0))
301+
if sigma == 0:
302+
df = np.exp(-r * T)
303+
fwd = S * np.exp((r - q) * T)
304+
if option_type == "call":
305+
intrinsic = max(fwd - K, 0.0)
306+
else: # put
307+
intrinsic = max(K - fwd, 0.0)
308+
return float(intrinsic * df)
309+
310+
# --- Compute tree parameters ---
311+
dt = T / num_steps
312+
u = np.exp(sigma * np.sqrt(dt))
313+
d = 1.0 / u
314+
p = (np.exp((r-q)*dt) - d) / (u-d)
315+
p = min(max(p, 0.0), 1.0) # Clamp probability
316+
317+
# --- Build asset price tree ---
318+
asset_prices = np.empty((num_steps + 1, num_steps + 1), dtype=np.float64)
319+
for i in range(num_steps + 1):
320+
j = np.arange(i + 1)
321+
asset_prices[i, :i+1] = S * (u ** j) * (d ** (i - j))
322+
323+
# --- Backward induction ---
324+
disc = np.exp(-r * dt)
325+
option_values = np.empty_like(asset_prices)
326+
327+
# Terminal payoffs
328+
if option_type == "call":
329+
option_values[-1, :num_steps+1] = np.maximum(asset_prices[-1, :num_steps+1] - K, 0)
330+
else: # put
331+
option_values[-1, :num_steps+1] = np.maximum(K - asset_prices[-1, :num_steps+1], 0)
332+
333+
# Backward induction loop
334+
for step in range(num_steps-1, -1, -1):
335+
option_values[step, :step+1] = disc * (
336+
p * option_values[step+1, 1:step+2] +
337+
(1 - p) * option_values[step+1, :step+1]
338+
)
339+
# American early exercise
340+
if exercise_style == "american":
341+
if option_type == "call":
342+
intrinsic = np.maximum(asset_prices[step, :step+1] - K, 0)
343+
else: # put
344+
intrinsic = np.maximum(K - asset_prices[step, :step+1], 0)
345+
option_values[step, :step+1] = np.maximum(option_values[step, :step+1], intrinsic)
346+
347+
return float(option_values[0,0])
348+
except Exception as e:
349+
logger.error(f"Binomial Tree fallback failed: {str(e)}")
350+
return 0.0
351+
270352
# ======================
271353
# PLOTTING FUNCTIONS
272354
# ======================
@@ -281,7 +363,7 @@ def create_option_pricing_chart(results: List[Dict]) -> go.Figure:
281363
fig.add_trace(go.Bar(
282364
x=[r['model'] for r in valid_results],
283365
y=[r['price'] for r in valid_results],
284-
marker_color=['#3b82f6' if 'Black-Scholes' in r['model'] else '#10b981' for r in valid_results],
366+
marker_color=['#3b82f6' if 'Black-Scholes' in r['model'] else '#10b981' if 'Binomial Tree' in r['model'] else '#ef4444' for r in valid_results], # Added color for Binomial Tree
285367
text=[f"${r['price']:.4f}" for r in valid_results],
286368
textposition='auto',
287369
))
@@ -301,19 +383,20 @@ def create_performance_chart(results: List[Dict]) -> go.Figure:
301383
"""Create performance comparison chart"""
302384
fig = go.Figure()
303385

304-
valid_results = [r for r in results if isinstance(r.get('time_ms'), (int, float)) and 'Prediction' in r['model']]
386+
# Include all valid models for performance comparison, not just 'Prediction'
387+
valid_results = [r for r in results if isinstance(r.get('time_ms'), (int, float))]
305388

306389
if valid_results:
307390
fig.add_trace(go.Bar(
308391
x=[r['model'] for r in valid_results],
309392
y=[r['time_ms'] for r in valid_results],
310-
marker_color='#8b5cf6',
393+
marker_color=['#3b82f6' if 'Black-Scholes' in r['model'] else '#10b981' if 'Binomial Tree' in r['model'] else '#8b5cf6' for r in valid_results], # Added color for Binomial Tree
311394
text=[f"{r['time_ms']:.1f}ms" for r in valid_results],
312395
textposition='auto',
313396
))
314397

315398
fig.update_layout(
316-
title="Model Performance (Prediction Time)",
399+
title="Model Performance (Execution Time)",
317400
xaxis_title="Pricing Model",
318401
yaxis_title="Execution Time (ms)",
319402
template="plotly_dark",
@@ -405,6 +488,10 @@ def create_risk_distribution_plot(returns: pd.Series, var: float, es: float, lev
405488
include_mc_advanced = st.checkbox("Advanced MC", value=True)
406489
with col4:
407490
include_ml = st.checkbox("ML Pricing", value=True)
491+
# --- ADD Binomial Tree Checkbox ---
492+
with col1: # Can add to any column, using col1 here
493+
include_bt = st.checkbox("Binomial Tree", value=True)
494+
# --- END ADD ---
408495
st.markdown('</div>', unsafe_allow_html=True)
409496

410497
# Pricing parameters
@@ -448,6 +535,15 @@ def create_risk_distribution_plot(returns: pd.Series, var: float, es: float, lev
448535
with st.spinner("Running pricing benchmarks..."):
449536
results = []
450537

538+
# Get pricing models
539+
try:
540+
from src.pricing_models.binomial_tree import BinomialTree
541+
binomial_model = BinomialTree(num_steps=500) # Using default steps as per original
542+
logger.info("Successfully imported BinomialTree")
543+
except ImportError as e:
544+
logger.warning(f"BinomialTree import failed: {str(e)}")
545+
binomial_model = None
546+
451547
# Black-Scholes
452548
if include_bs:
453549
price, latency = timeit_ms(fallback_black_scholes, S, K, T, r, sigma, option_type)
@@ -477,7 +573,31 @@ def create_risk_distribution_plot(returns: pd.Series, var: float, es: float, lev
477573
"time_ms": latency,
478574
"type": "Simulation"
479575
})
480-
576+
577+
# --- NEW: Binomial Tree Benchmark ---
578+
if include_bt:
579+
try:
580+
if binomial_model is not None:
581+
price, latency = timeit_ms(
582+
binomial_model.price, S, K, T, r, sigma, option_type, "european", q=0.0
583+
)
584+
else:
585+
# Fallback implementation uses european by default
586+
price, latency = timeit_ms(
587+
fallback_binomial_tree, S, K, T, r, sigma, option_type, "european", q=0.0, num_steps=500 # Using default steps
588+
)
589+
results.append({
590+
"model": "Binomial Tree (Eur)",
591+
"price": price,
592+
"time_ms": latency,
593+
"type": "Lattice"
594+
})
595+
except Exception as e:
596+
logger.error(f"Binomial Tree benchmark failed: {str(e)}")
597+
# Optionally add an error result, or just skip
598+
st.error(f"Binomial Tree benchmark failed: {str(e)}")
599+
# --- END NEW ---
600+
481601
# ML Pricing (simulated)
482602
if include_ml:
483603
# Simulate ML pricing being faster but slightly different
@@ -687,4 +807,4 @@ def create_risk_distribution_plot(returns: pd.Series, var: float, es: float, lev
687807
st.markdown(f'<span style="color: #94a3b8;">#{i}</span>', unsafe_allow_html=True)
688808
st.markdown(f'<span style="color: #ef4444;">{loss:.4%}</span>', unsafe_allow_html=True)
689809
st.markdown('</div>', unsafe_allow_html=True)
690-
st.markdown('</div>', unsafe_allow_html=True)
810+
st.markdown('</div>', unsafe_allow_html=True)

0 commit comments

Comments
 (0)