Back to Chart
Getting Started

Build your own indicators & trading bots

AlgoCdk lets you write plain JavaScript files that plug directly into the live chart. You write the logic — the platform handles the WebSocket, chart rendering, and trade execution automatically.

What can you build?
  • Custom Indicators — draw anything on the chart: lines, bars, shapes, overlays, or oscillators in a second pane below
  • Trading Bots — analyse candles and automatically place Rise/Fall, Digit Over/Under, Match/Differ, Even/Odd contracts on Deriv
  • Strategy Backtests — load a bot into the Strategy Lab and replay it against historical data before going live
Where to load your file
1

Custom Indicator

Chart page → Indicators dropdown → Manage My Indicators → Upload .js file

/app → Indicators → My Indicators
2

Strategy Lab Bot

Chart page → Strategy button (top right) → Load tab → drag in your .js file → Run Backtest or go live

/app → Strategy → Load
3

Digit Lab Bot

Go to Digit Lab → Load Bot File button → select your .js file → Start Bot

/digit-lab → Load Bot File
4

Bot Store

Publish your bot to the marketplace so other users can discover and use it

/botstore → Upload
How the platform calls your code
New candle arrives calculate() runs draw() renders getSignalAt() fires Trade placed onTradeResult()

You never touch the WebSocket or chart library. Just write the logic inside the functions and return the right values.

Every file is a plain JS object literal wrapped in ({}). No imports, no export default, no build step needed.
Data Model

Candle Data

Every function receives an array of candle objects sorted oldest to newest. data[data.length-1] is always the current candle.

Candle object fields
FieldTypeDescription
candle.opennumberOpening price
candle.highnumberHighest price during the candle
candle.lownumberLowest price during the candle
candle.closenumberClosing price — most commonly used
candle.timenumberUnix timestamp (seconds) of candle open
candle.volumenumberVolume (always 0 on Deriv synthetic indices)
common patternsJS
// All closing prices as a plain array
const closes = data.map(c => c.close);

// Last digit of close price (digit contracts)
const digit = parseInt(String(c.close).slice(-1));

// Candle body size
const body = Math.abs(c.close - c.open);

// True range
const tr = Math.max(c.high - c.low,
  Math.abs(c.high - prev.close),
  Math.abs(c.low  - prev.close));
The array is always sorted oldest first. Index 0 is the oldest candle, the last index is the current one.
Indicators

Indicator Structure

An indicator is a JS object with a fixed set of fields. The platform reads these to know how to render it.

Fields
FieldTypeNotes
namestringREQUIRED Display name in the UI
colorstringOPTIONAL Default line colour e.g. #FF4500
lineWidthnumberOPTIONAL Stroke width (default 2)
hasWindow2booleanOPTIONAL true = draw in a separate pane below the chart
defaultParamsobjectOPTIONAL User-editable parameters with defaults
calculatefunctionREQUIRED Returns an array of values from candle data
drawfunctionOPTIONAL Custom canvas drawing. Omit to use the default line renderer
indicator skeletonJS
({
  name: 'My Indicator',
  color: '#FF4500',
  lineWidth: 2,
  hasWindow2: false,

  defaultParams: {
    period: 14,
    color:  '#FF4500',
  },

  // Required — return one value per candle
  calculate(data, params) {
    return data.map(() => null);
  },

  // Optional — custom canvas drawing
  draw(ctx, values, pad, spacing, yFunc, params) {
  },
})
Indicators

calculate(data, params)

Called every time new candle data arrives. Return an array of numbers the same length as data. Use null for warmup positions where the indicator has no value yet.

Parameters & return value
ParamTypeDescription
dataCandle[]Full candle array, oldest first
paramsobjectMerged defaultParams + user overrides

Returns: number[] — one value per candle. Return null at index i if the indicator is not ready yet.

