Ctrl K

Company Analysis

Volatility, earnings impact, profitability, and risk analysis for major NASDAQ-listed companies.

All data is fetched live from two sources:

  • Tiingo API for market prices
  • SEC EDGAR API for company fundamentals and filing-based financial data

Set TICKER in the config cell below to switch companies.

Set TICKER in the config cell below to switch companies.

Section Description
Price & Volatility Snapshot Current vol, regime, percentile rank, vs SPY
Relative Performance Rolling beta, correlation, vol ratio vs SPY
Earnings Impact 3-day window around SEC filing dates
Profitability Trend Gross/operating/net margin, R&D intensity from EDGAR
Tail Risk & Drawdowns Worst days, VaR, drawdown history
Volatility Regime Calibrated regime bands, time in each regime

Requires: TIINGO_API_KEY in .env (same directory as this notebook).

Data sources and setup

This notebook combines live market data and live filing data:

  • Tiingo provides daily adjusted price history used for volatility, beta, drawdowns, and return-based risk analysis
  • SEC EDGAR provides company fundamentals used for profitability and margin trend analysis

To run the notebook, you must provide a Tiingo API key through a local .env file. The code expects this variable to be available:

In [1]:
# -------------------------------------------
# CONFIG - change TICKER to switch companies
# -------------------------------------------

TICKER = 'AAPL'   # AAPL | NVDA | GOOGL | MSFT | META

# Per-ticker SEC EDGAR CIK numbers
CIK_MAP = {
    'AAPL':  '0000320193',
    'NVDA':  '0001045810',
    'GOOGL': '0001652044',
    'MSFT':  '0000789019',
    'META':  '0001326801',
}

# Per-ticker XBRL revenue tag (EDGAR tags differ by company)
REVENUE_TAG_MAP = {
    'AAPL':  ['RevenueFromContractWithCustomerExcludingAssessedTax'],
    'NVDA':  ['Revenues', 'RevenueFromContractWithCustomerExcludingAssessedTax'],
    'GOOGL': ['Revenues', 'RevenueFromContractWithCustomerExcludingAssessedTax'],
    'MSFT':  ['RevenueFromContractWithCustomerExcludingAssessedTax', 'Revenues'],
    'META':  ['RevenueFromContractWithCustomerExcludingAssessedTax', 'Revenues'],
}

# Per-ticker COGS tag
COST_TAG_MAP = {
    'AAPL':  'CostOfGoodsAndServicesSold',
    'NVDA':  'CostOfRevenue',
    'GOOGL': 'CostOfRevenue',
    'MSFT':  'CostOfRevenue',
    'META':  'CostOfRevenue',
}

# Regime bands calibrated per ticker
# NVDA is higher-vol; others are typical mega-cap
REGIME_BANDS_MAP = {
    'AAPL':  [( 0,15,'Low','#22c55e'),(15,25,'Normal','#3b82f6'),(25,35,'Elevated','#f59e0b'),(35,50,'High','#ef4444'),(50,999,'Crisis','#7f1d1d')],
    'NVDA':  [( 0,25,'Low','#22c55e'),(25,40,'Normal','#3b82f6'),(40,60,'Elevated','#f59e0b'),(60,80,'High','#ef4444'),(80,999,'Crisis','#7f1d1d')],
    'GOOGL': [( 0,15,'Low','#22c55e'),(15,25,'Normal','#3b82f6'),(25,35,'Elevated','#f59e0b'),(35,50,'High','#ef4444'),(50,999,'Crisis','#7f1d1d')],
    'MSFT':  [( 0,15,'Low','#22c55e'),(15,25,'Normal','#3b82f6'),(25,35,'Elevated','#f59e0b'),(35,50,'High','#ef4444'),(50,999,'Crisis','#7f1d1d')],
    'META':  [( 0,20,'Low','#22c55e'),(20,35,'Normal','#3b82f6'),(35,50,'Elevated','#f59e0b'),(50,70,'High','#ef4444'),(70,999,'Crisis','#7f1d1d')],
}

# Resolve config for selected ticker
CIK          = CIK_MAP[TICKER]
REVENUE_TAGS = REVENUE_TAG_MAP[TICKER]
COST_TAG     = COST_TAG_MAP[TICKER]
REGIME_BANDS = REGIME_BANDS_MAP[TICKER]  # list of (floor, ceil, label, color)

SEC_USER_AGENT = 'MLNotebooks research@mlnotebooks.io'
BENCHMARK      = 'SPY'
START_DATE     = '2015-01-01'

print(f'Configured for: {TICKER}  (CIK {CIK})')
print(f'Regime bands   : {[(b[2], b[0], b[1]) for b in REGIME_BANDS]}')
Configured for: AAPL  (CIK 0000320193)
Regime bands   : [('Low', 0, 15), ('Normal', 15, 25), ('Elevated', 25, 35), ('High', 35, 50), ('Crisis', 50, 999)]
In [2]:
import os
import numpy as np
import pandas as pd
import requests
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import matplotlib.ticker as mticker
from matplotlib.patches import Patch

from pathlib import Path
from dotenv import load_dotenv
from IPython.display import display, HTML

FL_BLUE   = '#2563eb'
FL_SLATE  = '#64748b'
FL_AMBER  = '#f59e0b'
FL_GREEN  = '#16a34a'
FL_RED    = '#ef4444'
FL_BG     = '#ffffff'
FL_GRID   = '#e2e8f0'
FL_TEXT   = '#0f172a'
FL_TEXT2  = '#334155'
FL_BORDER = '#e2e8f0'

