Qtick
Sign in

Risk & tails

How QTick calculates the 8 metrics in this group — the real method named, then translated: what it tells you, how it is computed, and where it breaks down. None of these is a trigger to act.

Beta

How it's calculated

Cov(stock daily return, SPY daily return) ÷ Var(SPY daily return) over the trailing 252 trading days. Stock closes come from a direct yfinance bulk download (yf.download with auto_adjust=True over a 3y window) — this path bypasses the project's adjusted_returns split-factor seam; the SPY benchmark closes come from stored intel_daily_bars (no live fetch at compute time) and also bypass that seam, read as a raw pct_change. The two return series are date-aligned and NaN-dropped (pd.concat axis=1, sort=True, dropna) before the regression. The denominator uses population variance (ddof=0) while the covariance uses the sample default, but the identical N cancels in the ratio so beta is unbiased.

How it affects judgement

Beta tells you how much of a stock's price noise comes from broad market swings versus company-specific events. A high beta amplifies whatever macro thesis you hold — if you expect the market to fall, high beta deepens that risk; if you expect a rally, it magnifies it. Low beta stocks add ballast when you're uncertain on the macro direction. It does not say anything about whether the company itself is cheap, overvalued, or fundamentally sound — pair it with alpha and R² to see whether the market-linked moves even dominate the story.

What the current value signifies

1.0 means the stock has historically moved in lockstep with the S&P 500. Above 1.0 it swings more than the market on most days; below 1.0 it swings less. A reading of 1.5 means the stock has historically moved ~50% more than SPY on a typical day in either direction. Near 0 means market-wide moves have almost no systematic pull on this name.

Range

Negative (inverse movers, rare), 0 (no market correlation), 0.3–0.7 (typical defensives/staples/utilities), 0.8–1.2 (broad-market-tracking large caps), 1.2–2.0 (growth/tech/small-caps), above 2.0 (leveraged exposures, meme stocks, early-stage names). No hard mathematical ceiling.

The catch

Backward-looking over one year of returns — a stock that was low-beta during a calm period can spike dramatically once a company-specific shock arrives, and the beta won't update until the next nightly run.

Realized volatility

How it's calculated

Population std-dev (ddof=0) of the last 21 daily adjusted returns, scaled to annual: × √252 × 100. Output is an annualized percentage. Requires ≥21 daily returns (≥22 price bars); NULL otherwise.

How it affects judgement

A higher number widens every risk estimate you form — it raises the bar for what counts as a meaningful move and compresses the signal-to-noise on any short-term thesis. Pair it with beta: a high-vol / low-beta name carries idiosyncratic risk that hedging the market won't cancel.

What the current value signifies

For large-caps, ~15–25% is ordinary; 30–50% is elevated (earnings chop, sector stress); above 50% is crisis- or meme-level turbulence. Below 15% often means the tape is unusually compressed — which can precede a breakout in either direction.

Range

Practically 0% (suspended / halted stock) to 200%+ (penny / meme / post-halt). Typical large-cap 15–30%; mid/small 25–50%; distressed or speculative names 60–150%+. SPY itself runs ~12–18% in calm markets, 30–50% in crises.

The catch

It uses only the last 21 sessions (about one calendar month), so a single shock week — earnings miss, macro event — can double the reading overnight, overstating the ongoing regime. It is backward-looking; it describes what just happened, not what is coming.

GARCH volatility

How it's calculated

Fits a GARCH(1,1) model — h_t = ω + α·r²_{t-1} + β·h_{t-1} — to ~2 years of daily log-returns (the returns are demeaned first) from free macro proxies: VIX (equity vol), crude/USO, natural gas/UNG, broad commodities/DBC, and freight/shipping (^BDI → BOAT → SEA). Parameters come from a deterministic 12×12 grid scan over α∈[0.01,0.30] and β∈[0.60,0.98] plus a local refinement of ω, picking the (α,β,ω) that maximises the Gaussian log-likelihood (pure numpy — no `arch` dependency). Today's conditional variance h_T is then annualised: √(h_T × 252) × 100 → a percentage. If the fit is non-stationary (α+β ≥ 0.999) or degenerate, EWMA (λ=0.94) substitutes and a `method` field records which path ran. This is market-wide, not per-ticker.

How it affects judgement

Read it as a macro-regime gauge, not a stock-specific input. When GARCH vol on crude or commodities sits in the 70th percentile or higher of its own trailing year, the macro backdrop is turbulent — position sizing and thesis confidence that assume normal conditions deserve a harder look. Low percentile ranks (under the 30th) point to a calmer macro backdrop where idiosyncratic factors dominate. Compare across channels: freight vol spiking while equity vol stays low flags a supply-chain narrative that may not yet be reflected in broad indices.

