LIVE
connecting to the live quote stream…
--:--:--UTC
Learn/Bots/LinReg Channel
Statistical
λ

LinReg Channel

Linear regression line + std-deviation envelopes.

⟢In plain English

Draws the best-fit straight line through recent prices, then puts dotted lines a few standard deviations above and below. Touching the top line is overextended — sell. Touching the bottom is washed-out — buy.

Draws the best-fit line through recent prices, then puts dotted lines a few standard deviations above and below. Touching the top is overextended; the bottom is washed out. The trend slope of the line itself is its own signal — positive slope = uptrend, negative = down.

⟢The math
formula
y = β·x + α; bands = y ± k·σ(residuals)
parameters
periodPeriodrange 20 → 200default · 60
bandsStd bandsrange 1 → 3default · 2
⟢Live demo

Real LinReg Channel 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/bots.ts·lines 538–596
TypeScript · MIT-licensed
const linregBot: BotDef = {
  id: "linreg",
  name: "LinReg Channel",
  category: "stats",
  glyph: "λ",
  tagline: "Linear regression line + std-deviation envelopes.",
  formula: "y = β·x + α; bands = y ± k·σ(residuals)",
  params: [
    { key: "period", label: "Period", kind: "number", default: 60, min: 20, max: 200, step: 1 },
    { key: "bands", label: "Std bands", kind: "number", default: 2, min: 1, max: 3, step: 0.1 },
  ],
  run: (ctx, p): BotResult => {
    const period = Math.round(num(p, "period", 60));
    const k = num(p, "bands", 2);
    const px = closes(ctx.candles);
    const r = linregChannel(px, period, k);
    const signals: Signal[] = [];
    const sigArr: (1 | -1 | 0)[] = px.map(() => 0);
    for (let i = 1; i < px.length; i++) {
      const u = r.upper[i], l = r.lower[i];
      if (u == null || l == null) continue;
      if (px[i - 1] < (r.lower[i - 1] ?? -Infinity) && px[i] >= l) {
        signals.push({ i, kind: "long", price: px[i] });
        sigArr[i] = 1;
      } else if (px[i - 1] > (r.upper[i - 1] ?? Infinity) && px[i] <= u) {
        signals.push({ i, kind: "short", price: px[i] });
        sigArr[i] = -1;
      }
    }
    const bt = backtestLongOnly(px, sigArr);
    const last = px[px.length - 1];
    const lm = r.mid[r.mid.length - 1] ?? last;
    const lu = r.upper[r.upper.length - 1] ?? last;
    const ll = r.lower[r.lower.length - 1] ?? last;
    return {
      signals,
      metrics: [
        { key: "fair", label: "Trend line", value: fmtNum(lm), tone: "info" },
        { key: "upper", label: "Upper", value: fmtNum(lu), tone: "bear" },
        { key: "lower", label: "Lower", value: fmtNum(ll), tone: "bull" },
        { key: "trades", label: "Trades", value: String(bt.trades) },
      ],
      overlay: [
        { values: r.mid, color: "var(--fg-dim)", label: "LinReg" },
        { values: r.upper, color: "var(--bear-dim)", label: "Upper", dashed: true },
        { values: r.lower, color: "var(--bull-dim)", label: "Lower", dashed: true },
      ],
      summary: `Trend ${lm.toFixed(2)}, channel ${ll.toFixed(2)}–${lu.toFixed(2)}.`,
      beginner:
        "Draws the best-fit line through recent prices, then puts dotted lines a few standard deviations above and below. Touching the top is overextended; the bottom is washed out.",
      verdict: {
        side: last < ll ? "buy" : last > lu ? "sell" : "hold",
        text: last < ll ? "Below lower channel." : last > lu ? "Above upper channel." : "Inside the channel.",
        confidence: Math.min(1, Math.abs(last - lm) / (lu - lm || 1)),
      },
      equity: bt.equity,
    };
  },
};
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
  • ·Visualising trend strength. Slope and channel width together summarise the regime.
  • ·Setting stops. Lower channel for long stops, upper channel for short stops.
  • ·Combining with momentum. Trade reversions inside the channel; trend-follow when price pierces the band.
✗ Fails when
  • ·Sharp regime changes. The regression refits; the channel snaps to the new trend, briefly leaving recent price action outside the band.
  • ·Curved trends (parabolic moves). Linear fits a curved trajectory poorly.
  • ·Window too short. <30 bars gives unstable slope estimates.
⟢How to read its verdict

BUY when price reclaims the lower band from below. SELL when it rejects the upper band. Confidence scales with distance from the mid-line. Slope of the regression line is published in the metrics — sustained positive slope means the trend bot interpretation is the safer one.

⟢FAQ
How is this different from Bollinger?+
Bollinger's mid-line is an SMA — slope-agnostic. LinReg's mid-line is a least-squares fit — it explicitly captures slope. In trending markets they diverge sharply; in ranges they look similar.
What's the right window?+
Match the window to your trade horizon. 60 bars (~3 months daily) for swing trades. 252 bars for position trades. Shorter windows over-fit recent noise.