LIVE
connecting to the live quote stream…
--:--:--UTC
Learn/Bots/Magnitude Regression
AI QuantsAI
Δ%

Magnitude Regression

Predicts the size of the next 20-day move, not just the sign.

In plain English

Direction tells you which way the price will move. This bot tells you how big the move will be. A 0.5% move is just noise. A 5% move is real. The size of the predicted move IS the conviction.

Direction's quieter sibling. Instead of asking 'up or down?', it predicts the size of the next 20-day move. A 0.5% expected return is meaningless noise; a 5% expected return clears the bot's noise floor and registers as a high-magnitude reading. The sign tells you direction, the magnitude tells you conviction.

The math
formula
HistGradientBoosting regressor · 5-fold ensemble
parameters
horizonHorizon (days)range 5 → 60default · 20
macroUse macro featureson / offdefault · true
Live demo

Real Magnitude Regression 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 703778
TypeScript · MIT-licensed
const magnitudeReg: BotDef = aiBot<DirReq, MagRes>(
  {
    id: "ai-magnitude",
    name: "Magnitude Regression",
    category: "ai",
    glyph: "Δ%",
    tagline: "Predicts the size of the next 20-day move, not just the sign.",
    formula: "HistGradientBoosting regressor · 5-fold ensemble",
    endpoint: "/api/magnitude",
    module: "ai quants/models/magnitude/train.py",
    params: [
      { key: "horizon", label: "Horizon (days)", kind: "number", default: 20, min: 5, max: 60, step: 1 },
      { key: "macro", label: "Use macro features", kind: "boolean", default: true, hint: "VIX · DXY · 10Y · WTI" },
    ],
  },
  {
    request: dirRequest,
    build: (data, _ctx, p) => {
      const expRet = data.expected_return;
      const macro = p["macro"] !== false;
      const band = data.magnitude_band;
      const expAcc = data.expected_dir_accuracy;
      return {
        signals: [],
        metrics: [
          { key: "exp", label: "Expected return", value: `${(expRet * 100).toFixed(2)}%`, tone: expRet > 0 ? "bull" : "bear" },
          { key: "band", label: "Magnitude", value: band.toUpperCase(), tone: band === "ultra" || band === "extreme" ? "bull" : "info" },
          { key: "dir", label: "Direction", value: data.direction.toUpperCase(), tone: data.direction === "up" ? "bull" : "bear" },
          { key: "std", label: "Ensemble σ", value: fmtNum(data.ensemble_std, 3) },
          { key: "exp_acc", label: "Dir accuracy", value: `${(expAcc * 100).toFixed(0)}%`, tone: "info" },
          { key: "macro", label: "Macro features", value: macro ? "ON" : "OFF", tone: macro ? "bull" : "neutral" },
        ],
        summary: `Real model predicts ${(expRet * 100).toFixed(2)}% over ${data.horizon_days}d (${band}). ${macro ? "Macro features on" : "Pure price"}.`,
        beginner: "Beyond direction — predicts how BIG the move will be. Sizing depends on this.",
        verdict: {
          side: expRet > 0.02 ? "buy" : expRet < -0.02 ? "sell" : "hold",
          text: `Expected ${(expRet * 100).toFixed(2)}% over ${data.horizon_days}d (${band}).`,
          confidence: Math.min(1, Math.abs(expRet) * 12),
        },
      };
    },
    mock: (ctx, p) => {
      const horizon = num(p, "horizon", 20);
      const macro = p["macro"] !== false;
      const px = closes(ctx.candles);
      const trend = trendStrength(px);
      const rv = realisedVol(px);
      const seed = hashStr(ctx.symbol + "magnitude" + horizon);
      const rand = seedRand(seed);
      const expRet = trend * 0.6 + (rand() - 0.5) * rv * 0.4;
      const std = rv * 0.18;
      const dir = expRet > 0 ? "up" : "down";
      const absMag = Math.abs(expRet);
      const band = absMag > 0.07 ? "ultra" : absMag > 0.045 ? "extreme" : absMag > 0.027 ? "high" : absMag > 0.015 ? "medium" : "low";
      const expAcc = band === "ultra" ? 0.66 : band === "extreme" ? 0.64 : band === "high" ? 0.61 : band === "medium" ? 0.60 : 0.555;
      return {
        signals: [],
        metrics: [
          { key: "exp", label: "Expected return", value: `${(expRet * 100).toFixed(2)}%`, tone: expRet > 0 ? "bull" : "bear" },
          { key: "band", label: "Magnitude", value: band.toUpperCase(), tone: band === "ultra" || band === "extreme" ? "bull" : "info" },
          { key: "dir", label: "Direction", value: dir.toUpperCase(), tone: dir === "up" ? "bull" : "bear" },
          { key: "std", label: "Ensemble σ", value: fmtNum(std, 3) },
          { key: "exp_acc", label: "Dir accuracy", value: `${(expAcc * 100).toFixed(0)}%`, tone: "info" },
          { key: "macro", label: "Macro features", value: macro ? "ON" : "OFF", tone: macro ? "bull" : "neutral" },
        ],
        summary: `Predicts ${(expRet * 100).toFixed(2)}% over ${horizon}d (${band}).`,
        beginner: "Beyond direction — predicts how BIG the move will be.",
        verdict: {
          side: expRet > 0.02 ? "buy" : expRet < -0.02 ? "sell" : "hold",
          text: `Expected ${(expRet * 100).toFixed(2)}% over ${horizon}d (${band}).`,
          confidence: Math.min(1, absMag * 12),
        },
      };
    },
  },
);
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
  • ·Position sizing decisions. Direction alone tells you which way to bet; magnitude tells you how much.
  • ·Filtering Direction Ensemble's signals — when both bots agree on direction AND magnitude clears 3%, it's a stronger setup than either alone.
  • ·Names with persistent edges (sector ETFs, large-cap growth). The regression learns dampened magnitudes that match real return distributions.
  • ·When 'macro features' is on. VIX and 10Y rate alignment lifts accuracy by ~2pts.
✗ Fails when
  • ·Turning points. Regression smooths over inflections; magnitude underestimates reversals.
  • ·High-vol assets. The MSE objective penalises large errors disproportionately, so the model under-bets on tail moves.
  • ·Out-of-distribution names. Like Direction, needs ≥250 bars and US-equity-shaped data.
How to read its verdict

BUY when expected return > +2%, SELL when < -2%, HOLD in between. Magnitude band classifies |expRet| into LOW (<1.5%), MEDIUM (1.5-2.7%), HIGH (2.7-4.5%), EXTREME (4.5-7%), ULTRA (>7%). Confidence scales linearly with |expRet| up to a cap of 1.0 at 8%.

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.

srcai quants/models/magnitude/train.pyapi/api/magnitude
FastAPI··/api/magnitudeCHECKING…
How the request flows+
01
BotCell.run()
User clicks Run on this bot in /quant
02
callApi()
POST to localhost:8000/api/magnitude
03
load_surrogate()
ai quants/models/magnitude/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
What's the difference between this and Direction Ensemble?+
Direction is a binary classifier — its output is P(up). This is a regression — its output is expected return. They use different loss functions, different model architectures, and different conviction bands. Stack both and use the agreement.
Why does it sometimes contradict Direction?+
Different objective functions. Classification is forced to commit to a side; regression can output a number near zero. When they disagree, the magnitude is usually small and HOLD is the right call.