EMA exampleJS
calculate(data, params) {
  const period = params.period || 14;
  const k = 2 / (period + 1);
  const out = [];
  let ema = data[0].close;

  for (let i = 0; i < data.length; i++) {
    if (i < period - 1) { out.push(null); continue; }
    ema = data[i].close * k + ema * (1 - k);
    out.push(ema);
  }
  return out;
},
If you skip draw(), the platform automatically draws your returned array as a line using color and lineWidth. You only need draw() for custom shapes, bars, or oscillators.
Indicators

draw(ctx, values, pad, spacing, yFunc, params)

Optional custom canvas renderer. Use the HTML5 Canvas 2D API directly.

Parameters
ParamTypeDescription
ctxCanvasRenderingContext2DDraw directly on this
valuesnumber[]Array returned by your calculate()
padnumberLeft pixel offset where the chart area starts
spacingnumberPixels per candle (bar width)
yFuncfunction(price) → yConverts a price to a canvas Y coordinate
paramsobjectUser params (same as in calculate)
draw a lineJS
draw(ctx, values, pad, spacing, yFunc, params) {
  ctx.strokeStyle = params.color || '#FF4500';
  ctx.lineWidth   = 2;
  ctx.beginPath();
  let started = false;

  values.forEach((val, i) => {
    if (val === null) { started = false; return; }
    const x = pad + i * spacing + spacing / 2;
    const y = yFunc(val);
    started ? ctx.lineTo(x, y) : (ctx.moveTo(x, y), started = true);
  });
  ctx.stroke();
},
Indicators

Drawing in a Second Pane

Set hasWindow2: true to get a separate pane below the main chart. Use the globals below to position your drawing correctly.

Window 2 globals
VariableTypeDescription
window.window2Bounds{ y, height }Top Y and height of the pane in pixels
window.window2HeightnumberHeight of the second pane in pixels
window.mainChartHeightnumberHeight of the main chart in pixels

At the end of your draw(), always write back: window.window2Bounds = { y: w2Y, height: w2H }

second pane patternJS
draw(ctx, values, pad, spacing, yFunc, params) {
  const w2Y = window.window2Bounds?.y ?? (window.mainChartHeight + 10);
  const w2H = window.window2Height ?? 150;
  const cW  = ctx.canvas.width - pad * 2;

  ctx.fillStyle = 'rgba(10,14,26,0.97)';
  ctx.fillRect(pad, w2Y, cW, w2H);

  // draw your content here...

  // REQUIRED at the end
  window.window2Bounds = { y: w2Y, height: w2H };
},
Indicators

Full Indicator Example — RSI

A complete working RSI indicator with a custom draw function that renders in the second pane with overbought/oversold lines.

rsi-indicator.jsJS
({
  name: 'RSI',
  color: '#22d3ee',
  hasWindow2: true,
  defaultParams: { period: 14, overbought: 70, oversold: 30, color: '#22d3ee' },

  calculate(data, params) {
    const p = params.period || 14;
    let g = 0, l = 0;
    const out = new Array(data.length).fill(null);
    for (let i = 1; i <= p; i++) {
      const d = data[i].close - data[i-1].close;
      d > 0 ? g += d : l -= d;
    }
    let ag = g/p, al = l/p;
    out[p] = al === 0 ? 100 : 100 - 100/(1+ag/al);
    for (let i = p+1; i < data.length; i++) {
      const d = data[i].close - data[i-1].close;
      ag = (ag*(p-1) + Math.max(0,d)) / p;
      al = (al*(p-1) + Math.max(0,-d)) / p;
      out[i] = al === 0 ? 100 : 100 - 100/(1+ag/al);
    }
    return out;
  },

  draw(ctx, values, pad, spacing, yFunc, params) {
    const w2Y = window.window2Bounds?.y ?? (window.mainChartHeight + 10);
    const w2H = window.window2Height ?? 150;
    const cW  = ctx.canvas.width - pad * 2;
    ctx.fillStyle = 'rgba(10,14,26,0.97)';
    ctx.fillRect(pad, w2Y, cW, w2H);
    [params.overbought||70, 50, params.oversold||30].forEach(lvl => {
      const y = w2Y + (1 - lvl/100) * w2H;
      ctx.strokeStyle = lvl === 50 ? 'rgba(255,255,255,0.06)' : 'rgba(255,69,0,0.3)';
      ctx.lineWidth = 1; ctx.setLineDash([4,4]);
      ctx.beginPath(); ctx.moveTo(pad,y); ctx.lineTo(pad+cW,y); ctx.stroke();
    });
    ctx.setLineDash([]);
    ctx.strokeStyle = params.color || '#22d3ee';
    ctx.lineWidth = 1.5; ctx.beginPath();
    let started = false;
    values.forEach((v, i) => {
      if (v === null) { started = false; return; }
      const x = pad + i * spacing + spacing/2;
      const y = w2Y + (1 - v/100) * w2H;
      started ? ctx.lineTo(x,y) : (ctx.moveTo(x,y), started=true);
    });
    ctx.stroke();
    window.window2Bounds = { y: w2Y, height: w2H };
  },
})
Trading Bots