matplotlib.rcParams.update({
    'figure.facecolor':  FL_BG,
    'axes.facecolor':    FL_BG,
    'axes.edgecolor':    FL_BORDER,
    'axes.labelcolor':   FL_TEXT2,
    'axes.spines.top':   False,
    'axes.spines.right': False,
    'axes.grid':         True,
    'grid.color':        FL_GRID,
    'grid.linewidth':    0.7,
    'xtick.color':       FL_TEXT2,
    'ytick.color':       FL_TEXT2,
    'xtick.labelsize':   10,
    'ytick.labelsize':   10,
    'axes.labelsize':    11,
    'axes.titlesize':    12,
    'axes.titlecolor':   FL_TEXT,
    'axes.titlepad':     12,
    'legend.frameon':    False,
    'legend.fontsize':   10,
    'figure.dpi':        300,
    'savefig.bbox':      'tight',
    'font.family':       'sans-serif',
    'font.sans-serif':   ['Inter', 'Helvetica Neue', 'Arial', 'DejaVu Sans'],
})

ENV_PATH = Path.cwd() / '.env'
load_dotenv(ENV_PATH)
# print(f'Loaded .env : {ENV_PATH}')

TIINGO_API_KEY = os.getenv('TIINGO_API_KEY')
if not TIINGO_API_KEY:
    raise RuntimeError(f'TIINGO_API_KEY not found in {ENV_PATH}')

Helper functions

Tiingo price fetch, SEC EDGAR fetch, XBRL quarterly extraction, formatting, and HTML card/table renderers.

In [3]:
def fetch_tiingo(ticker: str, start_date: str) -> pd.DataFrame:
    resp = requests.get(
        f'https://api.tiingo.com/tiingo/daily/{ticker}/prices',
        headers={'Authorization': f'Token {TIINGO_API_KEY}', 'Content-Type': 'application/json'},
        params={'startDate': start_date, 'resampleFreq': 'daily', 'format': 'json'},
        timeout=60,
    )
    resp.raise_for_status()
    raw = pd.DataFrame(resp.json())
    if raw.empty:
        raise ValueError(f'Tiingo returned no data for {ticker}')
    close_col = next((c for c in ['adjClose', 'close'] if c in raw.columns), None)
    if close_col is None:
        raise ValueError(f'No close column. Got: {list(raw.columns)}')
    df = pd.DataFrame()
    df['date']  = pd.to_datetime(raw['date'].str[:10])
    df['close'] = pd.to_numeric(raw[close_col].values, errors='coerce')
    df = df.set_index('date').sort_index().dropna(subset=['close'])
    df['logret']  = np.log(df['close'] / df['close'].shift(1))
    df['pct_ret'] = df['close'].pct_change()
    for w, label in [(5,'vol_5'),(21,'vol_21'),(63,'vol_63'),(252,'vol_252')]:
        df[label] = df['logret'].rolling(w).std(ddof=1) * np.sqrt(252) * 100
    df['cum_max']  = df['close'].cummax()
    df['drawdown'] = (df['close'] / df['cum_max'] - 1) * 100
    return df


def fetch_edgar_facts(cik: str) -> dict:
    url = f'https://data.sec.gov/api/xbrl/companyfacts/CIK{cik}.json'
    resp = requests.get(url, headers={'User-Agent': SEC_USER_AGENT}, timeout=30)
    resp.raise_for_status()
    return resp.json()


def fetch_filing_dates(cik: str) -> list:
    url = f'https://data.sec.gov/submissions/CIK{cik}.json'
    resp = requests.get(url, headers={'User-Agent': SEC_USER_AGENT}, timeout=30)
    resp.raise_for_status()
    data  = resp.json()
    recent = data.get('filings', {}).get('recent', {})
    forms  = recent.get('form', [])
    filed  = recent.get('filingDate', [])
    return [
        {'form': f, 'filed': filed[i]}
        for i, f in enumerate(forms) if f in ('10-Q', '10-K')
    ]


