@@ -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