Trading Bot Structure

A bot uses the same object format as an indicator but adds trading-specific methods. The platform calls getSignalAt() on every candle and executes trades automatically.

Bot fields
FieldTypeNotes
namestringREQUIRED Bot display name
descriptionstringOPTIONAL Short description shown in the UI
defaultParamsobjectOPTIONAL Editable params: stake, duration, SL, TP, etc.
calculatefunctionOPTIONAL Pre-compute indicators once per candle batch
getSignalAtfunctionREQUIRED Returns a signal object or null
onTradeResultfunctionOPTIONAL Called when a trade settles
drawfunctionOPTIONAL Render a stats panel in window 2
hasWindow2booleanOPTIONAL Request a second pane for your draw()
bot skeletonJS
({
  name: 'My Bot',
  description: 'What it does',
  defaultParams: {
    stake: 1, duration: 1, duration_unit: 't',
    stopLoss: 20, takeProfit: 50, martingale: 1, maxStake: 50,
  },
  calculate(data, params) { /* optional pre-compute */ },
  getSignalAt(candles, index, params) { return null; },
  onTradeResult(result, params) { /* optional */ },
})
Trading Bots

getSignalAt(candles, index, params)

The core of your bot. Called on every candle. Return a signal object to place a trade, or null to skip.

Parameters
ParamTypeDescription
candlesCandle[]Full candle history up to and including index
indexnumberCurrent candle index (usually candles.length - 1)
paramsobjectMerged defaultParams + user overrides
Signal object — what to return
FieldTypeDescription
signal'buy'|'sell'REQUIRED 'buy' = Rise/Call/Over, 'sell' = Fall/Put/Under
contract_typestringOPTIONAL Override e.g. DIGITOVER, CALL
barrierstringOPTIONAL Barrier for digit contracts e.g. '5'
stakenumberOPTIONAL Override stake for this trade
durationnumberOPTIONAL Override duration
duration_unitstringOPTIONAL 't' 's' 'm' 'h' 'd'
confidencenumberOPTIONAL 0–100, shown in the UI
labelstringOPTIONAL Short label e.g. 'Over 5'
EMA cross signalJS
getSignalAt(candles, index, params) {
  if (candles.length < 30) return null;
  if (params.stopLoss   > 0 && this._pnl <= -params.stopLoss)  return null;
  if (params.takeProfit > 0 && this._pnl >= params.takeProfit) return null;

  const closes = candles.map(c => c.close);
  const k = 2 / (params.fastPeriod + 1);
  let fast = closes[0], slow = closes[0];
  closes.forEach(v => { fast = v*k + fast*(1-k); slow = v*(2/22) + slow*(1-2/22); });

  if (fast > slow) return { signal: 'buy',  stake: params.stake, duration: params.duration, duration_unit: params.duration_unit, label: 'EMA Cross Up' };
  if (fast < slow) return { signal: 'sell', stake: params.stake, duration: params.duration, duration_unit: params.duration_unit, label: 'EMA Cross Down' };
  return null;
},
Trading Bots

onTradeResult(result, params)

Called automatically after each trade settles. Use this to implement martingale, track stats, or adjust your next stake.

