Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import yfinance as yf
|
| 3 |
+
import numpy as np
|
| 4 |
+
import pandas as pd
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from scipy.optimize import brentq
|
| 7 |
+
from scipy.stats import norm, gaussian_kde
|
| 8 |
+
from scipy.interpolate import splrep, BSpline
|
| 9 |
+
from scipy.integrate import simps
|
| 10 |
+
import plotly.graph_objects as go
|
| 11 |
+
from plotly.subplots import make_subplots
|
| 12 |
+
|
| 13 |
+
st.set_page_config(layout="wide", page_title="Forward-Looking Probability")
|
| 14 |
+
|
| 15 |
+
st.markdown("# Forward-Looking Probability Distribution")
|
| 16 |
+
st.write(
|
| 17 |
+
"This application analyzes the implied probability distribution of a stock's future price using call option data. "
|
| 18 |
+
"It calculates implied volatilities via the Black-Scholes model, derives a risk-neutral probability density function using "
|
| 19 |
+
"the Breeden-Litzenberger formula, and then smooths the result with Kernel Density Estimation (KDE). "
|
| 20 |
+
"A unified strike grid is used for the 3D surface, while 2D analysis focuses on individual expiration dates."
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
with st.expander("How It Works", expanded=False):
|
| 24 |
+
st.write("The analysis is based on the Black-Scholes model for European call options:")
|
| 25 |
+
st.latex(r"C(S,K,T,r,\sigma)=S\Phi(d_1)-Ke^{-rT}\Phi(d_2)")
|
| 26 |
+
st.latex(r"d_1=\frac{\ln\left(\frac{S}{K}\right)+(r+0.5\sigma^2)T}{\sigma\sqrt{T}}")
|
| 27 |
+
st.latex(r"d_2=d_1-\sigma\sqrt{T}")
|
| 28 |
+
st.write("The risk-neutral probability density function (PDF) is derived using the Breeden-Litzenberger formula:")
|
| 29 |
+
st.latex(r"\text{PDF}(K)=e^{rT}\frac{\partial^2C}{\partial K^2}")
|
| 30 |
+
st.write("The resulting PDF is then smoothed using Kernel Density Estimation (KDE).")
|
| 31 |
+
|
| 32 |
+
# =============================================================================
|
| 33 |
+
# SIDEBAR - General Settings
|
| 34 |
+
# =============================================================================
|
| 35 |
+
with st.sidebar.expander("General Settings", expanded=True):
|
| 36 |
+
ticker_input = st.text_input("Ticker Symbol", value="NVDA")
|
| 37 |
+
lower_pct = st.number_input(
|
| 38 |
+
"Price Decrease Percentage", value=10, min_value=1, max_value=100, step=1,
|
| 39 |
+
help="For 2D plots: lower threshold = current price * (1 - percentage/100)"
|
| 40 |
+
)
|
| 41 |
+
upper_pct = st.number_input(
|
| 42 |
+
"Price Increase Percentage", value=10, min_value=1, max_value=100, step=1,
|
| 43 |
+
help="For 2D plots: upper threshold = current price * (1 + percentage/100)"
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# =============================================================================
|
| 47 |
+
# SIDEBAR - Advanced Settings
|
| 48 |
+
# =============================================================================
|
| 49 |
+
with st.sidebar.expander("Advanced Settings", expanded=True):
|
| 50 |
+
risk_free = st.number_input(
|
| 51 |
+
"Risk-Free Rate", value=0.04, step=0.01, format="%.2f",
|
| 52 |
+
help="The annualized risk-free rate used in option pricing."
|
| 53 |
+
)
|
| 54 |
+
min_volume = st.number_input(
|
| 55 |
+
"Minimum Volume", value=20, step=1,
|
| 56 |
+
help="Minimum trading volume required for an option to be considered liquid."
|
| 57 |
+
)
|
| 58 |
+
max_spread_ratio = st.number_input(
|
| 59 |
+
"Max Spread Ratio", value=0.2, step=0.01, format="%.2f",
|
| 60 |
+
help="Maximum acceptable ratio of bid-ask spread to ask price. Options exceeding this will be excluded."
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# =============================================================================
|
| 64 |
+
# Run Analysis Button (placed outside the expanders)
|
| 65 |
+
# =============================================================================
|
| 66 |
+
run_analysis = st.sidebar.button("Run Analysis")
|
| 67 |
+
|
| 68 |
+
# =============================================================================
|
| 69 |
+
# HELPER FUNCTIONS
|
| 70 |
+
# =============================================================================
|
| 71 |
+
def call_bs_price(S, K, T, r, sigma):
|
| 72 |
+
if T <= 0:
|
| 73 |
+
return max(S - K, 0)
|
| 74 |
+
d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
|
| 75 |
+
d2 = d1 - sigma * np.sqrt(T)
|
| 76 |
+
return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
|
| 77 |
+
|
| 78 |
+
def implied_vol_call(price, S, K, T, r):
|
| 79 |
+
if T <= 0:
|
| 80 |
+
return np.nan
|
| 81 |
+
def f(iv):
|
| 82 |
+
return call_bs_price(S, K, T, r, iv) - price
|
| 83 |
+
try:
|
| 84 |
+
return brentq(f, 1e-9, 5.0)
|
| 85 |
+
except:
|
| 86 |
+
return np.nan
|
| 87 |
+
|
| 88 |
+
def build_pdf(K_grid, iv_spline_tck, S, T, r):
|
| 89 |
+
iv_vals = BSpline(*iv_spline_tck)(K_grid)
|
| 90 |
+
call_prices = [call_bs_price(S, K, T, r, iv) for K, iv in zip(K_grid, iv_vals)]
|
| 91 |
+
dC_dK = np.gradient(call_prices, K_grid)
|
| 92 |
+
d2C_dK2 = np.gradient(dC_dK, K_grid)
|
| 93 |
+
pdf_raw = np.exp(r * T) * d2C_dK2
|
| 94 |
+
return np.clip(pdf_raw, 0, None)
|
| 95 |
+
|
| 96 |
+
def build_cdf(K_grid, pdf_vals):
|
| 97 |
+
cdf_vals = []
|
| 98 |
+
running = 0.0
|
| 99 |
+
for i in range(len(K_grid)):
|
| 100 |
+
if i == 0:
|
| 101 |
+
cdf_vals.append(0.0)
|
| 102 |
+
else:
|
| 103 |
+
area = simps(pdf_vals[i-1:i+1], K_grid[i-1:i+1])
|
| 104 |
+
running += area
|
| 105 |
+
cdf_vals.append(running)
|
| 106 |
+
cdf_vals = np.array(cdf_vals)
|
| 107 |
+
if cdf_vals[-1] > 0:
|
| 108 |
+
cdf_vals /= cdf_vals[-1]
|
| 109 |
+
return cdf_vals
|
| 110 |
+
|
| 111 |
+
# Function to filter illiquid options
|
| 112 |
+
def filter_liquid_options(df, min_volume=20, max_spread_ratio=0.2):
|
| 113 |
+
spread = df["ask"] - df["bid"]
|
| 114 |
+
return df[(spread / df["ask"] < max_spread_ratio) & (df["bid"] > 0) & (df["volume"] >= min_volume)]
|
| 115 |
+
|
| 116 |
+
# =============================================================================
|
| 117 |
+
# 3D ANALYSIS FUNCTION (CALLS ONLY)
|
| 118 |
+
# =============================================================================
|
| 119 |
+
def compute_3d_pdf(data_ticker, current_price, r, min_volume, max_spread_ratio):
|
| 120 |
+
all_expirations = data_ticker.options
|
| 121 |
+
valid_expiries = []
|
| 122 |
+
days_list = []
|
| 123 |
+
calls_data_dict = {}
|
| 124 |
+
|
| 125 |
+
# First pass: collect calls data from valid expiries.
|
| 126 |
+
for exp_date in all_expirations:
|
| 127 |
+
try:
|
| 128 |
+
expiry_dt = datetime.strptime(exp_date, "%Y-%m-%d")
|
| 129 |
+
except:
|
| 130 |
+
continue
|
| 131 |
+
days_forward = (expiry_dt - datetime.now()).days
|
| 132 |
+
if days_forward < 1:
|
| 133 |
+
continue
|
| 134 |
+
try:
|
| 135 |
+
chain = data_ticker.option_chain(exp_date)
|
| 136 |
+
except Exception:
|
| 137 |
+
continue
|
| 138 |
+
calls_df = chain.calls[['strike', 'lastPrice', 'bid', 'ask', 'volume']].dropna().copy()
|
| 139 |
+
calls_df = filter_liquid_options(calls_df, min_volume, max_spread_ratio)
|
| 140 |
+
calls_df = calls_df[calls_df['lastPrice'] > 0].sort_values('strike')
|
| 141 |
+
if calls_df.empty:
|
| 142 |
+
continue
|
| 143 |
+
valid_expiries.append(exp_date)
|
| 144 |
+
days_list.append(days_forward)
|
| 145 |
+
calls_data_dict[exp_date] = calls_df
|
| 146 |
+
|
| 147 |
+
if not valid_expiries:
|
| 148 |
+
raise ValueError("No valid expiries with call data.")
|
| 149 |
+
|
| 150 |
+
K_grid_3d = np.linspace(current_price * 0.25, current_price * 3, 300)
|
| 151 |
+
pdf_list = []
|
| 152 |
+
|
| 153 |
+
# Second pass: compute smoothed PDF for each expiry.
|
| 154 |
+
for exp_date in valid_expiries:
|
| 155 |
+
expiry_dt = datetime.strptime(exp_date, "%Y-%m-%d")
|
| 156 |
+
T_val = (expiry_dt - datetime.now()).days / 365.0
|
| 157 |
+
calls_df = calls_data_dict[exp_date]
|
| 158 |
+
iv_vals = []
|
| 159 |
+
for _, row in calls_df.iterrows():
|
| 160 |
+
vol = implied_vol_call(row['lastPrice'], current_price, row['strike'], T_val, r)
|
| 161 |
+
iv_vals.append(vol)
|
| 162 |
+
calls_df['iv'] = iv_vals
|
| 163 |
+
calls_df.dropna(subset=['iv'], inplace=True)
|
| 164 |
+
if calls_df.empty:
|
| 165 |
+
pdf_list.append(np.zeros_like(K_grid_3d))
|
| 166 |
+
continue
|
| 167 |
+
strikes = calls_df['strike'].values
|
| 168 |
+
ivs = calls_df['iv'].values
|
| 169 |
+
try:
|
| 170 |
+
iv_spline_tck = splrep(strikes, ivs, s=10, k=3)
|
| 171 |
+
except Exception:
|
| 172 |
+
pdf_list.append(np.zeros_like(K_grid_3d))
|
| 173 |
+
continue
|
| 174 |
+
pdf_raw = build_pdf(K_grid_3d, iv_spline_tck, current_price, T_val, r)
|
| 175 |
+
try:
|
| 176 |
+
kde = gaussian_kde(K_grid_3d, weights=pdf_raw)
|
| 177 |
+
pdf_smooth = kde(K_grid_3d)
|
| 178 |
+
area = np.trapz(pdf_smooth, K_grid_3d)
|
| 179 |
+
if area > 0:
|
| 180 |
+
pdf_smooth /= area
|
| 181 |
+
except Exception:
|
| 182 |
+
pdf_smooth = pdf_raw
|
| 183 |
+
pdf_list.append(pdf_smooth)
|
| 184 |
+
|
| 185 |
+
pdf_matrix = np.array(pdf_list)
|
| 186 |
+
days_array = np.array(days_list)
|
| 187 |
+
|
| 188 |
+
TT, KK = np.meshgrid(days_array, K_grid_3d, indexing='ij')
|
| 189 |
+
|
| 190 |
+
fig = go.Figure(data=[go.Surface(
|
| 191 |
+
x=KK,
|
| 192 |
+
y=TT,
|
| 193 |
+
z=pdf_matrix,
|
| 194 |
+
colorscale='Viridis',
|
| 195 |
+
opacity=0.8
|
| 196 |
+
)])
|
| 197 |
+
fig.update_layout(
|
| 198 |
+
scene=dict(
|
| 199 |
+
xaxis_title='Strike',
|
| 200 |
+
yaxis_title='Days to Expiry',
|
| 201 |
+
zaxis_title='PDF'
|
| 202 |
+
),
|
| 203 |
+
title="3D Smoothed Implied PDF Across Expiries",
|
| 204 |
+
width=900,
|
| 205 |
+
height=700
|
| 206 |
+
)
|
| 207 |
+
fig.add_annotation(
|
| 208 |
+
x=0.98, y=0.98, xref="paper", yref="paper",
|
| 209 |
+
text=f"Current Price: {current_price:.2f}",
|
| 210 |
+
showarrow=False,
|
| 211 |
+
align="right",
|
| 212 |
+
font=dict(size=12),
|
| 213 |
+
bordercolor="black",
|
| 214 |
+
borderwidth=1,
|
| 215 |
+
bgcolor="white",
|
| 216 |
+
opacity=0.8
|
| 217 |
+
)
|
| 218 |
+
return fig, valid_expiries
|
| 219 |
+
|
| 220 |
+
# =============================================================================
|
| 221 |
+
# 2D ANALYSIS FUNCTION (CALLS ONLY)
|
| 222 |
+
# =============================================================================
|
| 223 |
+
def compute_2d_pdf(exp_date, data_ticker, current_price, r, lower_pct, upper_pct, min_volume, max_spread_ratio):
|
| 224 |
+
try:
|
| 225 |
+
expiry_dt = datetime.strptime(exp_date, "%Y-%m-%d")
|
| 226 |
+
except:
|
| 227 |
+
return None
|
| 228 |
+
days_forward = (expiry_dt - datetime.now()).days
|
| 229 |
+
if days_forward < 1:
|
| 230 |
+
return None
|
| 231 |
+
T_val = days_forward / 365.0
|
| 232 |
+
try:
|
| 233 |
+
chain = data_ticker.option_chain(exp_date)
|
| 234 |
+
except:
|
| 235 |
+
return None
|
| 236 |
+
calls_df = chain.calls[['strike', 'lastPrice', 'bid', 'ask', 'volume']].dropna().copy()
|
| 237 |
+
calls_df = filter_liquid_options(calls_df, min_volume, max_spread_ratio)
|
| 238 |
+
calls_df = calls_df[calls_df['lastPrice'] > 0].sort_values('strike')
|
| 239 |
+
if calls_df.empty:
|
| 240 |
+
return None
|
| 241 |
+
|
| 242 |
+
iv_list = []
|
| 243 |
+
for _, row in calls_df.iterrows():
|
| 244 |
+
vol = implied_vol_call(row['lastPrice'], current_price, row['strike'], T_val, r)
|
| 245 |
+
iv_list.append(vol)
|
| 246 |
+
calls_df['iv'] = iv_list
|
| 247 |
+
calls_df.dropna(subset=['iv'], inplace=True)
|
| 248 |
+
if calls_df.empty:
|
| 249 |
+
return None
|
| 250 |
+
|
| 251 |
+
strikes = calls_df['strike'].values
|
| 252 |
+
ivs = calls_df['iv'].values
|
| 253 |
+
try:
|
| 254 |
+
iv_spline_tck = splrep(strikes, ivs, s=10, k=3)
|
| 255 |
+
except:
|
| 256 |
+
return None
|
| 257 |
+
|
| 258 |
+
K_min = strikes.min()
|
| 259 |
+
K_max = strikes.max()
|
| 260 |
+
K_grid_2d = np.linspace(K_min, K_max, 300)
|
| 261 |
+
pdf_raw = build_pdf(K_grid_2d, iv_spline_tck, current_price, T_val, r)
|
| 262 |
+
try:
|
| 263 |
+
kde = gaussian_kde(K_grid_2d, weights=pdf_raw)
|
| 264 |
+
pdf_smooth = kde(K_grid_2d)
|
| 265 |
+
area = np.trapz(pdf_smooth, K_grid_2d)
|
| 266 |
+
if area > 0:
|
| 267 |
+
pdf_smooth /= area
|
| 268 |
+
except:
|
| 269 |
+
pdf_smooth = pdf_raw
|
| 270 |
+
|
| 271 |
+
cdf = build_cdf(K_grid_2d, pdf_smooth)
|
| 272 |
+
|
| 273 |
+
lower_thresh = current_price * (1 - lower_pct / 100)
|
| 274 |
+
upper_thresh = current_price * (1 + upper_pct / 100)
|
| 275 |
+
mask_below = K_grid_2d < lower_thresh
|
| 276 |
+
mask_between = (K_grid_2d >= lower_thresh) & (K_grid_2d <= upper_thresh)
|
| 277 |
+
mask_above = K_grid_2d > upper_thresh
|
| 278 |
+
p_below = np.trapz(pdf_smooth[mask_below], K_grid_2d[mask_below])
|
| 279 |
+
p_between = np.trapz(pdf_smooth[mask_between], K_grid_2d[mask_between])
|
| 280 |
+
p_above = np.trapz(pdf_smooth[mask_above], K_grid_2d[mask_above])
|
| 281 |
+
|
| 282 |
+
fig_pdf_cdf = make_subplots(rows=1, cols=2, subplot_titles=("Smoothed PDF", "Smoothed CDF"))
|
| 283 |
+
fig_pdf_cdf.add_trace(go.Scatter(
|
| 284 |
+
x=K_grid_2d, y=pdf_smooth, mode='lines', name='PDF', line=dict(color='blue')
|
| 285 |
+
), row=1, col=1)
|
| 286 |
+
fig_pdf_cdf.add_vline(x=current_price, line=dict(color='red', dash='dash'), row=1, col=1)
|
| 287 |
+
fig_pdf_cdf.update_xaxes(title_text="Strike", row=1, col=1)
|
| 288 |
+
fig_pdf_cdf.update_yaxes(title_text="PDF", row=1, col=1)
|
| 289 |
+
fig_pdf_cdf.add_trace(go.Scatter(
|
| 290 |
+
x=K_grid_2d, y=cdf, mode='lines', name='CDF', line=dict(color='blue')
|
| 291 |
+
), row=1, col=2)
|
| 292 |
+
fig_pdf_cdf.add_vline(x=current_price, line=dict(color='red', dash='dash'), row=1, col=2)
|
| 293 |
+
fig_pdf_cdf.update_xaxes(title_text="Strike", row=1, col=2)
|
| 294 |
+
fig_pdf_cdf.update_yaxes(title_text="CDF", row=1, col=2)
|
| 295 |
+
fig_pdf_cdf.update_layout(title_text="2D Analysis: PDF and CDF")
|
| 296 |
+
fig_pdf_cdf.add_annotation(
|
| 297 |
+
x=0.98, y=0.98, xref="paper", yref="paper",
|
| 298 |
+
text=f"Current Price: {current_price:.2f}",
|
| 299 |
+
showarrow=False,
|
| 300 |
+
align="right",
|
| 301 |
+
font=dict(size=12),
|
| 302 |
+
bordercolor="white",
|
| 303 |
+
borderwidth=1,
|
| 304 |
+
opacity=0.8
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
fig_threshold = go.Figure()
|
| 308 |
+
fig_threshold.add_trace(go.Scatter(
|
| 309 |
+
x=K_grid_2d, y=pdf_smooth, mode='lines', name='PDF', line=dict(color='blue')
|
| 310 |
+
))
|
| 311 |
+
fig_threshold.add_vline(
|
| 312 |
+
x=lower_thresh,
|
| 313 |
+
line=dict(color='orange', dash='dash'),
|
| 314 |
+
annotation_text=f'Lower: {lower_thresh:.2f}',
|
| 315 |
+
annotation_position="top left",
|
| 316 |
+
annotation_xshift=10,
|
| 317 |
+
annotation_yshift=-10
|
| 318 |
+
)
|
| 319 |
+
fig_threshold.add_vline(
|
| 320 |
+
x=upper_thresh,
|
| 321 |
+
line=dict(color='purple', dash='dash'),
|
| 322 |
+
annotation_text=f'Upper: {upper_thresh:.2f}',
|
| 323 |
+
annotation_position="top right",
|
| 324 |
+
annotation_xshift=-10,
|
| 325 |
+
annotation_yshift=-10
|
| 326 |
+
)
|
| 327 |
+
fig_threshold.add_vline(
|
| 328 |
+
x=current_price,
|
| 329 |
+
line=dict(color='red', dash='dash'),
|
| 330 |
+
annotation_text=f'Current: {current_price:.2f}',
|
| 331 |
+
annotation_position="bottom right",
|
| 332 |
+
annotation_xshift=-10,
|
| 333 |
+
annotation_yshift=10
|
| 334 |
+
)
|
| 335 |
+
fig_threshold.add_trace(go.Scatter(
|
| 336 |
+
x=K_grid_2d[mask_below], y=pdf_smooth[mask_below], mode='lines',
|
| 337 |
+
fill='tozeroy', line=dict(color='lightblue'), showlegend=False
|
| 338 |
+
))
|
| 339 |
+
fig_threshold.add_trace(go.Scatter(
|
| 340 |
+
x=K_grid_2d[mask_between], y=pdf_smooth[mask_between], mode='lines',
|
| 341 |
+
fill='tozeroy', line=dict(color='lightgrey'), showlegend=False
|
| 342 |
+
))
|
| 343 |
+
fig_threshold.add_trace(go.Scatter(
|
| 344 |
+
x=K_grid_2d[mask_above], y=pdf_smooth[mask_above], mode='lines',
|
| 345 |
+
fill='tozeroy', line=dict(color='lightcoral'), showlegend=False
|
| 346 |
+
))
|
| 347 |
+
fig_threshold.update_layout(
|
| 348 |
+
title="Threshold Probability Plot",
|
| 349 |
+
xaxis_title="Strike",
|
| 350 |
+
yaxis_title="PDF"
|
| 351 |
+
)
|
| 352 |
+
annotation_text = (
|
| 353 |
+
f"Probability below {lower_thresh:.2f} is {p_below:.2%}<br>"
|
| 354 |
+
f"Probability between {lower_thresh:.2f} and {upper_thresh:.2f} is {p_between:.2%}<br>"
|
| 355 |
+
f"Probability above {upper_thresh:.2f} is {p_above:.2%}"
|
| 356 |
+
)
|
| 357 |
+
fig_threshold.add_annotation(
|
| 358 |
+
x=0.75, y=0.5, xref="paper", yref="paper",
|
| 359 |
+
text=annotation_text,
|
| 360 |
+
showarrow=False,
|
| 361 |
+
align="left",
|
| 362 |
+
font=dict(size=12),
|
| 363 |
+
bordercolor="white",
|
| 364 |
+
borderwidth=1,
|
| 365 |
+
opacity=0.8
|
| 366 |
+
)
|
| 367 |
+
fig_threshold.add_annotation(
|
| 368 |
+
x=0.98, y=0.98, xref="paper", yref="paper",
|
| 369 |
+
text=f"Current Price: {current_price:.2f}",
|
| 370 |
+
showarrow=False,
|
| 371 |
+
align="right",
|
| 372 |
+
font=dict(size=12),
|
| 373 |
+
bordercolor="black",
|
| 374 |
+
borderwidth=1,
|
| 375 |
+
bgcolor="white",
|
| 376 |
+
opacity=0.8
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
result = {
|
| 380 |
+
"K_grid_2d": K_grid_2d,
|
| 381 |
+
"pdf_smooth": pdf_smooth,
|
| 382 |
+
"cdf": cdf,
|
| 383 |
+
"lower_thresh": lower_thresh,
|
| 384 |
+
"upper_thresh": upper_thresh,
|
| 385 |
+
"p_below": p_below,
|
| 386 |
+
"p_between": p_between,
|
| 387 |
+
"p_above": p_above,
|
| 388 |
+
"fig_pdf_cdf": fig_pdf_cdf,
|
| 389 |
+
"fig_threshold": fig_threshold,
|
| 390 |
+
"days_to_exp": days_forward
|
| 391 |
+
}
|
| 392 |
+
return result
|
| 393 |
+
|
| 394 |
+
# =============================================================================
|
| 395 |
+
# MAIN RUN (only run when the button is clicked)
|
| 396 |
+
# =============================================================================
|
| 397 |
+
if run_analysis:
|
| 398 |
+
with st.spinner("Running analysis, please wait..."):
|
| 399 |
+
try:
|
| 400 |
+
data_ticker = yf.Ticker(ticker_input)
|
| 401 |
+
hist_data = data_ticker.history(period="1d")
|
| 402 |
+
if hist_data.empty:
|
| 403 |
+
st.error("No price data found.")
|
| 404 |
+
st.stop()
|
| 405 |
+
current_price = hist_data["Close"].iloc[-1]
|
| 406 |
+
except Exception as e:
|
| 407 |
+
st.error(f"Error fetching data: {e}")
|
| 408 |
+
st.stop()
|
| 409 |
+
|
| 410 |
+
st.write(f"Current Price: {round(current_price, 2)}")
|
| 411 |
+
r = risk_free
|
| 412 |
+
|
| 413 |
+
try:
|
| 414 |
+
fig3d, valid_expiries_3d = compute_3d_pdf(data_ticker, current_price, r, min_volume, max_spread_ratio)
|
| 415 |
+
except Exception as e:
|
| 416 |
+
st.error(f"3D analysis error: {e}")
|
| 417 |
+
st.stop()
|
| 418 |
+
|
| 419 |
+
results_2d = {}
|
| 420 |
+
for exp_date in data_ticker.options:
|
| 421 |
+
res = compute_2d_pdf(exp_date, data_ticker, current_price, r, lower_pct, upper_pct, min_volume, max_spread_ratio)
|
| 422 |
+
if res is not None:
|
| 423 |
+
results_2d[exp_date] = res
|
| 424 |
+
|
| 425 |
+
if not results_2d:
|
| 426 |
+
st.error("No valid expirations for 2D analysis.")
|
| 427 |
+
st.stop()
|
| 428 |
+
|
| 429 |
+
st.session_state.analysis_data = {
|
| 430 |
+
"current_price": current_price,
|
| 431 |
+
"expirations": list(results_2d.keys()),
|
| 432 |
+
"results": results_2d,
|
| 433 |
+
"fig3d": fig3d
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
# =============================================================================
|
| 437 |
+
# DISPLAY RESULTS (if analysis data exists)
|
| 438 |
+
# =============================================================================
|
| 439 |
+
if "analysis_data" in st.session_state:
|
| 440 |
+
ad = st.session_state.analysis_data
|
| 441 |
+
st.write(f"**Current Price:** {round(ad['current_price'], 2)}")
|
| 442 |
+
st.markdown("## 3D Probability Surface")
|
| 443 |
+
st.plotly_chart(ad["fig3d"], use_container_width=True)
|
| 444 |
+
st.markdown("## 2D Plots for Selected Expiration Date")
|
| 445 |
+
chosen = st.selectbox("Choose expiration date:", options=ad["expirations"])
|
| 446 |
+
res2d = ad["results"][chosen]
|
| 447 |
+
st.plotly_chart(res2d["fig_pdf_cdf"], use_container_width=True)
|
| 448 |
+
st.plotly_chart(res2d["fig_threshold"], use_container_width=True)
|
| 449 |
+
st.write("The 2D plots use calls data only. The 3D surface uses a unified strike grid.")
|
| 450 |
+
else:
|
| 451 |
+
st.info("Click 'Run Analysis' to start.")
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
hide_streamlit_style = """
|
| 455 |
+
<style>
|
| 456 |
+
#MainMenu {visibility: hidden;}
|
| 457 |
+
footer {visibility: hidden;}
|
| 458 |
+
</style>
|
| 459 |
+
"""
|
| 460 |
+
st.markdown(hide_streamlit_style, unsafe_allow_html=True)
|