def extract_quarterly(facts: dict, tag: str, unit: str = 'USD') -> pd.Series:
    """
    Extract quarterly values from SEC EDGAR XBRL data.
    Tries three approaches (frame-based, duration-based, cumulative subtraction)
    and returns the one with best recent coverage.
    """
    entries = (facts.get('facts', {})
                    .get('us-gaap', {})
                    .get(tag, {})
                    .get('units', {})
                    .get(unit, []))
    if not entries:
        return pd.Series(dtype=float)

    candidates = []

    frame_rows = [e for e in entries if e.get('form') in ('10-Q','10-K')
                  and e.get('frame') and 'Q' in str(e.get('frame',''))]
    if frame_rows:
        df = pd.DataFrame(frame_rows)
        df['end'] = pd.to_datetime(df['end'])
        df = df.sort_values('end').drop_duplicates('frame', keep='last').set_index('end')
        if len(df) >= 4:
            candidates.append(df['val'])

    dur_rows = [e for e in entries if e.get('form') in ('10-Q','10-K')
                and e.get('start') and e.get('end')]
    if dur_rows:
        df2 = pd.DataFrame(dur_rows)
        df2['s'] = pd.to_datetime(df2['start'])
        df2['e'] = pd.to_datetime(df2['end'])
        df2['d'] = (df2['e'] - df2['s']).dt.days
        sq = df2[(df2['d'] >= 70) & (df2['d'] <= 110)].copy()
        if len(sq) >= 4:
            sq = sq.sort_values('e').drop_duplicates('e', keep='last').set_index('e')
            candidates.append(sq['val'])

        dc = df2.sort_values('e').drop_duplicates(subset=['e','d'], keep='last')
        recs = []
        for _, grp in dc.groupby('s'):
            grp = grp.sort_values('d')
            prev = 0
            for _, row in grp.iterrows():
                recs.append({'end': row['e'], 'val': row['val'] - prev})
                prev = row['val']
        if recs:
            df3 = pd.DataFrame(recs)
            df3['end'] = pd.to_datetime(df3['end'])
            df3 = df3.sort_values('end').drop_duplicates('end', keep='last').set_index('end')
            if len(df3) >= 4:
                candidates.append(df3['val'])

    if not candidates:
        return pd.Series(dtype=float)

    def score(s):
        recent = s[s.index >= s.index.max() - pd.DateOffset(years=3)]
        if len(recent) < 2:
            return len(recent)
        gap = recent.index.to_series().diff().dt.days.dropna().median()
        penalty = max(0, (gap - 100) / 100) * 5 if gap > 100 else 0
        return len(recent) - penalty

    return max(candidates, key=score)


def extract_best_revenue(facts: dict, tags: list) -> pd.Series:
    best, best_score = pd.Series(dtype=float), -1
    for tag in tags:
        s = extract_quarterly(facts, tag)
        if s.empty:
            continue
        sc = len(s[s.index >= s.index.max() - pd.DateOffset(years=3)])
        if sc > best_score:
            best, best_score = s, sc
    return best


def classify_regime(vol: float) -> tuple:
    for floor, ceil, label, color in REGIME_BANDS:
        if floor <= vol < ceil:
            return label, color
    return REGIME_BANDS[-1][2], REGIME_BANDS[-1][3]


def fmt_pct(v, d=2):
    if v is None or (isinstance(v, float) and not np.isfinite(v)): return '-'
    return f'{v:.{d}f}%'

def fmt_price(v):
    if v is None or (isinstance(v, float) and not np.isfinite(v)): return '-'
    return f'${v:,.2f}'

def safe_float(v):
    try:
        f = float(v)
        return f if np.isfinite(f) else None
    except Exception:
        return None


def render_cards(cards: list) -> str:
    items = ''.join(
        f'<div style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;'
        f'padding:18px 16px;text-align:center;min-width:130px;flex:1 1 130px">'
        f'<div style="font-size:20px;font-weight:600;color:#0f172a;'
        f'letter-spacing:-0.02em;margin-bottom:6px">{c["value"]}</div>'
        f'<div style="font-size:11.5px;color:#64748b;font-weight:500">{c["label"]}</div>'
        f'</div>'
        for c in cards
    )
    return f'<div style="display:flex;flex-wrap:wrap;gap:12px;margin:16px 0">{items}</div>'


print('Helpers loaded.')
Helpers loaded.

Data fetch

Prices for the selected ticker and SPY benchmark are fetched from Tiingo. SEC EDGAR facts and filing dates are fetched for profitability and earnings sections.

In [4]:
print(f'Fetching {TICKER} prices from Tiingo...')
df = fetch_tiingo(TICKER, START_DATE)

print(f'Fetching {BENCHMARK} prices from Tiingo...')
df_spy = fetch_tiingo(BENCHMARK, START_DATE)

print(f'Fetching SEC EDGAR facts for {TICKER} (CIK {CIK})...')
try:
    facts = fetch_edgar_facts(CIK)
    filing_dates = fetch_filing_dates(CIK)
    print(f'  EDGAR: {len(filing_dates)} 10-Q/10-K filings found')
except Exception as e:
    print(f'  EDGAR fetch failed: {e}')
    facts, filing_dates = None, []

start_str = df.index[0].strftime('%Y-%m-%d')
end_str   = df.index[-1].strftime('%Y-%m-%d')
print(f'\n{TICKER}: {len(df):,} daily rows  ({start_str} → {end_str})')
print(f'{BENCHMARK}: {len(df_spy):,} daily rows')
Fetching AAPL prices from Tiingo...
Fetching SPY prices from Tiingo...
Fetching SEC EDGAR facts for AAPL (CIK 0000320193)...
  EDGAR: 44 10-Q/10-K filings found

AAPL: 2,864 daily rows  (2015-01-02 → 2026-05-22)
SPY: 2,864 daily rows

Price & volatility snapshot

Current price, drawdown from ATH, realized volatility at multiple windows, percentile rank in history, regime classification, and ratio vs SPY.

In [5]:
last       = df.iloc[-1]
last_date  = df.index[-1].strftime('%Y-%m-%d')
vol_21     = safe_float(last['vol_21'])
vol_63     = safe_float(last['vol_63'])
vol_252    = safe_float(last['vol_252'])
vol_5      = safe_float(last['vol_5'])
price      = safe_float(last['close'])
dd         = safe_float(last['drawdown'])

hist_vol   = df['vol_21'].dropna()
pct_rank   = float((hist_vol < vol_21).mean() * 100) if vol_21 else None
regime_lbl, regime_color = classify_regime(vol_21) if vol_21 else ('Unknown', '#6b7280')