result object
FieldTypeDescription
result.profitnumberProfit/loss in USD. Positive = win, negative = loss
result.contract_idstringDeriv contract ID
result.statusstring'won' or 'lost'
martingale patternJS
_stake: null, _wins: 0, _losses: 0, _pnl: 0,

onTradeResult(result, params) {
  if (result.profit > 0) {
    this._wins++; this._pnl += result.profit;
    this._stake = params.stake; // reset to base stake on win
  } else {
    this._losses++; this._pnl += result.profit;
    this._stake = Math.min(
      (this._stake || params.stake) * (params.martingale || 1),
      params.maxStake || 50
    );
  }
},
Trading Bots

defaultParams

All fields in defaultParams are exposed as editable inputs in the Strategy Lab UI. Users can change them without touching your code.

Common params convention
KeyDefaultDescription
stake1Base stake per trade in USD
duration1Contract duration number
duration_unit't''t'=ticks, 's'=seconds, 'm'=minutes, 'h'=hours, 'd'=days
stopLoss0Stop bot when total PnL drops below this (0 = off)
takeProfit0Stop bot when total PnL reaches this (0 = off)
martingale1Stake multiplier after a loss (1 = no martingale)
maxStake50Maximum stake cap when using martingale
cooldown0Candles to skip after a loss before trading again
minConfidence60Minimum confidence score to fire a signal
Trading Bots

Digit Contract Bots

Digit contracts trade on the last digit of the closing price. Return a specific contract_type and barrier from getSignalAt().

Digit contract types
contract_typebarrierWin condition
DIGITOVER"2"–"8"Last digit is strictly greater than barrier
DIGITUNDER"1"–"7"Last digit is strictly less than barrier
DIGITMATCH"0"–"9"Last digit exactly matches barrier
DIGITDIFF"0"–"9"Last digit does NOT match barrier
DIGITEVENnullLast digit is even (0,2,4,6,8)
DIGITODDnullLast digit is odd (1,3,5,7,9)
digit signal exampleJS
getSignalAt(candles, index, params) {
  const digits = candles.map(c => parseInt(String(c.close).slice(-1)));
  const recent = digits.slice(-50);
  const overRate = recent.filter(d => d > 4).length / recent.length;

  if (overRate > 0.62) {
    return {
      signal:        'buy',
      contract_type: 'DIGITOVER',
      barrier:       '4',
      stake:         params.stake,
      duration:      1,
      duration_unit: 't',
      confidence:    Math.round(overRate * 100),
      label:         'Over 4',
    };
  }
  return null;
},
Trading Bots

Full Bot Example — EMA Crossover

A complete Rise/Fall bot with martingale, stop-loss, and a stats panel in window 2.

ema-cross-bot.jsJS
({
  name: 'EMA Cross Bot',
  hasWindow2: true,
  defaultParams: { fastPeriod:9, slowPeriod:21, stake:1, duration:5, duration_unit:'t', martingale:2, maxStake:50, stopLoss:20, takeProfit:50 },
  _wins:0, _losses:0, _pnl:0, _stake:null,

  _ema(data, period) {
    const k = 2/(period+1); let v = data[0].close;
    return data.map(c => (v = c.close*k + v*(1-k)));
  },

  getSignalAt(candles, index, params) {
    if (candles.length < 30) return null;
    if (params.stopLoss   > 0 && this._pnl <= -params.stopLoss)  return null;
    if (params.takeProfit > 0 && this._pnl >= params.takeProfit) return null;
    const fast = this._ema(candles, params.fastPeriod);
    const slow = this._ema(candles, params.slowPeriod);
    const i = index;
    if (!this._stake) this._stake = params.stake;
    if (fast[i] > slow[i] && fast[i-1] <= slow[i-1]) return { signal:'buy',  stake:this._stake, duration:params.duration, duration_unit:params.duration_unit, label:'EMA Cross Up' };
    if (fast[i] < slow[i] && fast[i-1] >= slow[i-1]) return { signal:'sell', stake:this._stake, duration:params.duration, duration_unit:params.duration_unit, label:'EMA Cross Down' };
    return null;
  },

  onTradeResult(result, params) {
    if (result.profit > 0) { this._wins++; this._pnl += result.profit; this._stake = params.stake; }
    else { this._losses++; this._pnl += result.profit; this._stake = Math.min((this._stake||params.stake)*params.martingale, params.maxStake); }
  },

  draw(ctx, values, pad, spacing, yFunc, params) {
    const w2Y = window.window2Bounds?.y ?? (window.mainChartHeight+10);
    const w2H = window.window2Height ?? 60;
    ctx.fillStyle = 'rgba(10,14,26,0.97)';
    ctx.fillRect(pad, w2Y, ctx.canvas.width-pad*2, w2H);
    const wr = (this._wins+this._losses) ? (this._wins/(this._wins+this._losses)*100).toFixed(1)+'%' : '--';
    ctx.fillStyle = this._pnl >= 0 ? '#22c55e' : '#ef4444';
    ctx.font = 'bold 12px Inter,sans-serif'; ctx.textAlign = 'left';
    ctx.fillText(`W:${this._wins} L:${this._losses} WR:${wr} PnL:${this._pnl.toFixed(2)}`, pad+10, w2Y+22);
    window.window2Bounds = { y: w2Y, height: w2H };
  },
})
Reference

