
В этом обучающем посте разберём, как создавать сигнальные боты с метками BUY и SELL в Spectra Charts. Дополнительно покажем, как встроить окно аналитики (HUD) для быстрого тестирования вашей стратегии с учётом догонов по мартингейлу или без них — по вашему выбору.
Сначала нужно обучить чат, в котором вы будете писать стратегию.

Ниже — единый блок с инструкциями для модели и готовым каркасом бота (включая визуальную аналитику и параметры догонов). Этот код-блок целиком нужно отправить в чат для инициализации.
Обучающий промпт:
– Я тебе даю код, тебе нужно изучить все настройки которые связаны с отрисовкой статистики, аналитики и догонов,
– Так же изучи структуру кода для правильного написания ботов,
– Изучи как правильно встраивать доп фильтры,
– Когда будешь писать мне исправления кода, обновления, вносить мелкие правки, всегда отдавай готовый код полностью,
– В ответ скажи мне что все понял и жди от меня дальнейших команд,
– Настройки в параметрах всегда прописывай на моем языке понятными словами
– Сам код:
// ======================== 📈 MACD Crossover Bot v1.2 (RU UI, HUD: статистика) ========================
export const meta = {
name: "MACD Crossover v1.2",
defaultParams: {
// ===== MACD =====
fastEMA: 12,
slowEMA: 26,
signalEMA: 9,
// ===== КУЛДАУН (1 фильтр) =====
useCooldown: true,
cooldownBars: 2, // минимум баров между сигналами
cooldownMode: "По любому направлению", // По любому направлению | Только по стороне сигнала | Выкл.
// ===== ВИД =====
buyText: "BUY",
sellText: "SELL",
buyColor: "#16a34a",
sellColor: "#ef4444",
buyTextColor: "#d1fae5",
sellTextColor: "#fee2e2",
// ===== HUD (карточка статистики) =====
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, // подсветка карточки, если WR ниже
// ===== СТАТИСТИКА / ДОГОНЫ =====
useWinRate: true,
useWrBreakdownPct: true,
expiryBars: 1, // длительность сделки в барах
dogonBars: 0, // 0 = как expiryBars
dogons: 0, // 0..7
tieMode: "Продолжить догоны", // Возврат (=): Продолжить догоны | Остановить догоны | Считать как минус
statsLookbackBars: 300 // окно статистики по барам
},
paramMeta: {
// ===== MACD =====
fastEMA: { label: "MACD: быстрая EMA", type: "number", min: 2, max: 200, step: 1 },
slowEMA: { label: "MACD: медленная EMA", type: "number", min: 2, max: 400, step: 1 },
signalEMA: { label: "MACD: EMA сигнала", type: "number", min: 1, max: 200, step: 1 },
// ===== КУЛДАУН =====
useCooldown: { label: "⏱ Кулдаун: включить", type: "boolean" },
cooldownBars: { label: "Кулдаун: окно (баров)", type: "number", min: 0, max: 20, step: 1 },
cooldownMode: {
label: "Кулдаун: режим",
type: "select",
options: [
{ label: "По любому направлению", value: "По любому направлению" },
{ label: "Только по стороне сигнала", value: "Только по стороне сигнала" },
{ label: "Выкл.", value: "Выкл." }
]
},
// ===== ВИД =====
buyText: { label: "🎨 Подпись BUY", type: "text", maxLength: 12 },
sellText: { label: "🎨 Подпись SELL", type: "text", maxLength: 12 },
buyColor: { label: "🎨 Цвет BUY", type: "color" },
sellColor: { label: "🎨 Цвет SELL", type: "color" },
buyTextColor: { label: "🎨 Цвет текста BUY", type: "color" },
sellTextColor:{ label: "🎨 Цвет текста SELL", type: "color" },
// ===== HUD =====
showHudCard: { label: "🪪 HUD: карточка — включить", type: "boolean" },
hudAnchor: {
label: "Позиция карточки",
type: "select",
options: [
{ label: "Верх-право (TR)", value: "TR" },
{ label: "Верх-лево (TL)", value: "TL" },
{ label: "Низ-право (BR)", value: "BR" },
{ label: "Низ-лево (BL)", value: "BL" }
]
},
hudTop: { label: "Отступ top (px)", type: "number", min: 0, max: 300, step: 1 },
hudRight: { label: "Отступ right (px)", type: "number", min: 0, max: 300, step: 1 },
hudBottom: { label: "Отступ bottom (px)", type: "number", min: 0, max: 300, step: 1 },
hudLeft: { label: "Отступ left (px)", type: "number", min: 0, max: 300, step: 1 },
hudFontPx: { label: "Шрифт (px)", type: "number", min: 8, max: 20, step: 1 },
hudMaxWidth: { label: "Макс. ширина (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: "Межстрочный", type: "number", min: 0, max: 20, step: 1 },
hudRadius: { label: "Радиус", type: "number", min: 0, max: 24, step: 1 },
hudBg: { label: "Фон карточки", type: "color" },
hudBorder: { label: "Рамка", type: "color" },
hudNeutral: { label: "Цвет нейтр.", type: "color" },
// ===== СТАТИСТИКА / ДОГОНЫ =====
useWinRate: { label: "📊 Показать WR %", type: "boolean" },
useWrBreakdownPct: { label: "📊 Проценты по фазам", type: "boolean" },
expiryBars: { label: "📊 Экспирация N (баров)", type: "number", min: 1, max: 50, step: 1 },
dogonBars: { label: "📊 Догон, баров (0 = как N)", type: "number", min: 0, max: 50, step: 1 },
dogons: { label: "📊 Кол-во догонов (0..7)", type: "number", min: 0, max: 7, step: 1 },
tieMode: {
label: "📊 Возврат (=): поведение",
type: "select",
options: [
{ label: "Продолжить догоны", value: "Продолжить догоны" },
{ label: "Остановить догоны", value: "Остановить догоны" },
{ label: "Считать как минус", value: "Считать как минус" }
]
},
statsLookbackBars: { label: "📊 Окно статистики (баров)", type: "number", min: 20, max: 5000, step: 10 }
}
};
// =================== HUD Primitive (показываем только статистику) ===================
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;
}
// Приведение русских значений к каноническим
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);
// ===== сигналы: пересечения MACD/Signal =====
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;
}
}
// ===== исходы + статистика с догонами =====
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); // индексы 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; // вход со следующей свечи
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;
// ===== маркеры (над сигналами — цифра попытки, на которой победили) =====
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}`; // ✅ цифра — с какого догона выиграли
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 статистика =====
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 };
}
Пример отправленного промпта в чат:

Теперь, когда чат обучен одним сообщением, переходите к описанию логики вашей стратегии, чтобы получить готовый инструмент.

| Раздел | Текст |
|---|---|
| Пример удачной формулировки | Линия MACD пересекает сигнальную линию сверху вниз, RSI < 40 → SELL. Линия MACD пересекает сигнальную линию снизу вверх, RSI > 60 → BUY. |
| Пример неудачной формулировки | «Линия макди пересекается выше 60 рси, тогда продаем и покупаем когда наоборот макди внизу и рси выше 60». |
Перейдите в меню инструментов и нажмите Создать бота.

Откроется окно создания, выбора категории и прав доступа к коду:


Скопируйте текст ошибки и отправьте его в ваш чат с нейросетью. Поскольку код генерировала она же, модель быстрее подскажет корректное исправление и вернёт полный обновлённый вариант.
Ничего отдельно подключать не нужно. Достаточно, чтобы в коде присутствовали сигналы BUY и SELL — они служат триггером звука. Текстовые подписи можно менять в параметрах, звук от этого не исчезнет.
«Сигналы появляются без звука, проверь почему так. Возможно, нужно сделать “мигающие” сигналы при формировании, а не только по закрытию свечи. Если нашёл причину — верни полный код с исправлениями».
Чаще всего это из-за лимитов бесплатной версии: модель переключается в упрощённый режим и обрывает ответы. Решение — создать новый чат или оформить платную подписку на месяц.
Создаем визуальный инструмент для Spectra Charts с отрисовкой линий поверх графика
Пошаговое руководство, как обучить GPT рисовать линии и зоны в Spectra Charts и использовать это при торговле бинарными опционами на Pocket Option.
Конструктор ботов в Spectra Charts — что это и с чего начать
Пошаговый гид по конструктору ботов в Spectra Charts: зачем нужны боты, какие стратегии можно делать на свечных данных без объёмов, какую нейросеть выбрать и как подготовиться к работе.