Qtick
Sign in

Odds & the avoid spine

How QTick calculates the 4 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.

Setup odds

How it's calculated

Two separate pipelines feed this card. The upside/downside split bar: the stock's red-flag count (0-3+) selects a pre-built cohort row from `risk_odds_reference`; `p_up = (fwd_12m > 0).mean()` and `p_down = (fwd_12m < 0).mean()` over a fixed 2011-2024 semi-annual panel for that bucket (bucket = min(risk_score, 3), with worst-populated-bucket inheritance; ABSTAIN path when a bucket is too thin). These rates are computed at build time and stored in the DB, not held as in-code constants. The pullback block samples each name's own point-in-time daily-bar history (non-overlapping windows strided by the horizon, described as ~25 years in the UI) into the `directional_odds_base_rates` grid; it selects the tightest cell of segment x tactical phase x vol tercile x 3-month momentum sign with n >= 200 (a hierarchical fallback widens the cell if the tight one is thin, else uses the deepest cell with a thinness flag). `p_pullback = n_pullback / n` where a pullback = max drawdown within 63 (or 21) trading days >= the segment threshold (10% default; 20% for penny/crypto). Sources: `backend/app/qtick/services/ticker_service.py:3639-3834`, `backend/app/qtick/services/directional_odds.py:206-236`, `backend/llm_lab/downside_odds.py:218-219`.

How it affects judgement

The split bar tells you whether similar names — same red-flag count — historically closed higher or lower 12 months out. A wide upside skew with a low pullback probability tightens the risk case; a wide downside skew with an elevated pullback multiple (>= 1.25x segment base rate) adds weight to caution. Neither dimension says when anything moves — treat them as a historical base rate, not a directional signal for this name specifically.

What the current value signifies

The bar shows, say, "65% upside / 35% downside": names with this stock's red-flag count finished higher in 65% of 12-month windows historically. The pullback block shows, say, "High - 38% - 1.31x base": in this tape state and segment, 38% of similar setups saw a drawdown at or past the segment threshold (10% default; 20% for penny/crypto) within 63 trading days — 1.31x the segment's unconditional rate. High/Elevated pullback = meaningfully above the segment norm; Low = below it; In line = within +/-5% of it.

Range

Upside / downside: each 0-100%, always summing to &lt;100% (ties excluded). Real cohort range roughly 35-70% for p_up across risk buckets 0-3. Pullback probability: roughly 5-50% depending on segment and tape state; a multiple of 1.0x = unconditional segment rate, typical range ~0.5x-2.0x. The bar carries a 50/50 midline as the disclosed uncalibrated prior. The `n` behind the cell is shown; thin cells (&lt;200) are flagged.

The catch

Both pipelines are survivorship-biased (only names that kept trading are in the history), so downside frequencies are a conservative floor — true realized risk is likely worse for the highest-risk cohorts. The two pipelines also use different time bases: the split bar is the fixed 2011-2024 panel, while the pullback block runs over each name's own point-in-time daily-bar history, so the two figures are not drawn from the same window.

Post-earnings drift

How it's calculated

Takes the most recent EPS surprise (actual minus estimate) and divides it by that stock's own trailing 8-quarter surprise standard deviation, computed strictly on prior quarters (shift(1).rolling(8, min_periods=5) — no lookahead), then clips the result to plus/minus 8 and tiers it (STRONG+ at >=1.5, + at >=0.5, NEUTRAL, - at <=-0.5, STRONG- at <=-1.5). A second "demeaned" variant first subtracts the trailing 8-quarter mean before dividing, and floors the denominator at 0.05% of the pre-announcement price (SUE_FLOOR_PCT=0.0005) so a tiny absolute beat off a near-zero base can't inflate the score (the Foster-1977 demeaning correction). Drift base rates are then measured separately as market-excess buy-and-hold return versus SPY, entered at T+2 (the Bernard-Thomas capturable-only lag), across 20/40/60 trading-day horizons over the full historical event set.

How it affects judgement