Contract Types Reference

Use these in the contract_type field of your signal return object.

Rise / Fall
contract_typesignalDescription
CALLbuyPrice will be higher at expiry (Rise)
PUTsellPrice will be lower at expiry (Fall)
Digit contracts
contract_typebarrierWin condition
DIGITOVER"2"–"8"Last digit > barrier
DIGITUNDER"1"–"7"Last digit < barrier
DIGITMATCH"0"–"9"Last digit = barrier
DIGITDIFF"0"–"9"Last digit ≠ barrier
DIGITEVENnullLast digit is even
DIGITODDnullLast digit is odd
Duration units
duration_unitMeaningTypical range
't'Ticks1 – 10
's'Seconds15 – 3600
'm'Minutes1 – 1440
'h'Hours1 – 24
'd'Days1 – 365
Reference

Global Variables

Available inside your draw() function to position elements correctly.

Available globals
VariableTypeDescription
window.mainChartHeightnumberPixel height of the main chart area
window.window2HeightnumberPixel height of the second pane
window.window2Bounds{ y, height }Current position of the second pane. Write back at end of draw()
Always write window.window2Bounds = { y: w2Y, height: w2H } at the end of your draw() so the platform can sync the crosshair and resize handle correctly.
Reference

Working Indicators & Bots

Ready-to-use indicators and trading bots available in the indicators/ folder. Copy and paste these into your platform.

Advanced Indicators
NameTypeDescription
AETHERIndicatorQuantum Market Intelligence with pattern recognition, SMC concepts, and behavior analysis
NEXUS AIIndicatorAdvanced pattern recognition engine detecting H&S, triangles, candlestick patterns, and market behavior
Spike DetectorIndicatorMarket spike identifier for volume, price, and volatility spikes with visual markers
AI Trade SignalsIndicatorAI-powered signal generator using RSI, MA, and momentum analysis with buy/sell arrows
Trading Bots
NameStrategyDescription
NEXUS BotPattern RecognitionPattern recognition trading with candlestick patterns, trend/momentum confluence
CIPHER BotVolatility BreakoutKeltner Channel breakout + RSI divergence with squeeze filter and oscillator
ORACLE BotAdaptive RegimeBollinger Band mean reversion in ranging markets, pullback strategy in trending markets
FUSION BotMulti-IndicatorMACD + Stochastic + MA + Bollinger Band confluence with configurable parameters
MA+BB+Stoch+MACD BotTechnical AnalysisMoving averages, Bollinger Bands on main chart, Stochastic + MACD in window 2
Digit Trading Bots
NameStrategyDescription
Digit Analyzer BotHMM + MLHidden Markov Model with Baum-Welch training for Over/Under, Differs, Matches, Even/Odd
HMM Digit Differ BotHMM Differs2-state HMM (LOW/HIGH regime) for DIFFERS trading on least probable digits
NEXUS Digit BotML + UICyberpunk holographic UI with live digit river animation and ML predictions
Digit Smart BotStatisticalStatistical pattern analyzer with even/odd bias, over/under thresholds, and cold digit matching
ML Differ BotPerceptronOnline-learning perceptron that adapts tick-by-tick to predict least likely digits
Markov Differ BotMarkov ChainUses 10x10 transition matrix to find digits with lowest probability of following current digit
How to use these files
  • Indicators: Upload .js files via Chart → Indicators → Manage My Indicators
  • Trading Bots: Upload via Chart → Strategy Lab → Load tab or Digit Lab → Load Bot File
  • Bot Store: Publish your customized versions to /botstore for others to use
  • All files are located in: indicators/ folder in your project