spy_vol21  = safe_float(df_spy['vol_21'].iloc[-1])
vol_ratio  = round(vol_21 / spy_vol21, 2) if vol_21 and spy_vol21 else None

display(HTML(render_cards([
    {'label': 'Last Close',          'value': fmt_price(price)},
    {'label': 'Drawdown from ATH',   'value': fmt_pct(dd)},
    {'label': '5d Vol (Ann.)',        'value': fmt_pct(vol_5)},
    {'label': '21d Vol (Ann.)',       'value': fmt_pct(vol_21)},
    {'label': '63d Vol (Ann.)',       'value': fmt_pct(vol_63)},
    {'label': '252d Vol (Ann.)',      'value': fmt_pct(vol_252)},
    {'label': 'Percentile Rank',     'value': f'{pct_rank:.0f}th' if pct_rank else '-'},
    {'label': 'Regime',              'value': regime_lbl},
    {'label': f'Vol Ratio vs {BENCHMARK}', 'value': f'{vol_ratio}x' if vol_ratio else '-'},
])))
$308.82
Last Close
0.00%
Drawdown from ATH
13.17%
5d Vol (Ann.)
18.77%
21d Vol (Ann.)
22.12%
63d Vol (Ann.)
22.27%
252d Vol (Ann.)
27th
Percentile Rank
Normal
Regime
1.84x
Vol Ratio vs SPY
In [6]:
plt.figure(figsize=(8, 4.5))
plt.plot(df.index, df['close'], color=FL_BLUE, linewidth=1.6)
plt.ylabel('Price ($)')
plt.title(f'{TICKER} adjusted close')
plt.gca().yaxis.set_major_formatter(
    mticker.FuncFormatter(lambda v, _: f'${v:,.0f}')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(2))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()

plt.figure(figsize=(8, 4.5))
for col, label, color, lw, ls in [
    ('vol_21', '21d', FL_BLUE, 1.8, '-'),
    ('vol_63', '63d', FL_SLATE, 1.4, '--'),
    ('vol_252', '252d', FL_AMBER, 1.2, ':'),
]:
    s = df[col].dropna()
    plt.plot(s.index, s.values, color=color, linewidth=lw, linestyle=ls, label=label)

for floor, ceil, label, color in REGIME_BANDS:
    plt.axhspan(floor, min(ceil, 120), alpha=0.04, color=color, zorder=0)

plt.ylabel('Realized volatility (%)')
plt.title(f'{TICKER} rolling realized volatility')
plt.gca().yaxis.set_major_formatter(
    mticker.FuncFormatter(lambda v, _: f'{v:.0f}%')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(2))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.legend(title='Window')
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()

snapshot_df = pd.DataFrame([
    {
        'Metric': 'Ticker',
        'Value': TICKER
    },
    {
        'Metric': 'Last close',
        'Value': fmt_price(price)
    },
    {
        'Metric': 'Last date',
        'Value': last_date
    },
    {
        'Metric': '21d volatility',
        'Value': fmt_pct(vol_21)
    },
    {
        'Metric': 'Volatility percentile',
        'Value': f'{pct_rank:.0f}th percentile'
    },
    {
        'Metric': 'Volatility regime',
        'Value': regime_lbl
    },
    {
        'Metric': f'Vol ratio vs {BENCHMARK}',
        'Value': f'{vol_ratio}x'
    }
])

display(snapshot_df)
No description has been provided for this image
No description has been provided for this image
Metric Value
0 Ticker AAPL
1 Last close $308.82
2 Last date 2026-05-22
3 21d volatility 18.77%
4 Volatility percentile 27th percentile
5 Volatility regime Normal
6 Vol ratio vs SPY 1.84x

Relative performance vs SPY

Rolling 63-day beta and correlation capture how the stock moves relative to the broad market. Beta > 1 amplifies market moves; correlation measures co-movement direction and strength.

In [7]:
common = df.index.intersection(df_spy.index)
r  = df.loc[common, 'logret'].dropna()
rs = df_spy.loc[common, 'logret'].dropna()
common2 = r.index.intersection(rs.index)
r, rs = r.loc[common2], rs.loc[common2]

roll_beta = (r.rolling(63).cov(rs) / rs.rolling(63).var()).dropna()
roll_corr = r.rolling(63).corr(rs).dropna()

cutoff_3y = roll_beta.index[-1] - pd.DateOffset(years=3)
beta_3y = roll_beta[roll_beta.index >= cutoff_3y]
corr_3y = roll_corr[roll_corr.index >= cutoff_3y]

curr_beta = safe_float(roll_beta.iloc[-1])
curr_corr = safe_float(roll_corr.iloc[-1])
avg_beta  = safe_float(roll_beta.mean())

v_ticker  = df.loc[common2, 'vol_21'].dropna()
v_spy     = df_spy.loc[common2, 'vol_21'].dropna()
v_common  = v_ticker.index.intersection(v_spy.index)
curr_vol_ratio = safe_float((v_ticker.loc[v_common] / v_spy.loc[v_common]).iloc[-1]) if len(v_common) else None