A positive SUE tier adds mild corroborating weight to a bullish read — not enough to stand on its own, but it stacks when momentum or analyst-revision signals point the same way. A negative tier weakens a near-term read slightly. An "already priced" flag (reaction z > 4 versus the stock's 21-day daily sigma) cancels the drift edge entirely: the announcement-day jump already absorbed what history says the drift would deliver, so at that point the tier label carries no base-rate information. One framing distinction: this admin-only per-ticker SUE drift tier is a standalone surface and does not itself feed the consumer-facing Net Signal bull/bear tally. The Net Signal carries its own separate, simpler earnings-surprise factor (latest-beat magnitude plus a beat/miss-streak count, net_signal.py:504-512 _f_surprises), so earnings surprise is present there — but the calibrated SUE drift tier described here is a distinct thing that Net Signal does not count.

What the current value signifies

STRONG+ or + means the most recent beat was large relative to this stock's own historical beat/miss volatility, and the drift window (roughly T+2 to T+63 trading days post-announcement) may not have fully run yet. NEUTRAL means the surprise was unremarkable by this stock's own standards. STRONG- or - flags a miss of meaningful size by the same yardstick. If the "already priced" flag is set, the reaction-day move was large enough (above 4 daily sigma) that the measured drift spread for that bucket sits at essentially zero, so the tier label becomes irrelevant for what happens next.

Range

SUE z-score: clipped to plus/minus 8, with most events landing roughly between -3 and +3. Tiers: STRONG- / - / NEUTRAL / + / STRONG+, thresholds at plus/minus 0.5 and plus/minus 1.5. Drift up-rate, P(BHAR > 0): 0 to 1, generally 0.45-0.60 for active tiers across the measured history. Reaction z-bucket for the already-priced check: z<1 / 1-2 / 2-4 / z>4.

The catch

Two different significance tests sit behind this signal and they answer different questions. The directional-factor validity test — quarterly rank-IC of SUE versus forward return, Newey-West corrected over 44 quarters (~11 years), survivorship-free — gives a t of about 2.23 across all names (2.00 on the SP500 subset). That clears the conventional |t|>2 bar but falls short of the strict Harvey-Liu-Zhu multiple-testing bar of |t|>3, which is why the tier is treated as observational base-rate color rather than a hardened standalone directional factor (QUANT_MODELS.md section 2, lines 107-111). Separately, the per-z-bucket BHAR-spread calibration (pead_jump.json, n=419,162) shows the post-reaction drift spread is strongly positive for small reactions — Newey-West t of 7.46 (z<1), 5.75 (1-2) and 3.35 (2-4), all above |t|>3 — but collapses to t = -0.06 at z>4, which is precisely why the already-priced flag zeroes the edge once the announcement move was extreme (served at avoid_surfaces.py:751,779,791). The honest caveats are therefore the survivor-biased universe, the T+2 capturable-only entry convention, and the thin pre-2015 estimate history — not a sub-threshold t on the active drift buckets.

Max defensible position size

How it's calculated

For your stock's (segment x volatility tercile x momentum direction) cell, we compute three caps from 25 years of 21-day returns: half-Kelly = 0.5 * max(0, mu/sigma-squared); a tail cap = 1% portfolio budget / empirical ES99 (the mean of the worst 1% of that cell's 21-day returns); and a catastrophe cap = 2% annual-loss budget / (segment bankruptcy rate x 0.6 loss-given-default). The displayed size is the smallest of those three, hard-capped at 10%. If the cell's measured edge mu is at or below zero, the size is zero ("no defensible size - avoid"). The binding constraint is named alongside the number. One serving detail: the "no defensible size - avoid" label actually fires whenever the size lands at or below 0.2%, so the thinnest real cells (penny names) read as "avoid" even though their Kelly fraction is small-but-positive rather than zero.

How it affects judgement

A tight ceiling (below ~1%) means the measured edge is thin relative to crash exposure: the tail cap (or, for the thinnest penny cells, Kelly itself) is holding the size down. A more generous ceiling (toward the 4-5% top of the live range) reflects a fatter measured edge relative to crash exposure, and raises the maximum weight this name can bear in a concentrated book before cross-name diversification is needed to keep tail exposure bounded. This is a sizing ceiling, not a directional call.