Current value

This metric’s live data pipeline is under review — the method above describes how it is intended to be calculated, but the current displayed value may not yet reflect that formula. Treat it as in progress, not a finished number.

Range

Annualised vol %: roughly 8–20% in calm regimes, 25–50% in stressed periods, 60–120%+ in crises (energy in 2022, freight in 2021). Percentile rank: 0–100 vs the proxy's own trailing year. Method field: "garch" (normal) or "ewma" (fallback when the series is too short or the fit degenerates).

The catch

This metric is market-wide (macro proxies), not per-ticker — it describes the ambient macro environment, not this stock's own price behaviour. Bigger caveat on the stock page: the macro_vol_service engine output has NO frontend consumer yet. The stock-page mapper (macro.ts) maps a separate /macro-sensitivity endpoint, and SecMacro.tsx renders the GARCH figure from MOCK data while carrying a "live" provenance tag — and the UI provenance label reads "GARCH 30d annualized," which is wrong: the engine annualises today's conditional variance, not a 30-day window. So a live user may see a placeholder number tagged "live" with a misleading label. The computed engine values described above are real; the path that surfaces them to the stock page is not wired.

Price stretch

How it's calculated

We take the last 20 daily closes, compute their mean (SMA-20) and standard deviation, then: z = (today's close − SMA-20) / stdev_20d, rounded to 2 decimal places. The standard deviation is pandas' sample std (ddof=1). Source: `backend/app/services/backtest/point_in_time.py:121-126`, mirrored in `data_provider.py:484-490` (the live path uses the last traded price, `snap.last_price`, in place of the settled close); stored as `IntelTechnicalFactors.z_score_20d` (`backend/app/models/intel.py:1081`); mapped to the frontend at `ticker_service.py:2238` → `backend/app/qtick/schemas/stocks.py:423`.

How it affects judgement

A stretched reading (roughly +2 or more, −2 or less) tells you the price has moved unusually far from its 20-day anchor — historically, mean-reversion pressure is higher there. A reading near zero says price sits mid-range, so momentum or fundamental catalysts carry more weight than stretch alone. Read it alongside RSI and Bollinger to see whether the stretch is fading or accelerating. Stretch on its own does not time reversals.

What the current value signifies

0 means today's price sits exactly at the 20-day average. The UI flags a reading as "Unusually stretched" only when the absolute z-score is strictly greater than 2 (`Math.abs(z) > 2`, `SecTech.tsx:173`); exactly ±2 is not flagged. Anything inside that gate renders as "Within normal range." (The idea of a "±1 to ±2 elevated but not extreme" band is editorial guidance, not a code threshold — the only gate in code is the single |z| > 2 check.)

Range

Theoretically unbounded, but in practice roughly −3 to +3 for liquid stocks. Most daily readings land between −1 and +1. Beyond ±2 is unusual (~5% of sessions on a normal distribution); beyond ±3 is rare and typically marks a news-driven gap or earnings spike.

The catch

The window is only 20 trading days (~one month). A trending stock can sit at +1.5 for weeks without snapping back — stretch alone does not time reversals. Also note the numerator differs by path: the backtest path uses the settled daily close, while the live `data_provider` path uses the last traded price, which may be intraday.

Max drawdown

How it's calculated

Each day, we divide that day's adjusted close by the running peak seen so far inside the trailing window, subtract 1, then take the worst (minimum) value across the trailing window (up to 252 bars; fewer if the listing is younger) and express it as a percentage. The peak resets at the window's left edge, so it doesn't inherit any high from before the window. Code: `recompute_technical_factors.py:608-613` — guarded by `if len(c) >= 2`, then `wdd = c[-252:]; peak = np.maximum.accumulate(wdd); (wdd / peak - 1).min() * 100`; if fewer than 2 bars exist the field is NULL.

How it affects judgement

A shallow drawdown (say -10%) on a stock with strong momentum tightens your confidence in an orderly-path read — the price didn't crater along the way. A deep drawdown (-40%+) widens the uncertainty around any thesis: recoveries from that depth tend to take longer and lean on catalysts, not just price drifting back up. Read it as context for how rough the road was, not as a call on where price goes next.

What the current value signifies

Values in the -5% to -20% band are typical for mid-cap equities in a calm year. Anything beyond -30% in the past year means this stock spent time in genuine distress territory — the price had a long road back to breakeven, and may not have completed it yet.

Range

Always <= 0%. Typical large-cap in a flat year: -5% to -20%. Volatile small-cap or stressed name: -30% to -60%. Catastrophic loss or near-delisting: below -80%. A value of 0% is only possible if the price never dipped below its starting peak within the window.

The catch

One single worst-peak-to-trough figure hides how long the stock stayed underwater. A -25% drawdown that lasted two weeks reads identically to one that dragged on eight months — pair it with CDaR (`cdar_05_1y`) or the underwater-streak/recovery-days metrics for duration context. Also note the peak only looks back within the trailing window, so a decline that began before the window starts can read shallower than the full drop felt.

Worst-stretch loss

How it's calculated

Each day over the past year, we compute how far the price sits below its running peak — the drawdown path, which is always 0% or negative. We then take the worst 5% of those depths — k = ceil(0.05 x 252) = 13 of the 252 trading days — and average them. That average is CDaR. Formally: sort all daily drawdown values most-negative first, take the top k = max(1, ceil(0.05 x N)) entries, return their mean. (The max(1, ...) floor is what makes CDaR collapse exactly to the max drawdown as the fraction goes to 0.) Code: `_cdar(dd, 0.05)` at `backend/scripts/recompute_technical_factors.py:151-167` — it returns a (CDaR, DaR) pair and only the first element (the mean of the worst stretch) is surfaced; the second is the single quantile depth. Drawdown path built at :800/:819; 1y and 3y values computed at :816-821 (`cdar_05_1y`, `cdar_05_3y`); fed into `RiskFactors.cdar_05_1y` via `ticker_service.py:2265`.

How it affects judgement

CDaR tells you how punishing the bad stretches are — not just the single worst day. Compare it to max drawdown: when CDaR is a large fraction of max drawdown, deep underwater stretches recur — the stock spends real time far below its peak. When CDaR is close to max drawdown only because the worst was one isolated spike, the rest of the history was shallower. Read it alongside `time_under_water` and `median_recovery_days`: deep average drawdowns plus slow recovery describe a name that stays underwater. Because CDaR is gated as a non-predictive descriptor — it is residualized against its own ingredients (max_dd, realized vol, ulcer) in the dedup test and carried as risk-character rather than a ranking factor — it describes drawdown character, not where the next trough lands. It informs how you size and how long you expect to hold, not the entry itself.

What the current value signifies

Values near 0% mean the stock almost never trades meaningfully below a peak — a shallow drawdown history. Values around -10% to -20% are common for volatile mid-caps; -30% or deeper is elevated and means the stock routinely spends time far underwater. Note the two horizons: the headline number is the 1-year CDaR (`cdar_05_1y`), while `cdarPctile` ranks the 3-year CDaR (`cdar_05_3y`) cross-sectionally — so the percentile and the headline figure describe different windows. The percentile runs 0-100 (higher = worse vs the full universe that day); 80th percentile means deeper average drawdown stretches over 3 years than 80% of stocks.

Range

Always 0% or negative. The deepest it can reach is the max drawdown itself (CDaR equals max drawdown as the fraction goes to 0, guaranteed by the max(1, ...) floor on k). Typical range: 0% (no drawdown ever) to roughly -50% for severely distressed names. Large-cap indices run around -5% to -15%; high-volatility single stocks -20% to -40%. The cross-sectional percentile (`cdarPctile`) runs 0-100.

The catch

It describes historical drawdown character, not future depth. It is built and carried as a non-signal descriptor — residualized against max drawdown, realized vol, and ulcer in the dedup test — so it does not tell you when or how deep the next trough will be, only how bad past troughs typically ran.

1-in-200 day

How it's calculated

Daily log-returns are standardized by a RiskMetrics EWMA volatility path (λ=0.94). The worst ~10% of those standardized losses become exceedances over the 90th-percentile threshold. A Generalized Pareto Distribution is fit to those exceedances via closed-form Probability-Weighted Moments (Hosking-Wallis). The fit is checked with an Anderson-Darling test against a parametric-bootstrap 5% critical value; the result is nulled if it fails. The 99.5th-percentile VaR of the full loss distribution, computed from the GPD tail fit (the McNeil-Frey POT estimator u + (β/ξ)·[((n/m)(1-q))^(-ξ) − 1]), is then rescaled by today's EWMA σ forecast and multiplied by 100 — producing a positive loss percentage. Code: `backend/scripts/recompute_technical_factors.py` lines 316–416 (`_evt_pot`), specifically line 402: `out["evt_var_995_1d"] = float(_var(0.995) * scale * 100.0)`. Frontend gates display on `tailMethod === "gpd_pwm_ewma" | "gpd_pwm"`: `qtick/frontend/src/lib/data/map/risk.ts` lines 19–21.

How it affects judgement

A high EVT tail relative to realized vol or beta tells you the distribution has a fat left tail that Gaussian VaR misses — the stock carries crash risk beyond what vol alone suggests. Pair it with `tailUnderstatement` (EVT ES₉₉ / Gaussian ES₉₉): a ratio well above 1 strengthens a bearish tail-risk read; a ratio near 1 is evidence the tail is benign and weakens it. When the metric is absent (GPD fit failed), treat tail risk as unquantifiable — not zero.

What the current value signifies

The number is the percentage loss you'd expect to be exceeded only about once every 200 trading days under current volatility conditions. A value of 4–7% is typical for large-cap equities. Above ~10% reflects a genuinely fat, crash-prone tail — often seen in small-caps, post-earnings names, or stressed stocks. Below 3% is unusually calm and may reflect a low-vol regime rather than structural safety.

Range

Positive loss percentage, no theoretical minimum above zero. Practical range: ~2% (very low volatility, thin tails) to ~20%+ (small-caps, meme stocks, stressed names in turbulent regimes). Large-cap S&P 500 constituents typically land 4–8%. The metric is null when the GPD fit fails (fewer than 250 days of history, fewer than 25 exceedances, Anderson-Darling rejection, or ξ ≥ 1 infinite-mean tail).

The catch

Because the threshold is fixed at the 90th-percentile loss and the GPD is fit on historical data, the estimate captures the tail shape of the past — a regime shift (e.g., a company entering financial distress mid-sample) can blend two different distributions and produce a misleading ξ. The EWMA conditioning helps with volatility clustering but does not fix structural breaks in the return-generating process.

Falling-knife score

How it's calculated

Three structural legs — trending-down slope (price below its 50-day MA, 1-month drop normalized by the stock's own volatility, plus 3-month acceleration), fresh 20-bar lows with an expanding range, and down-volume dominance over the last 20 days — combined 38/32/30, then taken as the max against a separate crash leg. The crash leg fires when price is behind its 20-day MA at a fresh low AND the move is fast: a drop of at least 8% in 2 days or 12% in 5 days. Those thresholds are cohort-scaled by a multiplier: they apply as-is for normal stocks (×1.0), banks need a bigger move (×0.8), and penny/crypto trip the gate on a smaller one (×1.4). Five recovery signals (volume drying up, a higher low printed, range tightening, the biggest recent bar closing green, and a reclaim of the 20-day MA) jointly dampen the raw score by up to 55%. Result is ×100 and clipped to 0–100. The phase-label threshold is cohort-banded: penny/crypto/nano use wider volatility bands and trip "falling_knife" at 60, normal stocks at 70, banks at 75.

How it affects judgement

A high score says the tape structure is still in active distribution — that weakens any "oversold bounce" read and shifts the burden of proof to confirming stabilization signals first. A low score, given prior damage, pairs with find_bottom to support a stabilization read. It is one leg of the avoid-spine; cross it with the EVT tail, short interest, and the forensic score before forming a view.

What the current value signifies

Under about 30: no active knife pattern — price may be down, but the structure isn't in freefall. 30–60 (medium): falling, distribution present, but recovery signals may be forming. 60–70+ (the exact line varies by cohort): an active knife — slope, volume, and momentum all confirm the decline isn't done. At 80–100: a crash-speed or strongly structural drop with no recovery-signal dampening at all.

Range

0–100. Settled names typically land 0–25. Moderate decline: 30–55. Active knife: 60–80. Full crash-leg with no recovery signals: 80–100. Cohort thresholds for the "falling_knife" phase label: 60 (penny/crypto/nano), 70 (normal), 75 (banks).

The catch

The structural legs need roughly 20 bars of confirmed slope, so a single-day vertical crash off a rally top scores low on fk_trend and fk_distribution until the crash leg fires (the ≥8%/2d or ≥12%/5d gate, cohort-scaled). That gap was the motivating miss (SMCI −28% on 2026-06-10) that prompted adding the crash pathway. Also note the crash leg combines via max(), not additively, so a fast drop can lift an otherwise-weak structural score on its own.

See these metrics live on a stock dossier

QTick shows cited data and code-computed models for self-directed research. Nothing here is a recommendation, solicitation, or investment advice. Where a metric’s live data pipeline is under review, its method is described but its current value is withheld rather than shown as reliable.