Direction Ensemble
Will the stock be up or down 20 days from now? GBM ensemble vote.
An AI brain made of 7 little decision-tree models, each guessing whether the price will go up or down 20 days from now. We average their guesses to get one probability. When the little models all agree (low spread), trust the answer more. When they disagree, they're guessing — sit it out.
An ensemble of 7-14 boosted-tree models, each predicting whether the stock closes up or down 20 days from now. The probability is the mean of their votes; the conviction band reflects how much that probability deviates from the 0.5 noise floor. Lower ensemble σ = trees agree = higher trust.
GradientBoosting · 12 levers · cross-asset features · embargo CV
Real Direction Ensemble 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 directionEnsemble: BotDef = aiBot<DirReq, DirRes>(
{
id: "ai-direction",
name: "Direction Ensemble",
category: "ai",
glyph: "▲▼",
tagline: "Will the stock be up or down 20 days from now? GBM ensemble vote.",
formula: "GradientBoosting · 12 levers · cross-asset features · embargo CV",
endpoint: "/api/direction",
module: "ai quants/models/direction/train.py",
params: [
{ key: "horizon", label: "Horizon (days)", kind: "number", default: 20, min: 1, max: 60, step: 1 },
{ key: "ensemble", label: "Ensemble size", kind: "number", default: 10, min: 1, max: 30, step: 1 },
],
},
{
request: dirRequest,
build: (data) => {
const pUp = data.p_up;
const label = data.prediction.toUpperCase();
const band = data.conviction_band;
const expAcc = data.expected_accuracy;
const conviction = Math.abs(pUp - 0.5);
return {
signals: [],
metrics: [
{ key: "p", label: "P(up)", value: `${(pUp * 100).toFixed(1)}%`, tone: pUp > 0.5 ? "bull" : "bear" },
{ key: "label", label: "Direction", value: label, tone: pUp > 0.5 ? "bull" : "bear" },
{ key: "band", label: "Conviction", value: band.toUpperCase(), tone: band === "ultra" || band === "extreme" ? "bull" : band === "high" ? "info" : "neutral" },
{ key: "std", label: "Ensemble σ", value: fmtNum(data.ensemble_std, 3), tone: data.ensemble_std < 0.05 ? "bull" : "neutral", hint: "lower σ = models agree" },
{ key: "exp", label: "Expected acc", value: `${(expAcc * 100).toFixed(0)}%`, tone: "info" },
{ key: "size", label: "Ensemble", value: `${data.ensemble_size} models`, tone: "neutral" },
],
summary: `Real ensemble of ${data.ensemble_size} GBMs on ${data.ticker} says ${label} (P=${(pUp * 100).toFixed(1)}%) for ${data.horizon_days}d — ${band} conviction. Embargoed CV accuracy ~${(expAcc * 100).toFixed(0)}%.`,
beginner:
"Boosted-tree models each vote on whether the stock will be up or down 20 days from now. When they agree (low σ), trust the call more.",
verdict: {
side: pUp > 0.6 ? "buy" : pUp < 0.4 ? "sell" : "hold",
text: `${(pUp * 100).toFixed(0)}% chance ${data.prediction}. ${band} conviction — historical accuracy ~${(expAcc * 100).toFixed(0)}%.`,
confidence: Math.min(1, conviction * 4),
},
};
},
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 + horizon + Math.round(px[px.length - 1] * 100));
const rand = seedRand(seed);
const probs: number[] = [];
for (let i = 0; i < num(p, "ensemble", 10); i++) {
const noise = (rand() - 0.5) * 0.18;
const raw = 0.5 + trend * 1.4 + noise;
probs.push(Math.min(0.97, Math.max(0.03, raw)));
}
const pUp = probs.reduce((a, b) => a + b, 0) / probs.length;
const std = Math.sqrt(probs.reduce((a, b) => a + (b - pUp) ** 2, 0) / probs.length);
const conviction = Math.abs(pUp - 0.5);
const band = conviction > 0.20 ? "ultra" : conviction > 0.15 ? "extreme" : conviction > 0.07 ? "high" : "low";
const expAcc = band === "ultra" ? 0.77 : band === "extreme" ? 0.66 : band === "high" ? 0.61 : 0.555;
const label: "up" | "down" = pUp > 0.5 ? "up" : "down";
return {
signals: [],
metrics: [
{ key: "p", label: "P(up)", value: `${(pUp * 100).toFixed(1)}%`, tone: pUp > 0.5 ? "bull" : "bear" },
{ key: "label", label: "Direction", value: label.toUpperCase(), tone: label === "up" ? "bull" : "bear" },
{ key: "band", label: "Conviction", value: band.toUpperCase(), tone: band === "ultra" || band === "extreme" ? "bull" : band === "high" ? "info" : "neutral" },
{ key: "std", label: "Ensemble σ", value: fmtNum(std, 3), tone: std < 0.05 ? "bull" : "neutral", hint: "lower σ = models agree" },
{ key: "exp", label: "Expected acc", value: `${(expAcc * 100).toFixed(0)}%`, tone: "info" },
{ key: "rv", label: "Realised vol", value: `${(rv * 100).toFixed(0)}%`, tone: "neutral" },
],
summary: `Ensemble of ${probs.length} GBMs says ${label.toUpperCase()} (P=${(pUp * 100).toFixed(1)}%) for ${horizon}d — ${band} conviction.`,
beginner: "Boosted-tree models each vote on direction. When they agree (low σ), trust the call more.",
verdict: {
side: pUp > 0.6 ? "buy" : pUp < 0.4 ? "sell" : "hold",
text: `${(pUp * 100).toFixed(0)}% chance ${label}. ${band} conviction.`,
confidence: Math.min(1, conviction * 4),
},
};
},
},
);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.
- ·Names with strong macro coupling (SPY, QQQ, AAPL). The features include cross-asset signals — VIX, DXY, 10Y, WTI — that anchor the prediction.
- ·Mid-cycle markets. The models see lots of those in training and generalise well.
- ·When ensemble σ is low (<0.05). That means the trees agree; you're not betting on a single fluky leaf.
- ·Earnings windows. The model treats the 20-day horizon as homogeneous, but earnings can flip the sign in a single bar.
- ·Illiquid tickers. Need ≥250 bars; small-caps with thin history under-perform.
- ·Tail events. The labels were clipped at the 99th percentile — the model literally hasn't seen 2020 March.
BUY at p_up > 0.6, SELL at p_up < 0.4, HOLD in between. Conviction band classifies that probability into LOW / HIGH / EXTREME / ULTRA, and the historical accuracy of each band is published on the card.
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 8000What features does it use?+
Why ensemble of 7 instead of 1?+
All AI quants vote. Tier emerges from agreement, not opinion.
Predicts the size of the next 20-day move, not just the sign.
Honest confidence interval — not just a point prediction.
Attention over a full year of OHLCV. Spots seasonality + regime.