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:
# -------------------------------------------
# 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)]
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.
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.
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.
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 '-'},
])))
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)
| 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.
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 '-'},
])))
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)
| 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.
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.')
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.
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)
| 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.
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)},
])))
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))
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.
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)
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% |
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)
| 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% |