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.
- 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
Custom Indicator
Chart page → Indicators dropdown → Manage My Indicators → Upload .js file
/app → Indicators → My IndicatorsStrategy Lab Bot
Chart page → Strategy button (top right) → Load tab → drag in your .js file → Run Backtest or go live
/app → Strategy → LoadDigit Lab Bot
Go to Digit Lab → Load Bot File button → select your .js file → Start Bot
/digit-lab → Load Bot FileBot Store
Publish your bot to the marketplace so other users can discover and use it
/botstore → UploadYou never touch the WebSocket or chart library. Just write the logic inside the functions and return the right values.
({}). No imports, no export default, no build step needed.Candle Data
Every function receives an array of candle objects sorted oldest to newest. data[data.length-1] is always the current candle.
| Field | Type | Description |
|---|---|---|
| candle.open | number | Opening price |
| candle.high | number | Highest price during the candle |
| candle.low | number | Lowest price during the candle |
| candle.close | number | Closing price — most commonly used |
| candle.time | number | Unix timestamp (seconds) of candle open |
| candle.volume | number | Volume (always 0 on Deriv synthetic indices) |
// 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));
Indicator Structure
An indicator is a JS object with a fixed set of fields. The platform reads these to know how to render it.
| Field | Type | Notes |
|---|---|---|
| name | string | REQUIRED Display name in the UI |
| color | string | OPTIONAL Default line colour e.g. #FF4500 |
| lineWidth | number | OPTIONAL Stroke width (default 2) |
| hasWindow2 | boolean | OPTIONAL true = draw in a separate pane below the chart |
| defaultParams | object | OPTIONAL User-editable parameters with defaults |
| calculate | function | REQUIRED Returns an array of values from candle data |
| draw | function | OPTIONAL Custom canvas drawing. Omit to use the default line renderer |
({
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) {
},
})
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.
| Param | Type | Description |
|---|---|---|
| data | Candle[] | Full candle array, oldest first |
| params | object | Merged defaultParams + user overrides |
Returns: number[] — one value per candle. Return null at index i if the indicator is not ready yet.
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;
},
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.draw(ctx, values, pad, spacing, yFunc, params)
Optional custom canvas renderer. Use the HTML5 Canvas 2D API directly.
| Param | Type | Description |
|---|---|---|
| ctx | CanvasRenderingContext2D | Draw directly on this |
| values | number[] | Array returned by your calculate() |
| pad | number | Left pixel offset where the chart area starts |
| spacing | number | Pixels per candle (bar width) |
| yFunc | function(price) → y | Converts a price to a canvas Y coordinate |
| params | object | User params (same as in calculate) |
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();
},
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.
| Variable | Type | Description |
|---|---|---|
| window.window2Bounds | { y, height } | Top Y and height of the pane in pixels |
| window.window2Height | number | Height of the second pane in pixels |
| window.mainChartHeight | number | Height of the main chart in pixels |
At the end of your draw(), always write back: window.window2Bounds = { y: w2Y, height: w2H }
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 };
},
Full Indicator Example — RSI
A complete working RSI indicator with a custom draw function that renders in the second pane with overbought/oversold lines.
({
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 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.
| Field | Type | Notes |
|---|---|---|
| name | string | REQUIRED Bot display name |
| description | string | OPTIONAL Short description shown in the UI |
| defaultParams | object | OPTIONAL Editable params: stake, duration, SL, TP, etc. |
| calculate | function | OPTIONAL Pre-compute indicators once per candle batch |
| getSignalAt | function | REQUIRED Returns a signal object or null |
| onTradeResult | function | OPTIONAL Called when a trade settles |
| draw | function | OPTIONAL Render a stats panel in window 2 |
| hasWindow2 | boolean | OPTIONAL Request a second pane for your draw() |
({
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 */ },
})
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.
| Param | Type | Description |
|---|---|---|
| candles | Candle[] | Full candle history up to and including index |
| index | number | Current candle index (usually candles.length - 1) |
| params | object | Merged defaultParams + user overrides |
| Field | Type | Description |
|---|---|---|
| signal | 'buy'|'sell' | REQUIRED 'buy' = Rise/Call/Over, 'sell' = Fall/Put/Under |
| contract_type | string | OPTIONAL Override e.g. DIGITOVER, CALL |
| barrier | string | OPTIONAL Barrier for digit contracts e.g. '5' |
| stake | number | OPTIONAL Override stake for this trade |
| duration | number | OPTIONAL Override duration |
| duration_unit | string | OPTIONAL 't' 's' 'm' 'h' 'd' |
| confidence | number | OPTIONAL 0–100, shown in the UI |
| label | string | OPTIONAL Short label e.g. 'Over 5' |
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;
},
onTradeResult(result, params)
Called automatically after each trade settles. Use this to implement martingale, track stats, or adjust your next stake.
| Field | Type | Description |
|---|---|---|
| result.profit | number | Profit/loss in USD. Positive = win, negative = loss |
| result.contract_id | string | Deriv contract ID |
| result.status | string | 'won' or 'lost' |
_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
);
}
},
defaultParams
All fields in defaultParams are exposed as editable inputs in the Strategy Lab UI. Users can change them without touching your code.
| Key | Default | Description |
|---|---|---|
| stake | 1 | Base stake per trade in USD |
| duration | 1 | Contract duration number |
| duration_unit | 't' | 't'=ticks, 's'=seconds, 'm'=minutes, 'h'=hours, 'd'=days |
| stopLoss | 0 | Stop bot when total PnL drops below this (0 = off) |
| takeProfit | 0 | Stop bot when total PnL reaches this (0 = off) |
| martingale | 1 | Stake multiplier after a loss (1 = no martingale) |
| maxStake | 50 | Maximum stake cap when using martingale |
| cooldown | 0 | Candles to skip after a loss before trading again |
| minConfidence | 60 | Minimum confidence score to fire a signal |
Digit Contract Bots
Digit contracts trade on the last digit of the closing price. Return a specific contract_type and barrier from getSignalAt().
| contract_type | barrier | Win 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 |
| DIGITEVEN | null | Last digit is even (0,2,4,6,8) |
| DIGITODD | null | Last digit is odd (1,3,5,7,9) |
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;
},
Full Bot Example — EMA Crossover
A complete Rise/Fall bot with martingale, stop-loss, and a stats panel in window 2.
({
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 };
},
})
Contract Types Reference
Use these in the contract_type field of your signal return object.
| contract_type | signal | Description |
|---|---|---|
| CALL | buy | Price will be higher at expiry (Rise) |
| PUT | sell | Price will be lower at expiry (Fall) |
| contract_type | barrier | Win 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 |
| DIGITEVEN | null | Last digit is even |
| DIGITODD | null | Last digit is odd |
| duration_unit | Meaning | Typical range |
|---|---|---|
| 't' | Ticks | 1 – 10 |
| 's' | Seconds | 15 – 3600 |
| 'm' | Minutes | 1 – 1440 |
| 'h' | Hours | 1 – 24 |
| 'd' | Days | 1 – 365 |
Global Variables
Available inside your draw() function to position elements correctly.
| Variable | Type | Description |
|---|---|---|
| window.mainChartHeight | number | Pixel height of the main chart area |
| window.window2Height | number | Pixel height of the second pane |
| window.window2Bounds | { y, height } | Current position of the second pane. Write back at end of draw() |
window.window2Bounds = { y: w2Y, height: w2H } at the end of your draw() so the platform can sync the crosshair and resize handle correctly.Working Indicators & Bots
Ready-to-use indicators and trading bots available in the indicators/ folder. Copy and paste these into your platform.
| Name | Type | Description |
|---|---|---|
| AETHER | Indicator | Quantum Market Intelligence with pattern recognition, SMC concepts, and behavior analysis |
| NEXUS AI | Indicator | Advanced pattern recognition engine detecting H&S, triangles, candlestick patterns, and market behavior |
| Spike Detector | Indicator | Market spike identifier for volume, price, and volatility spikes with visual markers |
| AI Trade Signals | Indicator | AI-powered signal generator using RSI, MA, and momentum analysis with buy/sell arrows |
| Name | Strategy | Description |
|---|---|---|
| NEXUS Bot | Pattern Recognition | Pattern recognition trading with candlestick patterns, trend/momentum confluence |
| CIPHER Bot | Volatility Breakout | Keltner Channel breakout + RSI divergence with squeeze filter and oscillator |
| ORACLE Bot | Adaptive Regime | Bollinger Band mean reversion in ranging markets, pullback strategy in trending markets |
| FUSION Bot | Multi-Indicator | MACD + Stochastic + MA + Bollinger Band confluence with configurable parameters |
| MA+BB+Stoch+MACD Bot | Technical Analysis | Moving averages, Bollinger Bands on main chart, Stochastic + MACD in window 2 |
| Name | Strategy | Description |
|---|---|---|
| Digit Analyzer Bot | HMM + ML | Hidden Markov Model with Baum-Welch training for Over/Under, Differs, Matches, Even/Odd |
| HMM Digit Differ Bot | HMM Differs | 2-state HMM (LOW/HIGH regime) for DIFFERS trading on least probable digits |
| NEXUS Digit Bot | ML + UI | Cyberpunk holographic UI with live digit river animation and ML predictions |
| Digit Smart Bot | Statistical | Statistical pattern analyzer with even/odd bias, over/under thresholds, and cold digit matching |
| ML Differ Bot | Perceptron | Online-learning perceptron that adapts tick-by-tick to predict least likely digits |
| Markov Differ Bot | Markov Chain | Uses 10x10 transition matrix to find digits with lowest probability of following current digit |
- 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
/botstorefor others to use - All files are located in:
indicators/folder in your project
| Category | Files | Path |
|---|---|---|
| Advanced Indicators | aether.js, nexus.js, spike-detector.js, ai_trade_signals.js | indicators/ |
| Trading Bots | nexus-bot.js, cipher-bot.js, oracle-bot.js, fusion-bot.js, ma-bb-stoch-macd-bot.js | indicators/ |
| Digit Bots | digit-analyzer-bot.js, hmm-digit-differ-bot.js, digit-smart-bot.js, ml-differ-bot.js, markov-differ-bot.js | indicators/ |
| Test Files | test.js, test2.js, simple.html | indicators/ |
({
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));
});
},
})
({
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;
},
})
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' };
}
defaultParams to customize behavior.