LIVE·
fetching live quotes from Yahoo Finance…
--:--:--UTC
Learn/Bots/Quantile Forecast (p10/p50/p90)
AI QuantsAIid · ai-quantile
│⁞│

Quantile Forecast (p10/p50/p90)

Honest confidence interval — not just a point prediction.

▶ Try it now↗ Browse all 27srcai quants/models/quantile/train.pyapi/api/quantile
In plain English

Most predictors give you one number. This one gives three: best case, worst case, and middle. If even the worst case is positive, that's a strong long signal. If even the best case is negative, that's a strong short. Anything in between, sit it out.

No jargon. Just what this bot does.
The longer version

Most predictors give you one number. This bot gives three — the 10th, 50th, and 90th percentile of the 20-day return distribution. That's a real confidence interval. If even the worst-case (p10) is positive, you've got a clean long signal. If even the best-case (p90) is negative, that's a clean short. Anything in between, sit it out.

The math
formula
GradientBoosting Quantile Regressor · 3 heads · pinball loss
parameters
horizonHorizon (days)range 5 → 60default · 20
Live demo

Real Quantile Forecast (p10/p50/p90) bot, running on real Yahoo data when the symbol is available. Drag the params — the bot re-runs instantly.

symbolloading…
loading AMZN bars…
Source code · public

This is the actual code the bot runs — not a re-explanation, not a simplified version. Whatever ships here is what executes when you press Run All in the workbench. Read it, copy it, fork it, build a better one.

lib/quant/ai-bots.ts·lines 887956
TypeScript · MIT-licensed
const quantileForecast: BotDef = aiBot<DirReq, QuantRes>(
  {
    id: "ai-quantile",
    name: "Quantile Forecast (p10/p50/p90)",
    category: "ai",
    glyph: "│⁞│",
    tagline: "Honest confidence interval — not just a point prediction.",
    formula: "GradientBoosting Quantile Regressor · 3 heads · pinball loss",
    endpoint: "/api/quantile",
    module: "ai quants/models/quantile/train.py",
    params: [
      { key: "horizon", label: "Horizon (days)", kind: "number", default: 20, min: 5, max: 60, step: 1 },
    ],
  },
  {
    request: dirRequest,
    build: (data) => {
      const decision = data.decision;
      return {
        signals: [],
        metrics: [
          { key: "p10", label: "p10 (worst)", value: `${(data.p10 * 100).toFixed(2)}%`, tone: "bear" },
          { key: "p50", label: "p50 (median)", value: `${(data.p50 * 100).toFixed(2)}%`, tone: data.p50 > 0 ? "bull" : "bear" },
          { key: "p90", label: "p90 (best)", value: `${(data.p90 * 100).toFixed(2)}%`, tone: "bull" },
          { key: "width", label: "80% CI width", value: `${(data.uncertainty_width * 100).toFixed(2)}%`, tone: "info" },
          { key: "decision", label: "Edge", value: decision.toUpperCase(), tone: decision === "long" ? "bull" : decision === "short" ? "bear" : "neutral" },
        ],
        summary: `80% CI: [${(data.p10 * 100).toFixed(2)}%, ${(data.p90 * 100).toFixed(2)}%]. Median ${(data.p50 * 100).toFixed(2)}%. Decision: ${decision.toUpperCase()}.`,
        beginner:
          "Most predictors give one number. This one gives three — best case (p90), worst case (p10), median (p50). Edge fires when the entire interval is on one side of zero.",
        verdict: {
          side: decision === "long" ? "buy" : decision === "short" ? "sell" : "hold",
          text: decision === "flat" ? "Range straddles zero — no clean edge." : `${decision === "long" ? "Long" : "Short"} signal — entire 80% interval ${decision === "long" ? "above" : "below"} zero.`,
          confidence: decision === "flat" ? 0.2 : 0.85,
        },
      };
    },
    mock: (ctx, p) => {
      const horizon = num(p, "horizon", 20);
      const px = closes(ctx.candles);
      const trend = trendStrength(px);
      const rv = realisedVol(px);
      const seed = hashStr(ctx.symbol + "quant" + horizon);
      const rand = seedRand(seed);
      const center = trend * 0.55 + (rand() - 0.5) * 0.02;
      const spread = rv * Math.sqrt(horizon / 252) * 1.28;
      const p10 = center - spread;
      const p50 = center;
      const p90 = center + spread;
      const decision = p10 > 0 ? "long" : p90 < 0 ? "short" : "flat";
      return {
        signals: [],
        metrics: [
          { key: "p10", label: "p10 (worst)", value: `${(p10 * 100).toFixed(2)}%`, tone: "bear" },
          { key: "p50", label: "p50 (median)", value: `${(p50 * 100).toFixed(2)}%`, tone: p50 > 0 ? "bull" : "bear" },
          { key: "p90", label: "p90 (best)", value: `${(p90 * 100).toFixed(2)}%`, tone: "bull" },
          { key: "width", label: "80% CI width", value: `${((p90 - p10) * 100).toFixed(2)}%`, tone: "info" },
          { key: "decision", label: "Edge", value: decision.toUpperCase(), tone: decision === "long" ? "bull" : decision === "short" ? "bear" : "neutral" },
        ],
        summary: `80% CI: [${(p10 * 100).toFixed(2)}%, ${(p90 * 100).toFixed(2)}%]. Decision: ${decision.toUpperCase()}.`,
        beginner: "Best-case, worst-case, median — three numbers, not one.",
        verdict: {
          side: decision === "long" ? "buy" : decision === "short" ? "sell" : "hold",
          text: decision === "flat" ? "Range straddles zero." : `${decision === "long" ? "Long" : "Short"} signal.`,
          confidence: decision === "flat" ? 0.2 : 0.85,
        },
      };
    },
  },
);
what each piece means
  • id — unique key the workbench uses to find the bot.
  • params — the sliders + inputs you see on the cell.
  • run(ctx, p) — the function that gets called with candles + your params and returns the verdict.
  • verdict — the BUY / SELL / HOLD pill at the top of the cell.
  • metrics — the small stat boxes shown in the cell body.
