Quantile Forecast (p10/p50/p90)
Honest confidence interval — not just a point prediction.
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.
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.
GradientBoosting Quantile Regressor · 3 heads · pinball loss
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.
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.
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,
},
};
},
},
);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.
- Copy the whole block above.
- On /quant, click + Import your bot in the bot library.
- Paste, hit save. It hot-loads into your workspace.
- Edit any param defaults or logic to your taste — it's now yours.
- ·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.
- ·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.
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.
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.
cd "ai quants" && uvicorn serve:app --reload --port 8000Why three quantiles instead of mean ± std?+
What's pinball loss?+
Will the stock be up or down 20 days from now? GBM ensemble vote.
Predicts the size of the next 20-day move, not just the sign.
Worst loss you'd expect 95% of the time.
All AI quants vote. Tier emerges from agreement, not opinion.
Attention over a full year of OHLCV. Spots seasonality + regime.