
In this tutorial, we’ll walk through how to create signal bots with BUY and SELL labels in Spectra Charts. We’ll also show how to add an analytics window (HUD) to quickly test your strategy, with or without martingale “dogon” trades — it’s your choice.
First, you need to prepare the chat where you’ll describe your strategy.

Below is a single block with instructions for the model and a ready-made bot skeleton (including visual analytics and martingale parameters). You need to send this entire code block to the chat as is to initialize it.
Training prompt:
– I will give you code; you need to study all settings related to rendering statistics, analytics, and martingale “dogon” trades.
– Also study the code structure so you understand how to write bots correctly.
– Learn how to properly integrate additional filters.
– When you send me code fixes, updates, or small tweaks, always return the **full** updated code, not just fragments.
– In your reply, just tell me that you understood everything and wait for my further commands.
– In the parameters, always name settings in my language with clear, human-readable labels.
– The code itself:
// ======================== 📈 MACD Crossover Bot v1.2 (RU UI, HUD: statistics) ========================
export const meta = {
name: "MACD Crossover v1.2",
defaultParams: {
// ===== MACD =====
fastEMA: 12,
slowEMA: 26,
signalEMA: 9,
// ===== COOLDOWN (1 filter) =====
useCooldown: true,
cooldownBars: 2, // minimum number of bars between signals
cooldownMode: "По любому направлению", // По любому направлению | Только по стороне сигнала | Выкл.
// ===== APPEARANCE =====
buyText: "BUY",
sellText: "SELL",
buyColor: "#16a34a",
sellColor: "#ef4444",
buyTextColor: "#d1fae5",
sellTextColor: "#fee2e2",
// ===== HUD (stats card) =====
showHudCard: true,
hudAnchor: "TR",
hudTop: 12, hudRight: 12, hudBottom: 12, hudLeft: 12,
hudFontPx: 12, hudMaxWidth: 320, hudPadX: 12, hudPadY: 8,
hudLineGap: 4, hudRadius: 10, hudBg: "rgba(17,24,39,.72)",
hudBorder: "rgba(148,163,184,.28)", hudNeutral: "#94a3b8",
hudAlertWrThresholdPct: 80, // highlight card if WR is below this
// ===== STATS / MARTINGALE =====
useWinRate: true,
useWrBreakdownPct: true,
expiryBars: 1, // trade duration in bars
dogonBars: 0, // 0 = same as expiryBars
dogons: 0, // 0..7
tieMode: "Продолжить догоны", // Return (=): Продолжить догоны | Остановить догоны | Считать как минус
statsLookbackBars: 300 // statistics lookback window in bars
},
paramMeta: {
// ===== MACD =====
fastEMA: { label: "MACD: fast EMA", type: "number", min: 2, max: 200, step: 1 },
slowEMA: { label: "MACD: slow EMA", type: "number", min: 2, max: 400, step: 1 },
signalEMA: { label: "MACD: signal EMA", type: "number", min: 1, max: 200, step: 1 },
// ===== COOLDOWN =====
useCooldown: { label: "⏱ Cooldown: enable", type: "boolean" },
cooldownBars: { label: "Cooldown: window (bars)", type: "number", min: 0, max: 20, step: 1 },
cooldownMode: {
label: "Cooldown: mode",
type: "select",
options: [
{ label: "Any direction", value: "По любому направлению" },
{ label: "Only same side as signal", value: "Только по стороне сигнала" },
{ label: "Off", value: "Выкл." }
]
},
// ===== APPEARANCE =====
buyText: { label: "🎨 BUY label text", type: "text", maxLength: 12 },
sellText: { label: "🎨 SELL label text", type: "text", maxLength: 12 },
buyColor: { label: "🎨 BUY color", type: "color" },
sellColor: { label: "🎨 SELL color", type: "color" },
buyTextColor: { label: "🎨 BUY text color", type: "color" },
sellTextColor:{ label: "🎨 SELL text color", type: "color" },
// ===== HUD =====
showHudCard: { label: "🪪 HUD: stats card — enable", type: "boolean" },
hudAnchor: {
label: "Card position",
type: "select",
options: [
{ label: "Top-right (TR)", value: "TR" },
{ label: "Top-left (TL)", value: "TL" },
{ label: "Bottom-right (BR)", value: "BR" },
{ label: "Bottom-left (BL)", value: "BL" }
]
},
hudTop: { label: "Top offset (px)", type: "number", min: 0, max: 300, step: 1 },
hudRight: { label: "Right offset (px)", type: "number", min: 0, max: 300, step: 1 },
hudBottom: { label: "Bottom offset (px)", type: "number", min: 0, max: 300, step: 1 },
hudLeft: { label: "Left offset (px)", type: "number", min: 0, max: 300, step: 1 },
hudFontPx: { label: "Font size (px)", type: "number", min: 8, max: 20, step: 1 },
hudMaxWidth: { label: "Max width (px)", type: "number", min: 120, max: 600, step: 10 },
hudPadX: { label: "Padding X", type: "number", min: 4, max: 40, step: 1 },
hudPadY: { label: "Padding Y", type: "number", min: 2, max: 40, step: 1 },
hudLineGap: { label: "Line spacing", type: "number", min: 0, max: 20, step: 1 },
hudRadius: { label: "Corner radius", type: "number", min: 0, max: 24, step: 1 },
hudBg: { label: "Card background", type: "color" },
hudBorder: { label: "Border color", type: "color" },
hudNeutral: { label: "Neutral text color", type: "color" },
// ===== STATS / MARTINGALE =====
useWinRate: { label: "📊 Show WR %", type: "boolean" },
useWrBreakdownPct: { label: "📊 Percent by attempts", type: "boolean" },
expiryBars: { label: "📊 Expiry N (bars)", type: "number", min: 1, max: 50, step: 1 },
dogonBars: { label: "📊 Dogon bars (0 = same as N)", type: "number", min: 0, max: 50, step: 1 },
dogons: { label: "📊 Number of dogons (0..7)", type: "number", min: 0, max: 7, step: 1 },
tieMode: {
label: "📊 Return (=): behavior",
type: "select",
options: [
{ label: "Continue dogons", value: "Продолжить догоны" },
{ label: "Stop dogons", value: "Остановить догоны" },
{ label: "Count as loss", value: "Считать как минус" }
]
},
statsLookbackBars: { label: "📊 Stats window (bars)", type: "number", min: 20, max: 5000, step: 10 }
}
};
// =================== HUD Primitive (showing stats only) ===================
class MacdHudPrimitive {
constructor(series, opt) { this._series = series; this._opts = { ...opt }; this._data = {}; this._paneView = new MacdHudPaneView(series, () => this._data, () => this._opts); }
setData(d) { this._data = d || {}; }
setOptions(opt) { Object.assign(this._opts, opt || {}); }
paneViews() { return [this._paneView]; }
priceAxisViews() { return []; }
timeAxisViews() { return []; }
}
class MacdHudPaneView { constructor(series, dg, og) { this._renderer = new MacdHudRenderer(series, dg, og); } renderer() { return this._renderer; } zOrder() { return 'top'; } update() {} }
class MacdHudRenderer {
constructor(series, dg, og) { this._series = series; this._dg = dg; this._og = og; }
draw(target) {
const opts = this._og(); const data = this._dg() || {}; const lines = [];
if (data.stats) lines.push(data.stats);
if (data.stats2) lines.push(data.stats2);
if (data.stats3) lines.push(data.stats3);
if (!lines.length || !target.useBitmapCoordinateSpace) return;
target.useBitmapCoordinateSpace(scope => {
const ctx = scope.context, hr = scope.horizontalPixelRatio || 1, vr = scope.verticalPixelRatio || 1;
const cssW = scope.cssWidth || scope.bitmapSize?.width || 0;
const cssH = scope.cssHeight || (scope.bitmapSize?.height || 0) / vr;
const padX = (opts.paddingX || 12) * hr, padY = (opts.paddingY || 8) * vr, gap = (opts.lineGap || 4) * vr;
const maxW = (opts.maxWidth || 320) * hr, radius = (opts.radius ?? 10) * hr, fontPx = (opts.fontSize || 12) * vr;
ctx.save(); ctx.font = `${fontPx}px system-ui,-apple-system, Segoe UI, Roboto, Ubuntu`; ctx.textBaseline = 'top';
let w = 0, h = padY * 2 - gap;
for (const ln of lines) { const m = ctx.measureText(ln.text); w = Math.min(Math.max(w, m.width), maxW); h += Math.ceil(fontPx * 1.2) + gap; }
const anc = (opts.anchor || 'TR').toUpperCase();
let x = 0, y = 0;
if (anc.includes('T')) y = (opts.top || 12) * vr; else y = (cssH - (h) - (opts.bottom || 12) * vr);
if (anc.includes('R')) x = (cssW - (w + padX * 2) - (opts.right || 12) * hr); else x = ((opts.left || 12) * hr);
roundRect(ctx, x, y, w + padX * 2, h, radius);
ctx.fillStyle = opts.background || "rgba(17,24,39,.72)"; ctx.fill();
ctx.strokeStyle = opts.border || "rgba(148,163,184,.28)"; ctx.lineWidth = 1 * hr; ctx.stroke();
let cy = y + padY;
for (const ln of lines) { ctx.fillStyle = ln.color || opts.neutral || "#94a3b8"; ctx.fillText(ln.text, x + padX, cy, maxW); cy += Math.ceil(fontPx * 1.2) + gap; }
ctx.restore();
});
}
hitTest() { return null; }
}
function roundRect(ctx, x, y, w, h, r) {
const rr = Math.max(0, Math.min(r, w / 2, h / 2));
ctx.beginPath();
ctx.moveTo(x + rr, y);
ctx.arcTo(x + w, y, x + w, y + h, rr);
ctx.arcTo(x + w, y + h, x, y + h, rr);
ctx.arcTo(x, y + h, x, y, rr);
ctx.arcTo(x, y, x + w, y, rr);
ctx.closePath();
}
// =================== BOT CORE ===================
export function init(ctx) {
const { candleSeries, params, createSeriesMarkers } = ctx;
const p = params || meta.defaultParams;
const markersApi = createSeriesMarkers(candleSeries, []);
let hudPrimitive = candleSeries.__macdHud || null;
function ensureHudAttached() {
if (!p.showHudCard || typeof candleSeries.attachPrimitive !== 'function') {
if (hudPrimitive && typeof candleSeries.detachPrimitive === 'function') candleSeries.detachPrimitive(hudPrimitive);
candleSeries.__macdHud = hudPrimitive = null;
return null;
}
const opt = {
anchor: p.hudAnchor, top: p.hudTop, right: p.hudRight, bottom: p.hudBottom, left: p.hudLeft,
maxWidth: p.hudMaxWidth, fontSize: p.hudFontPx, paddingX: p.hudPadX, paddingY: p.hudPadY,
lineGap: p.hudLineGap, radius: p.hudRadius, background: p.hudBg, border: p.hudBorder, neutral: p.hudNeutral
};
if (!hudPrimitive) {
hudPrimitive = new MacdHudPrimitive(candleSeries, opt);
candleSeries.attachPrimitive(hudPrimitive);
candleSeries.__macdHud = hudPrimitive;
} else {
hudPrimitive.setOptions(opt);
}
return hudPrimitive;
}
// ===== helpers =====
const EPS = 1e-12;
function calcEMA(src, L) {
const n = src.length;
if (L <= 1) return src.slice();
const out = new Array(n).fill(null);
let ema = src[0];
out[0] = ema;
const k = 2 / (L + 1);
for (let i = 1; i < n; i++) {
ema = src[i] * k + ema * (1 - k);
out[i] = ema;
}
return out;
}
// Map Russian string values to canonical internal modes
function normalizeCooldown(v) {
const s = String(v || "").toLowerCase().trim();
if (["по любому направлению","по_всем_направлениям","global","любой","any"].includes(s)) return "global";
if (["только по стороне сигнала","по_стороне_сигнала","side","side-only"].includes(s)) return "side";
if (["выкл.","выкл","none","off","disabled"].includes(s)) return "none";
return "global";
}
function normalizeTie(v) {
const s = String(v || "").toLowerCase().trim();
if (["продолжить догоны","продолжить","retry"].includes(s)) return "retry";
if (["остановить догоны","остановить","stop"].includes(s)) return "stop";
if (["считать как минус","минус","asloss","as loss"].includes(s)) return "asloss";
return "retry";
}
function update(candles) {
const len = candles.length;
if (len < 2) { markersApi.setMarkers([]); return []; }
// arrays
const times = new Array(len), highs = new Array(len), lows = new Array(len), opens = new Array(len), closes = new Array(len);
for (let i = 0; i < len; i++) { const d = candles[i]; times[i] = d.time; highs[i] = d.high; lows[i] = d.low; opens[i] = d.open; closes[i] = d.close; }
// ===== MACD =====
const f = Math.max(2, Math.floor((p.fastEMA ?? 12)));
const s = Math.max(2, Math.floor((p.slowEMA ?? 26)));
const sg = Math.max(1, Math.floor((p.signalEMA ?? 9)));
const emaFast = calcEMA(closes, f);
const emaSlow = calcEMA(closes, s);
const macd = new Array(len).fill(null);
for (let i = 0; i < len; i++) macd[i] = (emaFast[i] != null && emaSlow[i] != null) ? (emaFast[i] - emaSlow[i]) : null;
// signal line (EMA of MACD)
const macdSrc = macd.map(v => v ?? 0);
const signal = calcEMA(macdSrc, sg);
// ===== signals: MACD/Signal crossovers =====
const events = [];
let lastIdx = -1, lastIdxBuy = -1, lastIdxSell = -1;
function cooldownOk(i, side) {
if (!p.useCooldown || normalizeCooldown(p.cooldownMode) === "none") return true;
const gap = Math.max(0, Math.floor(p.cooldownBars || 0));
const mode = normalizeCooldown(p.cooldownMode);
if (mode === "global") return !(lastIdx >= 0 && i - lastIdx <= gap);
if (mode === "side") {
const last = side === 'buy' ? lastIdxBuy : lastIdxSell;
return !(last >= 0 && i - last <= gap);
}
return true;
}
for (let i = 1; i < len; i++) {
if (macd[i - 1] == null || signal[i - 1] == null || macd[i] == null || signal[i] == null) continue;
const crossUp = (macd[i - 1] <= signal[i - 1]) && (macd[i] > signal[i]); // BUY
const crossDn = (macd[i - 1] >= signal[i - 1]) && (macd[i] < signal[i]); // SELL
if (crossUp && cooldownOk(i, 'buy')) {
events.push({ side: "buy", i });
lastIdx = lastIdxBuy = i;
} else if (crossDn && cooldownOk(i, 'sell')) {
events.push({ side: "sell", i });
lastIdx = lastIdxSell = i;
}
}
// ===== outcomes + stats with martingale =====
const K = Math.max(0, Math.min(7, Math.floor(p.dogons || 0)));
const N1 = Math.max(1, Math.floor(p.expiryBars || 1));
const stepN = Math.max(0, Math.floor(p.dogonBars || 0)) || N1;
const tieMode = normalizeTie(p.tieMode);
let ties = 0, losses = 0;
const winsAt = new Array(1 + K + 1).fill(0); // indices 1..(1+K)
const sigOutcome = new Map(); // iSig -> { resolved, win, attempt }
{
const Lb = Math.max(1, Math.floor(p.statsLookbackBars || 300));
const fromBar = Math.max(0, (len - 1) - Lb);
for (const ev of events) {
const iSig = ev.i; if (iSig < fromBar) continue;
const sign = ev.side === "buy" ? 1 : -1;
let attempt = 1;
let idxEntry = iSig + 1; // entry on the next bar
let finished = false;
while (!finished) {
const dur = (attempt === 1) ? N1 : stepN;
const jExit = idxEntry + dur - 1;
if (idxEntry >= len || jExit >= len) break;
const d = (closes[jExit] - opens[idxEntry]) * sign; // OPEN→CLOSE
if (Math.abs(d) < EPS) {
ties++;
if (tieMode === "retry") {
idxEntry = jExit + 1;
continue;
} else if (tieMode === "asloss") {
if (attempt <= K) {
attempt++;
idxEntry = jExit + 1;
continue;
} else {
losses++; finished = true;
sigOutcome.set(iSig, { resolved: true, win: false, attempt });
continue;
}
} else { // "stop"
break;
}
}
if (d > 0) {
winsAt[attempt]++; finished = true;
sigOutcome.set(iSig, { resolved: true, win: true, attempt });
} else {
if (attempt <= K) {
attempt++;
idxEntry = jExit + 1;
} else {
losses++; finished = true;
sigOutcome.set(iSig, { resolved: true, win: false, attempt });
}
}
}
}
}
const totalWins = winsAt.reduce((a, b) => a + b, 0);
const denom = totalWins + losses;
const wrPct = denom > 0 ? Math.round((totalWins / denom) * 100) : null;
const pctAt = winsAt.map((w, idx) => (idx === 0 ? null : (деном > 0 ? Math.round((w / деном) * 100) : null)));
const pctL = denom > 0 ? Math.round((losses / denom) * 100) : null;
// ===== markers (above signals — attempt number on which the trade won) =====
const markers = [];
for (const ev of events) {
const i = ev.i;
const side = ev.side;
const base = side === 'buy' ? p.buyText : p.sellText;
const out = sigOutcome.get(i);
let text = base;
const showAttemptNumber = (p.dogons > 0) && out && out.resolved && out.win === true && out.attempt != null;
if (showAttemptNumber) text += ` ${out.attempt}`; // attempt number that closed in profit
if (side === 'buy') {
markers.push({
name: "buy", time: times[i], position: "belowBar", shape: "arrowUp",
color: p.buyColor, text, textColor: p.buyTextColor, price: lows[i]
});
} else {
markers.push({
name: "sell", time: times[i], position: "aboveBar", shape: "arrowDown",
color: p.sellColor, text, textColor: p.sellTextColor, price: highs[i]
});
}
}
// ===== HUD stats =====
const prim = ensureHudAttached();
if (prim) {
const thr = Math.max(0, Math.min(100, p.hudAlertWrThresholdPct ?? 80));
const isAlert = (wrPct != null && wrPct < thr);
if (isAlert) prim.setOptions({ background: "rgba(220,38,38,.76)", border: "rgba(239,68,68,.75)", neutral: "#ffe4e6" });
else prim.setOptions({ background: p.hudBg, border: p.hudBorder, neutral: p.hudNeutral });
const colN = p.hudNeutral || "#94a3b8";
const colBuy = p.buyColor || "#16a34a";
const colSell = p.sellColor || "#ef4444";
const statsColor = isAlert ? "#ffe4e6"
: (totalWins > losses) ? colBuy : (losses > totalWins) ? colSell : colN;
const baseStats = `📊: +${totalWins} −${losses} =${ties}` + (p.useWinRate && wrPct != null ? ` | WR ${wrPct}%` : "");
const parts = []; for (let a = 1; a <= 1 + Math.max(0, Math.min(7, Math.floor(p.dogons || 0))); a++) parts.push(`${a}:+${winsAt[a] || 0}`);
const breakdownText = `🏆: ${parts.join(", ")}, =:${ties}, −:${losses}`;
let pctText = null;
if (p.useWrBreakdownPct) {
const pp = []; for (let a = 1; a <= 1 + Math.max(0, Math.min(7, Math.floor(p.dогons || 0))); a++) pp.push(`${a}:${pctAt[a] ?? 0}%`);
pctText = `📈: ${pp.join(", ")}, −:${pctL ?? 0}%`;
}
prim.setData({
stats: { text: baseStats, color: statsColor },
stats2: { text: breakdownText, color: statsColor },
stats3: { text: pctText, color: statsColor }
});
}
markersApi.setMarkers(markers);
return markers;
}
function destroy() {
markersApi.setMarkers([]);
if (candleSeries.__macdHud && typeof candleSeries.detachPrimitive === 'function') {
candleSeries.detachPrimitive(candleSeries.__macdHud);
}
candleSeries.__macdHud = null;
}
return { update, destroy };
}
Example of the prompt sent to the chat:

Now that the chat has been trained with a single message, move on to describing the logic of your strategy so you can get a ready-to-use tool.

| Section | Text |
|---|---|
| Good example of a description | The MACD line crosses the signal line from top to bottom, RSI < 40 → SELL. The MACD line crosses the signal line from bottom to top, RSI > 60 → BUY. |
| Bad example of a description | “The MACD line crosses somewhere above 60 RSI, then we sell, and we buy when MACD is below and RSI is above 60, something like that.” |
Open the tools menu and click Create bot.

You’ll see a window for creation, category selection, and code access permissions:


Copy the error text and send it to your AI chat. Since the code was generated by the same model, it will usually fix the problem faster and return a full updated version of the code.
You don’t need to connect anything separately. It’s enough for the code to produce BUY and SELL signals — they are the triggers for the sound. You can change the label text in the parameters; the sound will still work.
“Signals appear without sound, check why. Maybe we need to add ‘blinking’ preliminary signals while a candle is forming, not only on candle close. If you find the reason, return the full code with fixes.”
Most often this is due to free version limits: the model switches to a simplified mode and cuts off responses. The solution is to create a new chat or purchase a paid subscription for a month.
Creating a Visual Tool for Spectra Charts with Line Drawing on the Chart
Step-by-step guide on how to teach GPT to draw lines and zones in Spectra Charts and use it when trading binary options on Pocket Option.
Bot Builder in Spectra Charts — what it is and how to get started
Step-by-step guide to the Bot Builder in Spectra Charts: why bots are needed, what strategies you can build on candle data without volumes, which AI model to choose, and how to prepare.