display(HTML(render_cards([
    {'label': '63d Beta',           'value': f'{curr_beta:.2f}' if curr_beta else '-'},
    {'label': 'Avg Beta (full)',    'value': f'{avg_beta:.2f}'  if avg_beta  else '-'},
    {'label': '63d Correlation',    'value': f'{curr_corr:.2f}' if curr_corr else '-'},
    {'label': 'Vol Ratio (21d)',    'value': f'{curr_vol_ratio:.2f}x' if curr_vol_ratio else '-'},
])))
0.93
63d Beta
1.20
Avg Beta (full)
0.61
63d Correlation
1.84x
Vol Ratio (21d)
In [8]:
plt.figure(figsize=(8, 4.5))
plt.plot(
    beta_3y.index,
    beta_3y.values,
    color=FL_BLUE,
    linewidth=1.6
)
plt.axhline(1.0, color=FL_GRID, linewidth=0.9, linestyle='--', label='Beta = 1')
plt.axhline(0.0, color=FL_GRID, linewidth=0.8)
plt.xlabel('Date')
plt.ylabel('Beta')
plt.title(f'{TICKER} 63d rolling beta vs {BENCHMARK} over 3 years')
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(bymonth=[1, 7]))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
plt.legend()
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()

plt.figure(figsize=(8, 4.5))
plt.fill_between(
    corr_3y.index,
    corr_3y.values,
    0,
    where=(corr_3y.values >= 0),
    alpha=0.12,
    color=FL_BLUE,
    interpolate=True
)
plt.fill_between(
    corr_3y.index,
    corr_3y.values,
    0,
    where=(corr_3y.values < 0),
    alpha=0.15,
    color=FL_RED,
    interpolate=True
)
plt.plot(
    corr_3y.index,
    corr_3y.values,
    color=FL_BLUE,
    linewidth=1.6
)
plt.axhline(0, color=FL_GRID, linewidth=0.8)
plt.xlabel('Date')
plt.ylabel('Correlation')
plt.title(f'{TICKER} 63d rolling correlation vs {BENCHMARK} over 3 years')
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(bymonth=[1, 7]))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()

relative_perf_df = pd.DataFrame([
    {
        'Metric': 'Current 63d beta',
        'Value': f'{curr_beta:.2f}'
    },
    {
        'Metric': 'Current 63d correlation',
        'Value': f'{curr_corr:.2f}'
    },
    {
        'Metric': 'Average beta over full history',
        'Value': f'{avg_beta:.2f}'
    }
])

display(relative_perf_df)
No description has been provided for this image
No description has been provided for this image
Metric Value
0 Current 63d beta 0.93
1 Current 63d correlation 0.61
2 Average beta over full history 1.20

Earnings impact

The largest single-day move in a 3-day window around each SEC 10-Q/10-K filing date. Since companies report earnings before or after market close - sometimes a day before the SEC filing - the 3-day window captures the true market reaction reliably.

In [9]:
if not filing_dates:
    print('No filing dates available. Skipping earnings section.')
else:
    ed_dates = sorted(pd.to_datetime(f['filed']) for f in filing_dates)
    moves = []

    for ed in ed_dates:
        locs = df.index.get_indexer([ed], method='nearest')
        if len(locs) == 0:
            continue

        idx = locs[0]
        if idx < 10 or idx >= len(df) - 5:
            continue

        window = df.iloc[max(idx - 1, 0):min(idx + 2, len(df))]
        wr = window['pct_ret'].dropna()
        if wr.empty:
            continue

        best_d = wr.abs().idxmax()
        best_r = safe_float(wr.loc[best_d])
        if best_r is None:
            continue

        moves.append({
            'filing': ed.strftime('%Y-%m-%d'),
            'reaction': best_d.strftime('%Y-%m-%d'),
            'ret': round(best_r * 100, 2),
        })

    if moves:
        avg_abs = np.mean([abs(m['ret']) for m in moves])
        pos_pct = sum(1 for m in moves if m['ret'] > 0) / len(moves) * 100
        max_m = max(moves, key=lambda m: abs(m['ret']))

        display(HTML(render_cards([
            {'label': 'Avg Abs Move', 'value': f'{avg_abs:.1f}%'},
            {'label': 'Positive Rate', 'value': f'{pos_pct:.0f}%'},
            {'label': 'Largest Move', 'value': f"{max_m['ret']:+.1f}%"},
            {'label': 'Earnings Count', 'value': str(len(moves))},
        ])))

        recent = moves[-12:]
        colors = [FL_GREEN if m['ret'] >= 0 else FL_RED for m in recent]

        plt.figure(figsize=(8, 4.5))
        plt.bar(
            range(len(recent)),
            [m['ret'] for m in recent],
            color=colors,
            alpha=0.80,
            width=0.6
        )
        plt.axhline(0, color=FL_GRID, linewidth=0.8)
        plt.xticks(
            range(len(recent)),
            [m['filing'][:7] for m in recent],
            rotation=45,
            ha='right',
            fontsize=9
        )
        plt.gca().yaxis.set_major_formatter(
            mticker.FuncFormatter(lambda v, _: f'{v:.1f}%')
        )
        plt.ylabel('Reaction')
        plt.title(f'{TICKER} earnings reactions for the last {len(recent)} filings')
        plt.tick_params(axis='both', which='both', length=0)
        plt.tight_layout()
        plt.show()

        recent_reactions_df = pd.DataFrame(reversed(recent[-8:]))
        recent_reactions_df = recent_reactions_df.rename(columns={
            'filing': 'Filing Date',
            'reaction': 'Reaction Date',
            'ret': 'Move'
        })
        recent_reactions_df['Move'] = recent_reactions_df['Move'].map(lambda v: f'{v:+.2f}%')

        print('Recent earnings reactions')
        display(recent_reactions_df.reset_index(drop=True))

        earnings_summary_df = pd.DataFrame([
            {
                'Metric': 'Earnings releases',
                'Value': f'{len(moves)}'
            },
            {
                'Metric': 'Average absolute move',
                'Value': f'{avg_abs:.1f}%'
            },
            {
                'Metric': 'Positive reaction rate',
                'Value': f'{pos_pct:.0f}%'
            },
            {
                'Metric': 'Largest move',
                'Value': f'{max_m["ret"]:+.1f}% ({max_m["filing"]})'
            }
        ])

        display(earnings_summary_df)
    else:
        print('Could not compute earnings reactions from filing dates.')