File locations
CategoryFilesPath
Advanced Indicatorsaether.js, nexus.js, spike-detector.js, ai_trade_signals.jsindicators/
Trading Botsnexus-bot.js, cipher-bot.js, oracle-bot.js, fusion-bot.js, ma-bb-stoch-macd-bot.jsindicators/
Digit Botsdigit-analyzer-bot.js, hmm-digit-differ-bot.js, digit-smart-bot.js, ml-differ-bot.js, markov-differ-bot.jsindicators/
Test Filestest.js, test2.js, simple.htmlindicators/
Spike Detector — Copy & Paste Ready
spike-detector.jsJS
({
  name: "SPIKE DETECTOR — Market Spike Identifier",
  window: 1,
  color: "#ff6b35",
  defaultParams: {
    sensitivity: 3,
    volumeThreshold: 2.0,
    priceThreshold: 1.5,
    atrPeriod: 14,
    lookback: 20,
    showLabels: true,
  },
  calculate: function(data, params) {
    if (!data || data.length < params.lookback + params.atrPeriod) return data.map(() => null);
    const spikes = [];
    for (let i = params.lookback + params.atrPeriod; i < data.length; i++) {
      const current = data[i], prev = data[i - 1];
      const totalChange = ((current.close - prev.close) / prev.close) * 100;
      if (Math.abs(totalChange) > params.priceThreshold) {
        spikes.push({
          idx: i,
          type: totalChange > 0 ? "Price ↑" : "Price ↓",
          bias: totalChange > 0 ? "bullish" : "bearish",
          strength: Math.min(5, Math.floor(Math.abs(totalChange))),
          info: `${totalChange > 0 ? '+' : ''}${totalChange.toFixed(2)}%`,
        });
      }
    }
    this._spikes = spikes;
    return data.map(() => null);
  },
  draw: function(ctx, values, pad, spacing, yFunc, params, visibleStartIdx) {
    if (!this._spikes) return;
    const start = visibleStartIdx || 0;
    this._spikes.forEach(spike => {
      if (spike.idx < start || spike.idx >= start + values.length) return;
      const x = pad + (spike.idx - start) * spacing + spacing / 2;
      const color = spike.bias === "bullish" ? "#00ff88" : "#ff4444";
      ctx.fillStyle = color;
      ctx.font = "16px Arial";
      ctx.textAlign = "center";
      ctx.fillText(spike.bias === "bullish" ? "▲" : "▼", x, yFunc(data[spike.idx].close) + (spike.bias === "bullish" ? -10 : 20));
    });
  },
})
CIPHER Bot — Volatility Breakout
cipher-bot.jsJS
({
  name: "CIPHER BOT — Volatility Breakout",
  defaultParams: { kcPeriod: 20, kcMult: 1.5, rsiPeriod: 14, minScore: 2, stake: 1, duration: 1, duration_unit: 't' },

  getSignalAt(data, i, params) {
    if (i < 55) return null;
    const closes = data.map(c => c.close);
    const rsi = this._rsi(data, params.rsiPeriod);
    const kc = this._keltner(data, params.kcPeriod, params.kcMult);

    const price = closes[i];
    const r = rsi[i] || 50;
    let score = 0;

    if (price > kc.upper[i]) score += 2;
    if (r > 30 && r < 60) score += 1;

    if (score >= params.minScore) {
      return { signal: 'buy', stake: params.stake, duration: params.duration, duration_unit: params.duration_unit };
    }

    score = 0;
    if (price < kc.lower[i]) score += 2;
    if (r < 70 && r > 40) score += 1;

    if (score >= params.minScore) {
      return { signal: 'sell', stake: params.stake, duration: params.duration, duration_unit: params.duration_unit };
    }

    return null;
  },

  _rsi(data, period) {
    let gains = 0, losses = 0;
    const result = new Array(data.length).fill(null);
    for (let i = 1; i <= period; i++) {
      const diff = data[i].close - data[i - 1].close;
      if (diff > 0) gains += diff; else losses -= diff;
    }
    let avgGain = gains / period, avgLoss = losses / period;
    result[period] = 100 - 100 / (1 + avgGain / avgLoss);
    for (let i = period + 1; i < data.length; i++) {
      const diff = data[i].close - data[i - 1].close;
      avgGain = (avgGain * (period - 1) + Math.max(0, diff)) / period;
      avgLoss = (avgLoss * (period - 1) + Math.max(0, -diff)) / period;
      result[i] = 100 - 100 / (1 + avgGain / avgLoss);
    }
    return result;
  },

  _keltner(data, period, mult) {
    const closes = data.map(c => c.close);
    const mid = this._ema(closes, period);
    const atr = this._atr(data, period);
    return {
      mid,
      upper: mid.map((v, i) => v + mult * atr[i]),
      lower: mid.map((v, i) => v - mult * atr[i])
    };
  },

  _ema(closes, period) {
    const k = 2 / (period + 1);
    const result = [closes[0]];
    for (let i = 1; i < closes.length; i++) {
      result[i] = closes[i] * k + result[i - 1] * (1 - k);
    }
    return result;
  },

  _atr(data, period) {
    const tr = data.map((c, i) => i === 0 ? c.high - c.low :
      Math.max(c.high - c.low, Math.abs(c.high - data[i-1].close), Math.abs(c.low - data[i-1].close)));
    const result = new Array(data.length).fill(null);
    let val = tr.slice(0, period).reduce((a, b) => a + b, 0) / period;
    result[period - 1] = val;
    for (let i = period; i < data.length; i++) {
      val = (val * (period - 1) + tr[i]) / period;
      result[i] = val;
    }
    return result;
  },
})
Digit Smart Bot — Statistical Analysis
digit-smart-bot.jsJS
const CONFIG = { minBias: 0.60, streakLimit: 4, cooldownTicks: 2 };
let _cooldown = 0, _lastWon = true;