use this code yourself
  1. Copy the whole block above.
  2. On /quant, click + Import your bot in the bot library.
  3. Paste, hit save. It hot-loads into your workspace.
  4. Edit any param defaults or logic to your taste — it's now yours.
Specialty · when it shines, when it fails
✓ Shines when
  • ·Risk-aware decisions. The width of the interval tells you how confident the model is — narrow CI = confident, wide CI = market is genuinely uncertain.
  • ·Liquid names where the underlying return distribution is well-behaved (ETFs, large-caps).
  • ·Combined with Kelly sizing. The p10 and p90 give you natural inputs for an EV calculation.
  • ·Periods of expanding volatility. Quantile regression handles heteroskedastic returns better than a point estimate.
✗ Fails when
  • ·Highly skewed return distributions. The pinball loss assumes the conditional CDF is well-defined; broken at the extremes.
  • ·Small datasets. Each quantile has its own model; need lots of data to estimate p10 and p90 reliably.
  • ·Tail events. p10 still under-estimates true downside in a 6-sigma move.
How to read its verdict

BUY (long) only when p10 > 0 — the entire 80% interval clears zero. SELL (short) only when p90 < 0. Otherwise FLAT (HOLD). Confidence is 0.85 when the bot fires, 0.2 when flat. Width of the interval is published on the card and is the single best 'how uncertain is the market?' indicator we have.

Python service

This bot tries to call the FastAPI service first. When it's up, you get real model output. When it's down, the bot transparently falls back to a deterministic TS surrogate.

FastAPI·http://localhost:8000·/api/quantileCHECKING…
data flow
01
BotCell.run()
User clicks Run on this bot in /quant
02
callApi()
POST to localhost:8000/api/quantile
03
load_surrogate()
ai quants/models/quantile/train.py
04
predict()
Forward pass on the inputs you provided
05
BotResult
JSON returned, card flips green Source: Python NN
spin it upcd "ai quants" && uvicorn serve:app --reload --port 8000
FAQ
Why three quantiles instead of mean ± std?+
Returns aren't Gaussian. Mean ± 2σ implies a symmetric distribution; quantiles let the model learn whatever shape the data actually has. p10 isn't necessarily mirror-image of p90 — that asymmetry is real information about skew.
What's pinball loss?+
The loss function for quantile regression. For the 0.9 quantile, it penalises predictions that are too low much more than too high. The opposite for 0.1. That's how the model learns to honestly estimate tails instead of just the mean.