aboutsummaryrefslogtreecommitdiff
path: root/components/options.py
blob: e9bf0160fa466257734bea102a924c9d23c784fc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
"""Options flow — put/call ratios, IV smile, open interest by strike."""
import pandas as pd
import plotly.graph_objects as go
import streamlit as st
from services.data_service import get_company_info, get_options_chain


def render_options(ticker: str):
    info = get_company_info(ticker)
    current_price = info.get("currentPrice") or info.get("regularMarketPrice")

    with st.spinner("Loading options data…"):
        data = get_options_chain(ticker)

    if not data or not data.get("chains"):
        st.info("Options data unavailable for this ticker.")
        return

    expirations = data["expirations"]
    chains = data["chains"]

    # ── Expiry selector ──────────────────────────────────────────────────────
    selected_expiry = st.selectbox(
        "Expiration date",
        options=expirations,
        key=f"options_expiry_{ticker}",
    )

    chain_data = next((c for c in chains if c["expiry"] == selected_expiry), None)
    if chain_data is None:
        # Expiry beyond the pre-fetched set — fetch on demand
        try:
            import yfinance as yf
            t = yf.Ticker(ticker.upper())
            chain = t.option_chain(selected_expiry)
            chain_data = {"expiry": selected_expiry, "calls": chain.calls, "puts": chain.puts}
        except Exception:
            st.info("Could not load chain for this expiry.")
            return

    calls: pd.DataFrame = chain_data["calls"].copy()
    puts: pd.DataFrame = chain_data["puts"].copy()

    # ── Summary metrics ──────────────────────────────────────────────────────
    total_call_vol = float(calls["volume"].sum()) if "volume" in calls.columns else 0.0
    total_put_vol = float(puts["volume"].sum()) if "volume" in puts.columns else 0.0
    total_call_oi = float(calls["openInterest"].sum()) if "openInterest" in calls.columns else 0.0
    total_put_oi = float(puts["openInterest"].sum()) if "openInterest" in puts.columns else 0.0

    pc_vol = total_put_vol / total_call_vol if total_call_vol > 0 else None
    pc_oi = total_put_oi / total_call_oi if total_call_oi > 0 else None

    def _pc_delta(val):
        if val is None:
            return None
        if val < 0.7:
            return "Bullish"
        if val < 1.0:
            return "Neutral"
        return "Bearish"

    m1, m2, m3, m4 = st.columns(4)
    m1.metric(
        "P/C Ratio (Volume)",
        f"{pc_vol:.2f}" if pc_vol is not None else "—",
        delta=_pc_delta(pc_vol),
        delta_color="inverse" if pc_vol and pc_vol >= 1.0 else "normal",
        help="Put/Call volume ratio. >1 = more put activity (bearish bets).",
    )
    m2.metric(
        "P/C Ratio (OI)",
        f"{pc_oi:.2f}" if pc_oi is not None else "—",
        delta=_pc_delta(pc_oi),
        delta_color="inverse" if pc_oi and pc_oi >= 1.0 else "normal",
        help="Put/Call open interest ratio.",
    )
    m3.metric("Total Call Volume", f"{int(total_call_vol):,}" if total_call_vol else "—")
    m4.metric("Total Put Volume", f"{int(total_put_vol):,}" if total_put_vol else "—")

    st.write("")

    # Filter strikes ±30% of current price for cleaner charts
    if current_price and not calls.empty:
        lo, hi = current_price * 0.70, current_price * 1.30
        calls_atm = calls[(calls["strike"] >= lo) & (calls["strike"] <= hi)]
        puts_atm = puts[(puts["strike"] >= lo) & (puts["strike"] <= hi)]
    else:
        calls_atm = calls
        puts_atm = puts

    if calls_atm.empty and puts_atm.empty:
        st.info("No near-the-money options found for this expiry.")
        return

    chart_col1, chart_col2 = st.columns(2)

    # ── IV Smile ─────────────────────────────────────────────────────────────
    with chart_col1:
        if "impliedVolatility" in calls_atm.columns:
            st.markdown("**Implied Volatility Smile**")
            fig_iv = go.Figure()
            fig_iv.add_trace(go.Scatter(
                x=calls_atm["strike"],
                y=calls_atm["impliedVolatility"] * 100,
                name="Calls IV",
                mode="lines+markers",
                line=dict(color="#4F8EF7", width=2),
                marker=dict(size=4),
            ))
            if not puts_atm.empty and "impliedVolatility" in puts_atm.columns:
                fig_iv.add_trace(go.Scatter(
                    x=puts_atm["strike"],
                    y=puts_atm["impliedVolatility"] * 100,
                    name="Puts IV",
                    mode="lines+markers",
                    line=dict(color="#F7A24F", width=2),
                    marker=dict(size=4),
                ))
            if current_price:
                fig_iv.add_vline(
                    x=current_price,
                    line_dash="dash",
                    line_color="rgba(255,255,255,0.35)",
                    annotation_text="ATM",
                    annotation_position="top",
                )
            fig_iv.update_layout(
                yaxis_title="Implied Volatility (%)",
                xaxis_title="Strike",
                plot_bgcolor="rgba(0,0,0,0)",
                paper_bgcolor="rgba(0,0,0,0)",
                margin=dict(l=0, r=0, t=10, b=0),
                height=300,
                legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
                hovermode="x unified",
            )
            st.plotly_chart(fig_iv, use_container_width=True)

    # ── Open Interest by strike ───────────────────────────────────────────────
    with chart_col2:
        if "openInterest" in calls_atm.columns:
            st.markdown("**Open Interest by Strike**")
            fig_oi = go.Figure()
            fig_oi.add_trace(go.Bar(
                x=calls_atm["strike"],
                y=calls_atm["openInterest"].fillna(0),
                name="Calls OI",
                marker_color="rgba(79,142,247,0.75)",
            ))
            if not puts_atm.empty and "openInterest" in puts_atm.columns:
                fig_oi.add_trace(go.Bar(
                    x=puts_atm["strike"],
                    y=-puts_atm["openInterest"].fillna(0),
                    name="Puts OI",
                    marker_color="rgba(247,162,79,0.75)",
                ))
            if current_price:
                fig_oi.add_vline(
                    x=current_price,
                    line_dash="dash",
                    line_color="rgba(255,255,255,0.35)",
                    annotation_text="ATM",
                    annotation_position="top",
                )
            fig_oi.update_layout(
                barmode="overlay",
                yaxis_title="Open Interest (puts mirrored)",
                xaxis_title="Strike",
                plot_bgcolor="rgba(0,0,0,0)",
                paper_bgcolor="rgba(0,0,0,0)",
                margin=dict(l=0, r=0, t=10, b=0),
                height=300,
                legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
            )
            st.plotly_chart(fig_oi, use_container_width=True)

    # ── Raw chain table ───────────────────────────────────────────────────────
    with st.expander("Full options chain"):
        tab_calls, tab_puts = st.tabs(["Calls", "Puts"])
        display_cols = ["strike", "lastPrice", "bid", "ask", "volume", "openInterest", "impliedVolatility"]

        with tab_calls:
            show_cols = [c for c in display_cols if c in calls.columns]
            if show_cols:
                df_show = calls[show_cols].copy()
                if "impliedVolatility" in df_show.columns:
                    df_show["impliedVolatility"] = df_show["impliedVolatility"].apply(
                        lambda v: f"{v*100:.1f}%" if pd.notna(v) else "—"
                    )
                st.dataframe(df_show, use_container_width=True, hide_index=True)

        with tab_puts:
            show_cols = [c for c in display_cols if c in puts.columns]
            if show_cols:
                df_show = puts[show_cols].copy()
                if "impliedVolatility" in df_show.columns:
                    df_show["impliedVolatility"] = df_show["impliedVolatility"].apply(
                        lambda v: f"{v*100:.1f}%" if pd.notna(v) else "—"
                    )
                st.dataframe(df_show, use_container_width=True, hide_index=True)