4.1%
Avg Abs Move
61%
Positive Rate
+10.5%
Largest Move
44
Earnings Count
No description has been provided for this image
Recent earnings reactions
Filing Date Reaction Date Move
0 2026-05-01 2026-05-01 +3.24%
1 2026-01-30 2026-02-02 +4.06%
2 2025-10-31 2025-10-30 +0.63%
3 2025-08-01 2025-08-01 -2.50%
4 2025-05-02 2025-05-02 -3.74%
5 2025-01-31 2025-02-03 -3.39%
6 2024-11-01 2024-10-31 -1.82%
7 2024-08-02 2024-08-05 -4.82%
Metric Value
0 Earnings releases 44
1 Average absolute move 4.1%
2 Positive reaction rate 61%
3 Largest move +10.5% (2020-07-31)

Profitability trend

Gross margin, operating margin, net margin, R&D intensity, and revenue growth derived from quarterly SEC EDGAR filings. No raw financial figures - only computed ratios.

In [10]:
if facts is None:
    print('EDGAR data unavailable. Skipping profitability section.')
else:
    rev = extract_best_revenue(facts, REVENUE_TAGS).dropna()
    cost = extract_quarterly(facts, COST_TAG).dropna()
    gp = extract_quarterly(facts, 'GrossProfit').dropna()
    opinc = extract_quarterly(facts, 'OperatingIncomeLoss').dropna()
    ni = extract_quarterly(facts, 'NetIncomeLoss').dropna()
    rnd = extract_quarterly(facts, 'ResearchAndDevelopmentExpense').dropna()

    if rev.empty:
        print('Revenue data not found in EDGAR for this ticker.')
    else:
        if gp.empty and not cost.empty:
            gp = (rev - cost.reindex(rev.index)).dropna()

        def margin(num):
            return (num.reindex(rev.index) / rev * 100).dropna()

        gm = margin(gp) if not gp.empty else pd.Series(dtype=float)
        om = margin(opinc) if not opinc.empty else pd.Series(dtype=float)
        nm = margin(ni) if not ni.empty else pd.Series(dtype=float)
        rndi = margin(rnd) if not rnd.empty else pd.Series(dtype=float)

        rev_yoy = rev.pct_change(4) * 100

        latest_gm = safe_float(gm.iloc[-1]) if not gm.empty else None
        latest_om = safe_float(om.iloc[-1]) if not om.empty else None
        latest_nm = safe_float(nm.iloc[-1]) if not nm.empty else None
        latest_rnd = safe_float(rndi.iloc[-1]) if not rndi.empty else None
        latest_yoy = safe_float(rev_yoy.dropna().iloc[-1]) if not rev_yoy.dropna().empty else None

        display(HTML(render_cards([
            {'label': 'Gross Margin', 'value': fmt_pct(latest_gm, 1)},
            {'label': 'Operating Margin', 'value': fmt_pct(latest_om, 1)},
            {'label': 'Net Margin', 'value': fmt_pct(latest_nm, 1)},
            {'label': 'R&D / Revenue', 'value': fmt_pct(latest_rnd, 1)},
            {'label': 'Revenue YoY', 'value': fmt_pct(latest_yoy, 1)},
        ])))

        plt.figure(figsize=(8, 4.5))
        for s, lbl, col, ls in [
            (gm, 'Gross', FL_BLUE, '-'),
            (om, 'Operating', FL_SLATE, '--'),
            (nm, 'Net', FL_AMBER, ':')
        ]:
            t = s.tail(12)
            if not t.empty:
                plt.plot(t.index, t.values, color=col, linewidth=1.6, linestyle=ls, label=lbl)

        plt.ylabel('Margin (%)')
        plt.title(f'{TICKER} margin trend over the last 12 quarters')
        plt.gca().yaxis.set_major_formatter(
            mticker.FuncFormatter(lambda v, _: f'{v:.0f}%')
        )
        plt.gca().xaxis.set_major_locator(mdates.MonthLocator(bymonth=[1, 7]))
        plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
        plt.legend()
        plt.tick_params(axis='both', which='both', length=0)
        plt.tight_layout()
        plt.show()

        ry12 = rev_yoy.dropna().tail(12)

        if not ry12.empty:
            plt.figure(figsize=(8, 4.5))
            colors = [FL_GREEN if v >= 0 else FL_RED for v in ry12.values]
            plt.bar(
                ry12.index,
                ry12.values,
                color=colors,
                alpha=0.80,
                width=40
            )
            plt.axhline(0, color=FL_GRID, linewidth=0.8)
            plt.ylabel('Revenue YoY (%)')
            plt.title(f'{TICKER} revenue growth over the last 12 quarters')
            plt.gca().yaxis.set_major_formatter(
                mticker.FuncFormatter(lambda v, _: f'{v:.0f}%')
            )
            plt.gca().xaxis.set_major_locator(mdates.MonthLocator(bymonth=[1, 7]))
            plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
            plt.tick_params(axis='both', which='both', length=0)
            plt.tight_layout()
            plt.show()

        profitability_summary_df = pd.DataFrame([
            {
                'Metric': 'Gross margin',
                'Value': fmt_pct(latest_gm, 1)
            },
            {
                'Metric': 'Operating margin',
                'Value': fmt_pct(latest_om, 1)
            },
            {
                'Metric': 'Net margin',
                'Value': fmt_pct(latest_nm, 1)
            },
            {
                'Metric': 'R&D / Revenue',
                'Value': fmt_pct(latest_rnd, 1)
            },
            {
                'Metric': 'Revenue YoY',
                'Value': fmt_pct(latest_yoy, 1)
            }
        ])

        display(profitability_summary_df)