What the current value signifies

In the current 36-cell grid the tail cap binds in 33 of 36 cells and Kelly in the other 3, so nearly every value reflects crash exposure rather than the 10% backstop. Most names sit between 1% and 3% (the median cohort is ~2.25%); only low-volatility bank and large-cap names with positive momentum reach the 4-5% top of the live range. The thinnest cells are penny names with high volatility, which fall below 0.2% and render as "avoid". A 0% "no defensible size" is possible by construction when mu is non-positive, but no cell in the current grid actually hits it.

Range

In the live grid, values run from ~0.06% (penny, high-volatility, Kelly-bound floor) to ~4.58% (bank/large low-volatility with positive momentum, tail-bound); the median cohort is ~2.25%, and 33 of 36 cells fall between 1% and 5% with most below 3%. The 10% hard cap is a theoretical backstop that no current cohort comes near. A 0% "no defensible size" outcome exists by construction (when mu is non-positive) but does not occur in the current grid; the avoid label triggers below 0.2%.

The catch

The edge estimates (mu, sigma-squared) are cohort averages, not this stock's personal history. A name that recently crossed into a new segment or volatility tercile inherits the cell's historical stats, which may not match its current regime. Also note the avoid label fires at or below 0.2%, not strictly at zero edge, so a few thin-but-positive cells are shown as avoid.

Lottery profile

How it's calculated

Right now this badge does no computation at all — it always returns "not available." The intended idea: a "lottery" stock is one whose single best up-day in the last month was extreme relative to peers its own size. The code is built to bucket the stock into a size cohort (micro / small / mid / large) and, in the future, flag it if that peak day landed in the top decile for that cohort. But the input it needs — a true trailing-21-day MAX single-day return — does not exist in the database. The only 21-day field available (ret_1m) is the CUMULATIVE 21-bar return (e.g. 25.0 means the stock crept up 25% over the month), not the largest one-day jump. The author deliberately refused to substitute the cumulative number for the max-daily number (that would falsely print "+25% single day" for a stock with no big move), so per the flag-don't-fake rule it short-circuits to covered=False before computing anything (avoid_surfaces.py:678-690). Note on provenance: the calibration file max_lottery.json does NOT store any p90 percentile cutoff — it holds only per-cohort historical base rates (catastrophe frequency and median forward-12-month return for the high vs. low ends), built from 1,178,524 observations. No code today derives a p90 threshold from anywhere, neither from those medians nor from a live cross-section query.

How it affects judgement

When live, a lottery flag would tell you this stock sits in a size cohort whose top-decile names have historically had worse outcomes — and for the smallest stocks that gap is stark. Among micro-caps, the high-lottery group went on to a catastrophe (deep drawdown) 39% of the time with a median forward-12-month return of -24%, versus 16% catastrophe and +2% median for the low end. The spread shrinks as size grows (large-caps: 8% vs. 1% catastrophe). Read it as a base-rate caution about the kind of stock it is, not a forecast for this specific name. Today, with the badge offline, it adds nothing to your read either way.

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

Designed output, once the input exists: isLottery is binary — true (top-decile single-day spike for its size) or false (below it). The underlying max single-day return would be a positive decimal (0.12 = a +12% best day in the month). Until then every field is null and isLottery never resolves. The cohort base rates that drive the judgement are fixed values per size bucket in the calibration file (e.g. micro high-end 39% catastrophe / -24% median); the p90 cutoff that would decide the flag is not stored and not yet computed anywhere.

The catch

The metric is offline for all stocks. There is no max-single-day-return column in the data (only the cumulative 21-day return), so both code branches hardcode covered=False and return without computing (avoid_surfaces.py:665-690). The "compare to the cohort's 90th percentile" step is intended behavior that has not been built — neither the percentile cutoff nor a live cross-section query exists.

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.