function onResult(won) {
  _lastWon = won;
  _cooldown = won ? 0 : CONFIG.cooldownTicks;
}

function digitSmartBot(ctx) {
  if (_cooldown > 0) { _cooldown--; return { trade: null, reason: 'cooldown' }; }

  const { evenPct, streak, stake, digits } = ctx;
  const oddPct = 1 - evenPct;

  if (streak >= CONFIG.streakLimit) return { trade: null, reason: 'streak too long' };

  if (evenPct >= CONFIG.minBias) {
    return { trade: 'even', stake, reason: 'even dominant ' + (evenPct * 100).toFixed(0) + '%' };
  }
  if (oddPct >= CONFIG.minBias) {
    return { trade: 'odd', stake, reason: 'odd dominant ' + (oddPct * 100).toFixed(0) + '%' };
  }

  if (digits && digits.length >= 20) {
    const recent = digits.slice(-20);
    for (let t = 1; t <= 8; t++) {
      const overPct = recent.filter(d => d > t).length / recent.length;
      if (overPct >= CONFIG.minBias) {
        return { trade: 'over', threshold: t, stake, reason: 'over ' + t + ' dominant' };
      }
    }
  }

  return { trade: null, reason: 'no edge' };
}
These are complete, working examples. Copy the code and paste directly into your platform's indicator/bot loader. Modify defaultParams to customize behavior.