49.3%
Gross Margin
32.3%
Operating Margin
26.6%
Net Margin
10.3%
R&D / Revenue
16.6%
Revenue YoY
No description has been provided for this image
No description has been provided for this image
Metric Value
0 Gross margin 49.3%
1 Operating margin 32.3%
2 Net margin 26.6%
3 R&D / Revenue 10.3%
4 Revenue YoY 16.6%

Tail risk & drawdowns

Worst single-day returns, Value at Risk at 95% and 99% confidence, Conditional VaR (expected shortfall), and drawdown from all-time high.

In [11]:
dd_s      = df['drawdown'].dropna()
curr_dd   = safe_float(dd_s.iloc[-1])
max_dd_d  = dd_s.idxmin()
max_dd_v  = safe_float(dd_s.loc[max_dd_d])

logret    = df['logret'].dropna().values
logret    = logret[np.isfinite(logret)]
var95     = float(np.percentile(logret, 5))  * 100
var99     = float(np.percentile(logret, 1))  * 100
cvar95    = float(logret[logret <= np.percentile(logret, 5)].mean()) * 100

worst_5   = df['logret'].dropna().nsmallest(5)
best_5    = df['logret'].dropna().nlargest(5)

display(HTML(render_cards([
    {'label': 'Current Drawdown', 'value': fmt_pct(curr_dd)},
    {'label': 'Max Drawdown',     'value': fmt_pct(max_dd_v)},
    {'label': 'Max DD Date',      'value': max_dd_d.strftime('%Y-%m-%d')},
    {'label': 'Daily VaR (95%)',  'value': fmt_pct(var95)},
    {'label': 'Daily VaR (99%)',  'value': fmt_pct(var99)},
    {'label': 'Daily CVaR (95%)', 'value': fmt_pct(cvar95)},
])))
0.00%
Current Drawdown
-38.52%
Max Drawdown
2019-01-03
Max DD Date
-2.75%
Daily VaR (95%)
-4.93%
Daily VaR (99%)
-4.20%
Daily CVaR (95%)
In [12]:
cutoff_10y = dd_s.index[-1] - pd.DateOffset(years=10)
dd_10y = dd_s[dd_s.index >= cutoff_10y]

plt.figure(figsize=(8, 4.5))
plt.fill_between(dd_10y.index, dd_10y.values, 0, alpha=0.20, color=FL_RED)
plt.plot(dd_10y.index, dd_10y.values, color=FL_RED, linewidth=1.4)
plt.axhline(0, color=FL_GRID, linewidth=0.8)
plt.ylabel('Drawdown (%)')
plt.title(f'{TICKER} drawdown from all-time high over 10 years')
plt.gca().yaxis.set_major_formatter(
    mticker.FuncFormatter(lambda v, _: f'{v:.0f}%')
)
plt.gca().xaxis.set_major_locator(mdates.YearLocator(2))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()

plt.figure(figsize=(8, 4.5))
plt.hist(logret * 100, bins=80, color=FL_BLUE, alpha=0.70, edgecolor='none')
plt.axvline(var95, color=FL_RED, linewidth=1.2, linestyle='--', label=f'VaR 95% ({fmt_pct(var95)})')
plt.axvline(var99, color=FL_AMBER, linewidth=1.0, linestyle=':', label=f'VaR 99% ({fmt_pct(var99)})')
plt.axvline(0, color=FL_GRID, linewidth=0.8)
plt.xlabel('Daily log return')
plt.ylabel('Frequency')
plt.title(f'{TICKER} daily return distribution')
plt.gca().xaxis.set_major_formatter(
    mticker.FuncFormatter(lambda v, _: f'{v:.0f}%')
)
plt.legend(fontsize=9)
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()

worst_5_df = pd.DataFrame({
    'Date': [d.strftime('%Y-%m-%d') for d in worst_5.index],
    'Daily Return': [f'{v * 100:.2f}%' for v in worst_5.values]
})

best_5_df = pd.DataFrame({
    'Date': [d.strftime('%Y-%m-%d') for d in best_5.index],
    'Daily Return': [f'{v * 100:.2f}%' for v in best_5.values]
})

print('5 worst single-day returns')
display(worst_5_df.reset_index(drop=True))

print('5 best single-day returns')
display(best_5_df.reset_index(drop=True))
No description has been provided for this image
No description has been provided for this image
5 worst single-day returns
Date Daily Return
0 2020-03-16 -13.77%
1 2019-01-03 -10.49%
2 2020-03-12 -10.40%
3 2025-04-03 -9.70%
4 2020-09-03 -8.34%
5 best single-day returns
Date Daily Return
0 2025-04-09 14.26%
1 2020-03-13 11.32%
2 2020-07-31 9.96%
3 2020-03-24 9.56%
4 2020-03-02 8.90%

Volatility regime

Regime classification uses ticker-calibrated vol thresholds defined in the config cell. The table shows how much of the historical record falls in each regime. Current regime streak counts consecutive days in the active regime.

In [13]:
vol_21s = df['vol_21'].dropna()
regime_series = vol_21s.apply(lambda v: classify_regime(v)[0])
total_days = len(regime_series)

regime_rows = []
for floor, ceil, label, color in REGIME_BANDS:
    count = int((regime_series == label).sum())
    pct = round(count / total_days * 100, 1)
    vrange = f'{floor} to {ceil}%' if ceil < 999 else f'{floor}%+'
    regime_rows.append({
        'Regime': label,
        'Vol Range': vrange,
        'Trading Days': count,
        '% of History': f'{pct}%'
    })

curr_regime = regime_series.iloc[-1]
streak = sum(1 for lbl in reversed(regime_series.values) if lbl == curr_regime)
mean_vol_all = float(vol_21s.mean())

display(HTML(render_cards([
    {'label': 'Current Regime', 'value': curr_regime},
    {'label': 'Streak', 'value': f'{streak} days'},
    {'label': 'Long-Run Mean Vol', 'value': fmt_pct(mean_vol_all)},
])))

regime_df = pd.DataFrame(regime_rows)

print('Historical time in each regime')
display(regime_df)
Normal
Current Regime
1294 days
Streak
25.97%
Long-Run Mean Vol
Historical time in each regime
Regime Vol Range Trading Days % of History
0 Low 0 to 15% 328 11.5%
1 Normal 15 to 25% 1294 45.5%
2 Elevated 25 to 35% 819 28.8%
3 High 35 to 50% 283 10.0%
4 Crisis 50%+ 119 4.2%
In [14]:
cutoff_2y = vol_21s.index[-1] - pd.DateOffset(years=2)
vol_2y = vol_21s[vol_21s.index >= cutoff_2y]

plt.figure(figsize=(8, 4.5))
for floor, ceil, label, color in REGIME_BANDS:
    plt.axhspan(
        floor,
        min(ceil, vol_2y.max() * 1.2),
        alpha=0.06,
        color=color,
        zorder=0
    )

plt.plot(vol_2y.index, vol_2y.values, color=FL_BLUE, linewidth=1.8)
plt.ylabel('21d volatility (%)')
plt.title(f'{TICKER} 21d volatility with regime bands over 2 years')
plt.gca().yaxis.set_major_formatter(
    mticker.FuncFormatter(lambda v, _: f'{v:.0f}%')
)
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(bymonth=[1, 4, 7, 10]))
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))

legend_patches = [Patch(color=b[3], alpha=0.4, label=b[2]) for b in REGIME_BANDS]
plt.legend(handles=legend_patches, fontsize=9, ncol=2)
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()

labels = [r['Regime'] for r in regime_rows]
pcts = [float(str(r['% of History']).replace('%', '')) for r in regime_rows]
colors = [b[3] for b in REGIME_BANDS]

plt.figure(figsize=(8, 4.5))
bars = plt.bar(labels, pcts, color=colors, alpha=0.82, width=0.5)

for bar, pct in zip(bars, pcts):
    plt.text(
        bar.get_x() + bar.get_width() / 2,
        bar.get_height() + 0.3,
        f'{pct:.1f}%',
        ha='center',
        va='bottom',
        fontsize=10,
        color=FL_TEXT2
    )

plt.ylabel('% of trading days')
plt.title(f'{TICKER} time in each volatility regime')
plt.gca().yaxis.set_major_formatter(
    mticker.FuncFormatter(lambda v, _: f'{v:.0f}%')
)
plt.tick_params(axis='both', which='both', length=0)
plt.tight_layout()
plt.show()

top = max(
    [
        {
            'Regime': r['Regime'],
            'Vol Range': r['Vol Range'],
            'Pct': float(str(r['% of History']).replace('%', ''))
        }
        for r in regime_rows
    ],
    key=lambda r: r['Pct']
)

regime_summary_df = pd.DataFrame([
    {
        'Metric': 'Current regime',
        'Value': curr_regime
    },
    {
        'Metric': 'Current streak',
        'Value': f'{streak} days'
    },
    {
        'Metric': 'Long-run mean volatility',
        'Value': fmt_pct(mean_vol_all)
    },
    {
        'Metric': 'Most common regime',
        'Value': f'{top["Regime"]} ({top["Vol Range"]})'
    },
    {
        'Metric': 'Most common regime share',
        'Value': f'{top["Pct"]:.1f}%'
    }
])

display(regime_summary_df)
No description has been provided for this image
No description has been provided for this image
Metric Value
0 Current regime Normal
1 Current streak 1294 days
2 Long-run mean volatility 25.97%
3 Most common regime Normal (15 to 25%)
4 Most common regime share 45.5%