Спектра Чартс

Создаем визуальный инструмент для Spectra Charts с отрисовкой линий поверх графика

Пошаговое руководство, как обучить GPT рисовать линии и зоны в Spectra Charts и использовать это при торговле бинарными опционами на Pocket Option.

Создаем визуальный инструмент для Spectra Charts с отрисовкой линий поверх графика

Обложка визуального построения поверх графиков

В предыдущем обучающем посте мы уже разобрали, как создавать сигнальные торговые боты. В этом материале сделаем следующий шаг и рассмотрим, как настроить визуальный инструмент для Spectra Charts, который будет рисовать любые прямые и наклонные линии поверх графика.

Такой инструмент особенно полезен тем, кто торгует на бинарных опционах и платформе Pocket Option, но не хочет вручную чертить каждый уровень и трендовую линию. Наша задача — обучить нейросеть помогать вам в этой рутинной работе и создавать наглядные свечные графики с линиями и зонами, которые упрощают анализ.

По мере того как вы будете осваивать создание разных видов инструментов, вы сможете обучать нейросеть:

  • создавать сигнальные боты с визуальными стрелками BUY и SELL;
  • рисовать горизонтальные уровни и зоны поддержки и сопротивления;
  • строить наклонные линии тренда;
  • визуализировать любые индикаторы, которые рисуются поверх цены.

Все зависит от вашего опыта, желаний и воображения. Главное — правильно подготовить промпты и каркасы кода.

Вся идея проста. Вы один раз обучаете GPT на базовых примерах, а дальше просите его создавать под ваши задачи новые визуальные инструменты для Spectra Charts и торговли на Pocket Option.

С чего начать

Аналогично созданию сигнальных ботов, начинать нужно с обучения чата. Только в этой статье мы делаем акцент не на логике сделок, а на рисовании поверх графика.

Важные условия подготовки

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

  1. Используйте полностью чистый чат
    Начинайте с нового диалога без прошлой переписки. Старые инструкции могут вмешаться в процесс и испортить результат.
    Чистый чат = предсказуемое обучение.
  2. Рекомендуем платную версию модели
    Подойдет базовый тариф, главное чтобы:
    • лимит сообщений был выше;
    • ответы не обрывались на середине;
    • большие куски кода проходили без сокращения.
  3. Отправляйте обучающие блоки целиком
    Любое сокращение, пропуск комментариев или строчек кода может привести к тому, что нейросеть перестанет корректно рисовать линии и зоны.
Если вы случайно обрезали код или промпт, лучше сразу очистить чат и начать обучение заново, чем пытаться «доправить» уже наполовину сломанный контекст.

1. Обучение GPT: промпт + каркас бота

Обучающий промпт для первого кода

Текст промпта, который вы даёте GPT перед вставкой первого кода:

- Я тебе даю код, тебе нужно изучить его и понять, как правильно рисовать поверх графика линии и делать заливки между линиями,
- Так же изучи структуру кода для правильного написания индикаторов и ботов,
- Когда будешь писать мне исправления кода, обновления, вносить мелкие правки, всегда отдавай готовый код полностью,
- В ответ скажи мне что все понял и жди от меня дальнейших команд,
- Настройки в параметрах всегда прописывай на моем языке понятными словами  

Сам код:  

// ======================== 📊 Trend Signals — EMA Ribbon (price-cross recolor + clean band fill + MACD/ADX arrows) ========================
export const meta = {
  name: "Trend Signals v1.2",
  defaultParams: {
    // ===== EMA =====
    maShortPeriod: 20,
    maLongPeriod: 50,

    // Меняем цвет ТОЛЬКО при реальном кроссе и удерживаем до следующего
    minFlipBars: 1,            // анти-дребезг: минимум баров между переключениями цвета

    // ===== Заливка между EMA =====
    showBand: true,
    bandFillColor: "#6b7280",  // серый fill (из исходника)
    bandOpacity: 0.35,

    // ===== Цвета EMA (обе EMA — одинаковые) =====
    shortUpColor:  "#22c55e",
    shortDownColor:"#ef4444",
    longUpColor:   "#22c55e",
    longDownColor: "#ef4444",
    lineWidthShort: 2,
    lineWidthLong:  3,

    // ===== MACD (для стрелок) =====
    fastLength: 12,
    slowLength: 26,
    signalLength: 9,

    // ===== ADX/DI (фильтр силы) =====
    diLen: 14,
    adxLen: 14,
    adxThreshold: 25,

    // ===== Маркеры =====
    showBuySell: false,        // включи true, если хочешь видеть стрелки по умолчанию
    buyText: "BUY",
    sellText:"SELL",
    buyColor:"#16a34a",
    sellColor:"#ef4444",
    buyTextColor:"#111827",
    sellTextColor:"#111827",
  },
  paramMeta: {
    maShortPeriod:  { label:"Короткая EMA", type:"number", min:2, max:200 },
    maLongPeriod:   { label:"Длинная EMA",  type:"number", min:5, max:400 },
    minFlipBars:    { label:"Мин. баров между перекрасами", type:"number", min:0, max:10 },

    showBand:       { label:"Показывать заливку", type:"boolean" },
    bandFillColor:  { label:"Цвет заливки", type:"color" },
    bandOpacity:    { label:"Прозрачность заливки", type:"number", min:0, max:1, step:0.05 },

    shortUpColor:   { label:"Короткая: цена выше", type:"color" },
    shortDownColor: { label:"Короткая: цена ниже",  type:"color" },
    longUpColor:    { label:"Длинная: цена выше",  type:"color" },
    longDownColor:  { label:"Длинная: цена ниже",  type:"color" },
    lineWidthShort: { label:"Толщина короткой", type:"number", min:1, max:6 },
    lineWidthLong:  { label:"Толщина длинной", type:"number", min:1, max:6 },

    fastLength:     { label:"MACD быстрая", type:"number", min:2, max:100 },
    slowLength:     { label:"MACD медленная", type:"number", min:5, max:300 },
    signalLength:   { label:"MACD сигнал", type:"number", min:2, max:100 },

    diLen:          { label:"DI период", type:"number", min:2, max:100 },
    adxLen:         { label:"ADX период", type:"number", min:2, max:100 },
    adxThreshold:   { label:"Порог ADX", type:"number", min:5, max:60 },
    showBuySell:    { label:"Показывать BUY/SELL", type:"boolean" },

    buyText:        { label:"Текст BUY", type:"text" },
    sellText:       { label:"Текст SELL", type:"text" },
    buyColor:       { label:"Цвет BUY", type:"color" },
    sellColor:      { label:"Цвет SELL", type:"color" },
    buyTextColor:   { label:"Цвет текста BUY", type:"color" },
    sellTextColor:  { label:"Цвет текста SELL", type:"color" },
  }
};

export function init(ctx) {
  const { chart, params, LineSeries, candleSeries, createSeriesMarkers } = ctx;

  // ===== математика =====
  function ema(src, len){
    const n=src.length; if(!n) return [];
    const out=new Array(n).fill(null); const a=2/(len+1);
    out[0]=src[0]; for(let i=1;i<n;i++) out[i]=a*src[i]+(1-a)*out[i-1];
    return out;
  }
  function rma(src, len){
    const n=src.length; if(!n) return [];
    const out=new Array(n).fill(null);
    out[0]=src[0]; for(let i=1;i<n;i++) out[i]=(out[i-1]*(len-1)+src[i])/len;
    return out;
  }
  function hexToRgba(hex, a){
    const h = hex.replace("#",""), n = parseInt(h.length===3?h.split("").map(c=>c+c).join(""):h,16);
    const r=(n>>16)&255, g=(n>>8)&255, b=n&255; return `rgba(${r},${g},${b},${a})`;
  }

  // устойчивое состояние «цена над/под EMA» — переключаем ТОЛЬКО при реальном кроссе
  function crossState(close, ema, minFlipBars){
    const n=close.length, state=new Array(n).fill(null);
    let i0=0; while(i0<n && !(Number.isFinite(close[i0]) && Number.isFinite(ema[i0]))) i0++;
    if (i0>=n) return state;
    state[i0] = close[i0] >= ema[i0];
    let lastFlip=i0;
    for (let i=i0+1;i<n;i++){
      if (!Number.isFinite(close[i]) || !Number.isFinite(ema[i])) { state[i]=state[i-1]; continue; }
      const prev=close[i-1]-ema[i-1], curr=close[i]-ema[i];
      let s=state[i-1];
      const crossUp   = prev <= 0 && curr > 0;
      const crossDown = prev >= 0 && curr < 0;
      if (crossUp   && (i-lastFlip)>=minFlipBars){ s=true;  lastFlip=i; }
      if (crossDown && (i-lastFlip)>=minFlipBars){ s=false; lastFlip=i; }
      state[i]=s;
    }
    return state;
  }

  // сегментация линий (каждый участок цвета — отдельная серия)
  function segmentsFromState(times, values, stateBool){
    const upSegs=[], downSegs=[];
    let curr=null;
    for (let i=0;i<values.length;i++){
      const v = values[i], st = stateBool[i];
      if (!Number.isFinite(v) || st==null){ curr=null; continue; }
      if (!curr || curr.type!==st){
        curr = { arr: [], type: st };
        (st ? upSegs : downSegs).push(curr.arr);
        if (i>0 && Number.isFinite(values[i-1]) && stateBool[i-1]!=null && stateBool[i-1]!==st){
          curr.arr.push({ time: times[i-1], value: values[i-1] });
        }
      }
      curr.arr.push({ time: times[i], value: v });
    }
    return { upSegs, downSegs };
  }

  // менеджер серий-сегментов
  const segSeries = { shortUp:[], shortDown:[], longUp:[], longDown:[] };
  function ensureSegSeries(bucket, idx, color, width){
    let s = bucket[idx];
    if (!s){
      s = chart.addSeries(LineSeries, {
        color,
        lineWidth: Math.max(1, width|0 || 2),
        lastValueVisible:false,
        priceLineVisible:false,
        crosshairMarkerVisible:false,
      });
      bucket[idx] = s;
    } else {
      s.applyOptions?.({ color, lineWidth: Math.max(1, width|0 || 2) });
    }
    return s;
  }
  function syncSegments(key, color, width, segs){
    const bucket = segSeries[key];
    for (let i=0;i<segs.length;i++){
      const s = ensureSegSeries(bucket, i, color, width);
      s.setData(segs[i]);
    }
    for (let i=segs.length; i<bucket.length; i++){
      const s=bucket[i]; if (s) chart.removeSeries(s);
    }
    bucket.length = segs.length;
  }
  function clearAllSegs(){
    for (const key of Object.keys(segSeries)){
      for (const s of segSeries[key]) if (s) chart.removeSeries(s);
      segSeries[key] = [];
    }
  }

  // ===== заливка между EMA (полигон без диагоналей, разбитый на участки между пересечениями EMA) =====
  class BandPrimitive {
    constructor(o={}){ this._v = new BandView(o); }
    attached(p){ this._v.attached(p); }
    detached(){ this._v.detached(); }
    paneViews(){ return [this._v]; }
    updateAllViews(){ this._v.update(); }
    setOptions(o){ this._v.setOptions(o); }
    setBand(times, upper, lower, whichUpper){ this._v.setBand(times, upper, lower, whichUpper); }
  }
  class BandView {
    constructor(o){
      this._s=null; this._c=null; this._req=null;
      this._t=[]; this._u=[]; this._l=[]; this._w=[];
      this._o={ fill:"#6b7280", alpha:0.35, ...o };
    }
    attached({series,chart,requestUpdate}){ this._s=series; this._c=chart; this._req=requestUpdate; }
    detached(){ this._s=this._c=this._req=null; }
    update(){}
    zOrder(){ return "bottom"; }
    setOptions(o){ Object.assign(this._o,o); this._req?.(); }
    setBand(t,u,l,w){ this._t=t||[]; this._u=u||[]; this._l=l||[]; this._w=w||[]; this._req?.(); }
    renderer(){
      const self=this;
      const drawImpl=(target)=>{
        const s=self._s,c=self._c; if(!s||!c) return;
        const ts=c.timeScale(); if(!ts) return;
        const o=self._o;
        const draw=(ctx,prX=1,prY=1)=>{
          ctx.save(); ctx.scale(prX,prY);
          ctx.globalAlpha=Math.max(0,Math.min(1,o.alpha));
          ctx.fillStyle=o.fill;

          const N=self._t.length;
          let segU=[], segL=[], prev=null;
          function flush(){
            if (segU.length<2){ segU=[]; segL=[]; return; }
            ctx.beginPath();
            ctx.moveTo(segU[0].x, segU[0].y);
            for (let i=1;i<segU.length;i++) ctx.lineTo(segU[i].x, segU[i].y);
            for (let i=segL.length-1;i>=0;i--) ctx.lineTo(segL[i].x, segL[i].y);
            ctx.closePath(); ctx.fill();
            segU=[]; segL=[];
          }
          for (let i=0;i<N;i++){
            const t=self._t[i], uV=self._u[i], lV=self._l[i], w=self._w[i];
            if (!Number.isFinite(uV)||!Number.isFinite(lV)||w==null){ flush(); prev=null; continue; }
            const x=ts.timeToCoordinate(t), yU=s.priceToCoordinate(uV), yL=s.priceToCoordinate(lV);
            if (x==null||yU==null||yL==null){ flush(); prev=null; continue; }
            if (prev===null) prev=w;
            if (w!==prev){ flush(); prev=w; }
            segU.push({x, y:yU});
            segL.push({x, y:yL});
          }
          flush();
          ctx.restore();
        };
        if (typeof target.useBitmapCoordinateSpace==="function"){
          target.useBitmapCoordinateSpace(sc=>draw(sc.context, sc.horizontalPixelRatio, sc.verticalPixelRatio));
        } else {
          draw(target.context, target.pixelRatio||1, target.pixelRatio||1);
        }
      };
      return { drawBackground: drawImpl, draw: drawImpl };
    }
  }

  let bandPrim = new BandPrimitive({
    fill: hexToRgba(params.bandFillColor, params.bandOpacity ?? 0.35),
    alpha: params.bandOpacity ?? 0.35
  });
  if (typeof candleSeries.attachPrimitive === "function") {
    candleSeries.attachPrimitive(bandPrim);
  } else if (ctx.primitivesApi?.attachPrimitive) {
    ctx.primitivesApi.attachPrimitive(candleSeries, bandPrim);
  } else {
    bandPrim = null; // нет primitives — без заливки
  }

  // ===== контроллер маркеров: пытаемся через оба API =====
  const markerCtrl = (() => {
    let api = null;
    try {
      api = (typeof createSeriesMarkers === "function")
        ? createSeriesMarkers(candleSeries, [])
        : null;
    } catch(_) {}
    return {
      set(list){ try { api?.setMarkers?.(list); } catch(_) {} try { candleSeries.setMarkers?.(list); } catch(_) {} },
      clear(){  try { api?.setMarkers?.([]);   } catch(_) {} try { candleSeries.setMarkers?.([]);   } catch(_) {} },
    };
  })();

  // быстрый хук на смену параметров (если рантайм его вызывает)
  function onParamsChanged(next){
    if (!next.showBuySell) markerCtrl.clear(); // мгновенно скрыть стрелки
  }

  function update(candles){
    const N=candles?.length||0;
    if (N<5){
      clearAllSegs();
      bandPrim?.setBand([],[],[],[]);
      if (!params.showBuySell) markerCtrl.clear();
      return [];
    }

    const times=candles.map(c=>c.time);
    const high =candles.map(c=>c.high);
    const low  =candles.map(c=>c.low);
    const close=candles.map(c=>c.close);

    // ===== EMA
    const sLen=Math.max(2, params.maShortPeriod|0 || 20);
    const lLen=Math.max(3, params.maLongPeriod|0 || 50);
    const emaS=ema(close, sLen);
    const emaL=ema(close, lLen);

    // ===== устойчивые состояния (перекраска только при кроссах)
    const minFlip = Math.max(0, params.minFlipBars|0 || 1);
    const stateS = crossState(close, emaS, minFlip);
    const stateL = crossState(close, emaL, minFlip);

    // ===== сегменты короткой/длинной EMA
    const segS = segmentsFromState(times, emaS, stateS);
    const segL = segmentsFromState(times, emaL, stateL);

    syncSegments("shortUp",   params.shortUpColor,   params.lineWidthShort, segS.upSegs);
    syncSegments("shortDown", params.shortDownColor, params.lineWidthShort, segS.downSegs);
    syncSegments("longUp",    params.longUpColor,    params.lineWidthLong,  segL.upSegs);
    syncSegments("longDown",  params.longDownColor,  params.lineWidthLong,  segL.downSegs);

    // ===== заливка между EMA
    if (bandPrim && params.showBand){
      const upper = emaS.map((v,i)=> Math.max(v ?? NaN, emaL[i] ?? NaN));
      const lower = emaS.map((v,i)=> Math.min(v ?? NaN, emaL[i] ?? NaN));
      const whichUpper = emaS.map((v,i)=> (Number.isFinite(v) && Number.isFinite(emaL[i])) ? v>=emaL[i] : null);
      bandPrim.setOptions({
        fill:  hexToRgba(params.bandFillColor, params.bandOpacity ?? 0.35),
        alpha: params.bandOpacity ?? 0.35
      });
      bandPrim.setBand(times, upper, lower, whichUpper);
    } else if (bandPrim){
      bandPrim.setBand([],[],[],[]);
    }

    // ===== стрелки BUY/SELL (MACD + ADX)
    if (params.showBuySell){
      const fastMA=ema(close, Math.max(2, params.fastLength|0 || 12));
      const slowMA=ema(close, Math.max(3, params.slowLength|0 || 26));
      const macd  =fastMA.map((v,i)=> (v ?? 0) - (slowMA[i] ?? 0));
      const signal=ema(macd, Math.max(2, params.signalLength|0 || 9));

      const tr=new Array(N).fill(0);
      for (let i=1;i<N;i++){
        const v1=high[i]-low[i], v2=Math.abs(high[i]-close[i-1]), v3=Math.abs(low[i]-close[i-1]);
        tr[i]=Math.max(v1,v2,v3);
      }
      const plusDM=new Array(N).fill(0), minusDM=new Array(N).fill(0);
      for (let i=1;i<N;i++){
        const d=close[i]-close[i-1];
        plusDM[i]=(d>0 && d>-d)? d:0;
        minusDM[i]=(-d>0 && -d>d)? -d:0;
      }
      const diLen = Math.max(2, params.diLen|0 || 14);
      const adxLen= Math.max(2, params.adxLen|0 || 14);
      const atrR  = rma(tr, diLen);
      const plusR = rma(plusDM, diLen);
      const minusR= rma(minusDM, diLen);
      const plusDI = plusR.map((v,i)=> 100 * (v ?? 0) / ((atrR[i] ?? 0) || 1));
      const minusDI= minusR.map((v,i)=> 100 * (v ?? 0) / ((atrR[i] ?? 0) || 1));
      const sumDI  = plusDI.map((_,i)=> (plusDI[i] + minusDI[i]) || 1);
      const diff   = plusDI.map((_,i)=> Math.abs(plusDI[i]-minusDI[i]) / sumDI[i]);
      const adxArr = rma(diff, adxLen).map(v=> 100*(v ?? 0));

      const thr = Number(params.adxThreshold ?? 25);
      const markers = [];
      for (let i=1;i<N;i++){
        const crossUp   = macd[i] >  signal[i] && macd[i-1] <= signal[i-1] && macd[i-1] < 0;
        const crossDown = signal[i] > macd[i]   && signal[i-1] <= macd[i-1] && macd[i-1] > 0;

        if (adxArr[i] > thr && crossUp){
          markers.push({ name:"buy", time:times[i], position:"belowBar", shape:"arrowUp",
            color:params.buyColor, price:low[i], text:params.buyText, textColor:params.buyTextColor });
        }
        if (adxArr[i] > thr && crossDown){
          markers.push({ name:"sell", time:times[i], position:"aboveBar", shape:"arrowDown",
            color:params.sellColor, price:high[i], text:params.sellText, textColor:params.sellTextColor });
        }
      }
      markerCtrl.set(markers);
    } else {
      markerCtrl.clear();
    }

    return [];
  }

  function destroy(){
    clearAllSegs();
    try{
      bandPrim?.setBand([],[],[],[]);
      if (typeof candleSeries.detachPrimitive === "function") candleSeries.detachPrimitive(bandPrim);
      else if (typeof candleSeries.removePrimitive === "function") candleSeries.removePrimitive(bandPrim);
    }catch(_){}
    markerCtrl.clear();
  }

  return { update, destroy, onParamsChanged };
}

2. Второй код: горизонтальные линии и блоки с заливкой

После первого обучающего кода вам нужно отправить второй блок, который отвечает за построение горизонтальных уровней и зон. На этом шаге нейросеть учится рисовать:

  • области поддержки и сопротивления;
  • прямоугольные зоны с заливкой;
  • статусы зон (живые, протестированные, сломанные);
  • визуальные сигналы BUY и SELL от этих зон.

Текст промпта для второго кода:

- Вот тебе второй код, научись как правильно рисовать горизонтальные линии и горизонтальные блоки с заливкой,
- Просто изучи код и скажи что все понял и принял, после чего жди от меня команду с моими пожеланиями для создания новых инструментов  

Сам код:  

export const meta = {
  name: "Support & Resistance Signals",
  defaultParams: {
    // ======= 🎯 СИГНАЛЫ =======
    signalsEnabled: true,           // 🔕 Сигналы: Включены/Выключены
    mode: "Отскок",                 // "Отскок" | "Пробой" | "Ретест"
    signalFilter: "Простые",        // "Простые" | "С фильтрами"
    bufferAtr: 0.20,                // ×ATR — буфер для событий/подтверждений

    // — Поведение сигналов —
    minLevelAgeBars: 1,             // Возраст уровня (бары) до первого сигнала
    firstTouchOnly: true,          // Только первое касание
    reentryAwayAtr: 0.00,           // Повторный заход: отход ≥ ×ATR (0=выкл)
    confirmOverMid: false,          // Для «Простых»: требовать закрытие за серединой зоны
    confirmBufferAtr: 0.15,         // Буфер по середине (×ATR)

    // ======= 🧱 УРОВНИ =======
    method: "По теням",             // "По теням" | "По телам"
    leftBars: 5,                    // L — пивот слева
    rightBars: 3,                   // R — подтверждение справа
    // Допуск равенства для пивотов (почти равные High/Low не ломают подтверждение)
    eqTolMode: "×ATR",              // "×ATR" | "Пункты"
    eqTolAtr: 0.04,                 // ×ATR (если режим ×ATR)
    eqTolPoints: 0,                 // пункты (если режим Пункты)

    thicknessMode: "Авто",          // "Авто" | "Пункты"
    zoneAtrMult: 0.30,              // ×ATR (толщина авто)
    thicknessPoints: 0,             // пункты (для «Пункты»)
    zoneMinPoints: 0,               // мин. толщина (пункты)
    mergeEnabled: true,             // сливать близкие уровни

    // ======= ⛔ СОБЫТИЯ/ПРОДЛЕНИЕ =======
    keepUntilBreak: true,           // 📌 Не удалять до пробоя (держать после касаний)
    stopOn: "Оба варианта",         // если keepUntilBreak=false: "Касание" | "Пробой" | "Оба варианта"
    expiryBars: 3,                  // экспирация «сырых» зон (когда keepUntilBreak=false)
    preSignals: true,               // предсигналы (логика до закрытия)

    // ======= ⚙️ ПРОФИ =======
    lookbackBars: 1000,
    atrPeriod: 14,
    minImpulseAtr: 0.0,             // мин. импульс после свинга (×ATR), 0=выкл
    mergeTolAtr: 0.25,
    mergeTimeGap: 10,
    touchBy: "Тени",                // "Тени" | "Закрытию"
    breakBy: "Закрытию",            // "Закрытию" | "Тени"
    minVolAtr: 0.0,
    cooldownBars: 2,
    minDistanceOppAtr: 0.7,
    maxLevels: 40,
    maxLevelsPerSide: 30,

    // ======= 🎨 ВИД ЗОН =======
    fillAlpha: 0.10,
    showBorder: false,
    borderWidth: 1,
    supColor: "#10b981",            // поддержка — зелёный
    resColor: "#f43f5e",            // сопротивление — красный
    mitigatedColor: "#3b82f6",      // тестированная — синий
    invalidColor: "#9ca3af",        // сломанная — серый

    // ======= 🏷 МАРКЕРЫ =======
    buyText: "BUY",
    sellText: "SELL",
    buyColor: "#16a34a",
    sellColor: "#ef4444",
    buyTextColor: "#ffffff",
    sellTextColor: "#ffffff",
    buyShape: "Стрелка вверх",      // "Стрелка вверх" | "Круг" | "Квадрат"
    sellShape: "Стрелка вниз"       // "Стрелка вниз" | "Круг" | "Квадрат"
  },
  paramMeta: {
    // 🎯 Сигналы
    signalsEnabled:     { label: "🔕 Сигналы", type: "boolean" },
    mode:               { label: "🎯 Режим сигналов", type: "select", options: ["Отскок","Пробой","Ретест"] },
    signalFilter:       { label: "Фильтр сигналов", type: "select", options: ["Простые","С фильтрами"] },
    bufferAtr:          { label: "Буфер события ×ATR", type: "number", min: 0, max: 2, step: 0.05 },

    minLevelAgeBars:    { label: "Возраст уровня (бары)", type: "number", min: 0, max: 20 },
    firstTouchOnly:     { label: "Только первое касание", type: "boolean" },
    reentryAwayAtr:     { label: "Повторный заход: отход ≥ ×ATR", type: "number", min: 0, max: 5, step: 0.05 },
    confirmOverMid:     { label: "Подтверждение по середине зоны", type: "boolean" },
    confirmBufferAtr:   { label: "Буфер по середине ×ATR", type: "number", min: 0, max: 2, step: 0.05 },

    // 🧱 Уровни
    method:             { label: "🧱 Метод зоны", type: "select", options: ["По теням","По телам"] },
    leftBars:           { label: "Пивот слева (L)", type: "number", min: 1, max: 20 },
    rightBars:          { label: "Подтверждение справа (R)", type: "number", min: 1, max: 20 },
    eqTolMode:          { label: "🧪 Допуск равенства — режим", type: "select", options: ["×ATR","Пункты"] },
    eqTolAtr:           { label: "Допуск равенства ×ATR", type: "number", min: 0, max: 0.5, step: 0.01 },
    eqTolPoints:        { label: "Допуск равенства (пункты)", type: "number", min: 0, max: 100000 },

    thicknessMode:      { label: "Толщина зоны", type: "select", options: ["Авто","Пункты"] },
    zoneAtrMult:        { label: "Толщина ×ATR (для Авто)", type: "number", min: 0.05, max: 2, step: 0.05 },
    thicknessPoints:    { label: "Толщина (пункты)", type: "number", min: 0, max: 100000 },
    zoneMinPoints:      { label: "Мин. толщина (пункты)", type: "number", min: 0, max: 100000 },
    mergeEnabled:       { label: "Сливать близкие уровни", type: "boolean" },

    // ⛔ События/Продление
    keepUntilBreak:     { label: "📌 Не удалять до пробоя", type: "boolean" },
    stopOn:             { label: "⛔ Останов продления (если выключен «до пробоя»)", type: "select", options: ["Касание","Пробой","Оба варианта"] },
    expiryBars:         { label: "⏱ Экспирация (бары)", type: "number", min: 1, max: 10 },
    preSignals:         { label: "✨ Предсигналы до закрытия", type: "boolean" },

    // ⚙️ Профи
    lookbackBars:       { label: "⚙️ Профи — Глубина скана (бары)", type: "number", min: 200, max: 20000 },
    atrPeriod:          { label: "ATR период", type: "number", min: 5, max: 200 },
    minImpulseAtr:      { label: "Мин. импульс после свинга ×ATR", type: "number", min: 0, max: 5, step: 0.05 },
    mergeTolAtr:        { label: "Слияние: допуск ×ATR", type: "number", min: 0.05, max: 2, step: 0.05 },
    mergeTimeGap:       { label: "Слияние: разрыв (бары)", type: "number", min: 0, max: 200 },
    touchBy:            { label: "Касание по", type: "select", options: ["Тени","Закрытию"] },
    breakBy:            { label: "Пробой по", type: "select", options: ["Закрытию","Тени"] },
    minVolAtr:          { label: "Мин. волатильность (ATR)", type: "number", min: 0, max: 100000 },
    cooldownBars:       { label: "Кулдаун по зоне (бары)", type: "number", min: 0, max: 50 },
    minDistanceOppAtr:  { label: "Мин. дистанция до встречного уровня ×ATR", type: "number", min: 0, max: 5, step: 0.05 },
    maxLevels:          { label: "Макс. уровней всего", type: "number", min: 1, max: 60 },
    maxLevelsPerSide:   { label: "Макс. зон на сторону", type: "number", min: 0, max: 60 },

    // 🎨 Вид
    fillAlpha:          { label: "🎨 Вид — Заливка (прозрачн.)", type: "number", min: 0.05, max: 1, step: 0.05 },
    showBorder:         { label: "Показывать рамку", type: "boolean" },
    borderWidth:        { label: "Толщина рамки", type: "number", min: 0, max: 6 },
    supColor:           { label: "Цвет Поддержек", type: "color" },
    resColor:           { label: "Цвет Сопротивлений", type: "color" },
    mitigatedColor:     { label: "Цвет Тестированных", type: "color" },
    invalidColor:       { label: "Цвет Сломанных", type: "color" },

    // 🏷 Маркеры
    buyText:            { label: "🏷 Маркеры — Текст BUY", type: "string" },
    sellText:           { label: "Текст SELL", type: "string" },
    buyColor:           { label: "Цвет стрелки BUY", type: "color" },
    sellColor:          { label: "Цвет стрелки SELL", type: "color" },
    buyTextColor:       { label: "Цвет текста BUY", type: "color" },
    sellTextColor:      { label: "Цвет текста SELL", type: "color" },
    buyShape:           { label: "Форма BUY", type: "select", options: ["Стрелка вверх","Круг","Квадрат"] },
    sellShape:          { label: "Форма SELL", type: "select", options: ["Стрелка вниз","Круг","Квадрат"] }
  }
};

export function init(ctx){
  const { candleSeries, params, createSeriesMarkers } = ctx;
  const markersApi = createSeriesMarkers(candleSeries, []);

  // ===== Примитив для прямоугольников =====
  class ZonesPrimitive {
    constructor(o={}){ this._v=new View(o); }
    attached(p){ this._v.attached(p); }
    detached(){ this._v.detached(); }
    paneViews(){ return [this._v]; }
    updateAllViews(){ this._v.update(); }
    setOptions(o){ this._v.setOptions(o); }
    setBoxes(list){ this._v.setBoxes(list); }
  }
  class View{
    constructor(o){
      this._s=null; this._c=null; this._req=null;
      this._boxes=[]; this._o={ fillAlpha:.12, showBorder:true, borderWidth:1, ...o };
    }
    attached({series,chart,requestUpdate}){ this._s=series; this._c=chart; this._req=requestUpdate; }
    detached(){ this._s=this._c=this._req=null; }
    update(){} zOrder(){ return "top"; }
    setOptions(o){ Object.assign(this._o,o); this._req?.(); }
    setBoxes(list){ this._boxes = Array.isArray(list)? list : []; this._req?.(); }
    renderer(){
      const self=this;
      const drawImpl=(target)=>{
        const s=self._s, c=self._c; if(!s||!c) return; const ts=c.timeScale(); if(!ts) return;
        const o=self._o;
        const draw=(ctx,prX=1,prY=1)=>{
          ctx.save(); ctx.scale(prX,prY);
          for(const b of self._boxes){
            const x1=ts.timeToCoordinate(b.t1), x2=ts.timeToCoordinate(b.t2);
            const y1=s.priceToCoordinate(b.top), y2=s.priceToCoordinate(b.bot);
            if(x1==null||x2==null||y1==null||y2==null) continue;
            const l=Math.min(x1,x2), r=Math.max(x1,x2);
            const t=Math.min(y1,y2), B=Math.max(y1,y2);
            const w=Math.max(1,r-l), h=Math.max(1,B-t);
            ctx.globalAlpha=Math.max(0,Math.min(1,o.fillAlpha));
            ctx.fillStyle=b.color; ctx.fillRect(l,t,w,h);
            if(o.showBorder && (o.borderWidth|0)>0){
              ctx.globalAlpha=1; ctx.lineWidth=Math.max(1,o.borderWidth|0);
              ctx.strokeStyle=b.color; ctx.strokeRect(l+.5,t+.5,w,h);
            }
          }
          ctx.restore();
        };
        if(typeof target.useBitmapCoordinateSpace==="function"){
          target.useBitmapCoordinateSpace(sc=>draw(sc.context,sc.horizontalPixelRatio,sc.verticalPixelRatio));
        } else draw(target.context,target.pixelRatio||1,target.pixelRatio||1);
      };
      return { drawBackground: drawImpl, draw: drawImpl };
    }
  }
  let prim = new ZonesPrimitive({
    fillAlpha: params.fillAlpha, showBorder: params.showBorder, borderWidth: params.borderWidth
  });
  if(typeof candleSeries.attachPrimitive==="function") candleSeries.attachPrimitive(prim);
  else if(ctx.primitivesApi?.attachPrimitive) ctx.primitivesApi.attachPrimitive(candleSeries, prim);

  // ===== Utils =====
  const clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
  const mapShape=(val,defVal)=>{
    if(val==="Стрелка вверх") return "arrowUp";
    if(val==="Стрелка вниз")  return "arrowDown";
    if(val==="Круг")          return "circle";
    if(val==="Квадрат")       return "square";
    if(["arrowUp","arrowDown","circle","square"].includes(val)) return val;
    return defVal;
  };
  function atrArr(c,L){
    const N=c.length, out=new Array(N).fill(NaN), tr=new Array(N).fill(0);
    if(!N) return out; tr[0]=c[0].high-c[0].low;
    for(let i=1;i<N;i++){
      const a=c[i], b=c[i-1];
      tr[i]=Math.max(a.high-a.low, Math.abs(a.high-b.close), Math.abs(a.low-b.close));
    }
    let s=0; for(let i=0;i<N;i++){ s+=tr[i]; if(i>=L) s-=tr[i-L]; if(i>=L-1) out[i]=s/L; } return out;
  }
  // Пивоты с допуском (строго L слева и R справа)
  function isPH_tol(c,i,L,R,tol){ const v=c[i].high;
    for(let k=1;k<=L;k++) if(c[i-k].high >= v - tol) return false;
    for(let k=1;k<=R;k++) if(c[i+k].high >  v + tol) return false;
    return true;
  }
  function isPL_tol(c,i,L,R,tol){ const v=c[i].low;
    for(let k=1;k<=L;k++) if(c[i-k].low  <= v + tol) return false;
    for(let k=1;k<=R;k++) if(c[i+k].low  <  v - tol) return false;
    return true;
  }
  function zoneFromPivot(c,i,side,method,half){
    const bar=c[i];
    if(method==="По телам"){
      if(side==='resistance'){ const base=Math.max(bar.open,bar.close); return { top:base+half, bot:base-half, mid:base }; }
      const base=Math.min(bar.open,bar.close); return { top:base+half, bot:base-half, mid:base };
    } else {
      if(side==='resistance'){ const base=bar.high; return { top:base+half, bot:base-half, mid:base }; }
      const base=bar.low; return { top:base+half, bot:base-half, mid:base };
    }
  }

  function isTouchAt(c,i,z,cfg){
    if(i<0||i>=c.length) return false; const b=c[i];
    if(cfg.touchBy==="Закрытию") return b.close>=z.bot && b.close<=z.top;
    return b.low<=z.top && b.high>=z.bot;
  }
  function isBreakAt(c,i,z,cfg,atr){
    if(i<0||i>=c.length) return false; const b=c[i];
    const buf=(cfg.bufferAtr||0)*(atr[i]||atr[i-1]||0);
    if(cfg.breakBy==="Закрытию"){
      if(z.side==="support")    return b.close<(z.bot-buf);
      if(z.side==="resistance") return b.close>(z.top+buf);
    } else {
      if(z.side==="support")    return b.low  <(z.bot-buf);
      if(z.side==="resistance") return b.high >(z.top+buf);
    }
    return false;
  }
  function firstEventIndex(c, z, iStart, iEnd, cfg, atr){
    let mit=null, inv=null;
    for(let i=iStart;i<=iEnd;i++){
      if(mit==null && isTouchAt(c,i,z,cfg)) mit=i;
      if(inv==null && isBreakAt(c,i,z,cfg,atr)) inv=i;
      if(mit!=null && inv!=null) break;
    }
    return { mit, inv };
  }
  const midPrice=(z)=> (z.top+z.bot)/2;

  function mergeZones(list,tolPrice,gapBars){
    if(!list.length) return [];
    const arr=list.slice().sort((a,b)=>a.born-b.born);
    const out=[]; let cur=null;
    for(const z of arr){
      if(!cur){ cur={...z}; continue; }
      const sameSide = cur.side===z.side;
      const priceClose = Math.abs(z.mid - cur.mid) <= tolPrice;
      const timeClose  = z.born <= (cur.right ?? cur.born) + gapBars;
      if(sameSide && priceClose && timeClose){
        cur.mid=(cur.mid+z.mid)/2; cur.top=(cur.top+z.top)/2; cur.bot=(cur.bot+z.bot)/2;
        // якорим на СВЕЖЕМ экстремуме, чтобы левый край не «полз» назад
        cur.anchor = Math.max(cur.anchor, z.anchor);
        cur.born   = Math.max(cur.born, z.born);   // подтверждение — тоже самое позднее
      } else { out.push(cur); cur={...z}; }
    }
    if(cur) out.push(cur);
    return out;
  }

  function buildZones(candles, atr){
    const N=candles.length, endIdx=N-1;
    const look=Math.max(200, Math.min((params.lookbackBars|0)||1500, N));
    const startIdx=Math.max(5, endIdx - look + 1);
    const atrRef=atr[endIdx] || atr.filter(Number.isFinite).slice(-1)[0] || (candles[endIdx].high - candles[endIdx].low);

    const L=clamp(params.leftBars|0,1,20);
    const R=clamp(params.rightBars|0,1,20);
    const eqTol=(i)=> (params.eqTolMode==="×ATR" ? Math.max(0,params.eqTolAtr||0)*(atr[i]||atrRef)
                                                 : Math.max(0,params.eqTolPoints||0));

    const method=params.method;
    const minImpulse=Math.max(0, params.minImpulseAtr||0);

    const raw=[];
    for(let i=startIdx+L; i<=endIdx-R; i++){
      const tol=eqTol(i);
      const ph = isPH_tol(candles,i,L,R,tol);
      const pl = isPL_tol(candles,i,L,R,tol);

      if(ph){
        const half = Math.max(
          params.thicknessMode==="Авто" ? (params.zoneAtrMult||0.3)*(atr[i]||atrRef)*0.5 : (params.thicknessPoints||0)*0.5,
          (params.zoneMinPoints||0)*0.5
        );
        const z=zoneFromPivot(candles,i,"resistance",method,half);
        const next=Math.min(endIdx,i+3);
        const imp = Math.max(0,(candles[i].high - candles[next].close))/Math.max(1e-9, atr[i]||atrRef);
        if(imp>=minImpulse) raw.push({ side:"resistance", anchor:i, born:i+R, top:z.top, bot:z.bot, mid:z.mid });
      }
      if(pl){
        const half = Math.max(
          params.thicknessMode==="Авто" ? (params.zoneAtrMult||0.3)*(atr[i]||atrRef)*0.5 : (params.thicknessPoints||0)*0.5,
          (params.zoneMinPoints||0)*0.5
        );
        const z=zoneFromPivot(candles,i,"support",method,half);
        const next=Math.min(endIdx,i+3);
        const imp = Math.max(0,(candles[next].close - candles[i].low))/Math.max(1e-9, atr[i]||atrRef);
        if(imp>=minImpulse) raw.push({ side:"support", anchor:i, born:i+R, top:z.top, bot:z.bot, mid:z.mid });
      }
    }

    // слияние
    let merged=raw;
    if(params.mergeEnabled){
      const tol=(params.mergeTolAtr||0.25)*atrRef;
      const gap=Math.max(0, params.mergeTimeGap|0);
      merged = mergeZones(raw, tol, gap);
    }
    merged.sort((a,b)=>a.born-b.born);

    // лимиты по сторонам
    const sup=merged.filter(z=>z.side==="support");
    const res=merged.filter(z=>z.side==="resistance");
    const cut=(arr)=> (params.maxLevelsPerSide>0 ? arr.slice(-params.maxLevelsPerSide) : arr);
    let zones = cut(sup).concat(cut(res)).sort((a,b)=>a.born-b.born);
    zones = zones.slice(-Math.max(1, params.maxLevels|0));

    // правая граница и статус
    const cfg={ touchBy: params.touchBy, breakBy: params.breakBy, bufferAtr: (params.signalFilter==="Простые")? 0 : (params.bufferAtr||0) };
    for(const z of zones){
      const start=Math.min(z.born+1, endIdx);
      let right=endIdx;
      const ev = firstEventIndex(candles, z, start, endIdx, cfg, atr);

      if(params.keepUntilBreak){
        // держим до пробоя: тянем до now, останавливаемся только на пробое
        if(ev.inv!=null){ right = Math.max(start, ev.inv); }
      } else {
        if(params.stopOn==="Касание" && ev.mit!=null) right=Math.max(start, ev.mit);
        else if(params.stopOn==="Пробой" && ev.inv!=null) right=Math.max(start, ev.inv);
        else if(params.stopOn==="Оба варианта"){
          const stopIdx = (ev.mit==null)? ev.inv : (ev.inv==null? ev.mit : Math.min(ev.mit,ev.inv));
          if(stopIdx!=null) right=Math.max(start, stopIdx);
        }
      }
      z.right = right;
      z.status = (ev.inv!=null && ev.inv<=right) ? -1 : ((ev.mit!=null && ev.mit<=right) ? 1 : 0);
    }

    return { zones, atrRef };
  }

  function drawZones(candles, zones){
    const endIdx=candles.length-1;
    const rects=[];
    for(const z of zones){
      const color = z.status===-1 ? params.invalidColor :
                    z.status=== 1 ? params.mitigatedColor :
                    (z.side==="support" ? params.supColor : params.resColor);
      const t1 = candles[clamp(z.anchor,0,endIdx)].time; // рисуем ОТ экстремума
      const t2 = candles[clamp(z.right, 0,endIdx)].time; // продлеваем по логике
      rects.push({ t1, t2, top:z.top, bot:z.bot, color });
    }
    prim.setOptions({ fillAlpha: params.fillAlpha, showBorder: params.showBorder, borderWidth: params.borderWidth });
    prim.setBoxes(rects);
  }

  // ===== Сигналы =====
  function firstTouchIndex(candles, z, cfg, end){ for(let j=z.born; j<=end; j++) if(isTouchAt(candles,j,z,cfg)) return j; return null; }
  function hasAwaySince(candles, z, i, atr, thr){
    if(thr<=0) return true;
    for(let j=z.born; j<i; j++){
      const a=atr[j]||atr[j-1]||0;
      const d=Math.abs((candles[j].close??candles[j].open) - z.mid);
      if(d >= thr*a && (candles[j].close>z.top || candles[j].close<z.bot)) return true;
    }
    return false;
  }

  function buildSignals(candles, zones, atr){
    if(!params.signalsEnabled) return [];
    const N=candles.length, endIdx=N-1;
    const simple = params.signalFilter==="Простые";
    const cfg = { touchBy: params.touchBy, breakBy: params.breakBy, bufferAtr: simple?0:(params.bufferAtr||0) };
    const markers=[];
    const bySide={ support: zones.filter(z=>z.side==='support'), resistance: zones.filter(z=>z.side==='resistance') };
    const allowOppDistance=(z, aVal)=>{
      if(simple) return true;
      const others = bySide[z.side==='support'?'resistance':'support'];
      if(!others.length) return true;
      const near = others.reduce((m,x)=> (m==null || Math.abs(x.mid-z.mid)<Math.abs(m.mid-z.mid)) ? x : m, null);
      return !near || Math.abs(near.mid-z.mid) >= (Math.max(0,params.minDistanceOppAtr||0)*aVal);
    };

    const cooldown=Math.max(0, params.cooldownBars|0);
    const lastSig=new Map();

    for(const z of zones){
      const start=Math.max(z.born + Math.max(0, params.minLevelAgeBars|0), 0);
      for(let i=start; i<=Math.min(z.right,endIdx); i++){
        const prev=Math.max(0,i-1);
        const a=atr[i]||atr[prev]||0;
        const awayBuf=(params.bufferAtr||0)*a;
        const last=lastSig.get(z) ?? -1;
        if(!simple && last>=0 && i-last<=cooldown) continue;

        if(params.firstTouchOnly){
          const ft = firstTouchIndex(candles, z, cfg, i);
          if(ft==null || (simple ? (i!==ft) : (prev!==ft))) continue;
        }
        if(!hasAwaySince(candles, z, i, atr, Math.max(0, params.reentryAwayAtr||0))) continue;

        if(simple){
          if(params.mode==="Отскок"){
            if(isTouchAt(candles,i,z,cfg)){
              if(params.confirmOverMid){
                const confBuf=(params.confirmBufferAtr||0)*a;
                const needUp   = z.side==="support"    ? (candles[i].close >= (midPrice(z)+confBuf)) : false;
                const needDown = z.side==="resistance" ? (candles[i].close <= (midPrice(z)-confBuf)) : false;
                if(!(needUp||needDown)) continue;
              }
              if(z.side==="support" && allowOppDistance(z,a)){
                markers.push({ name:"buy", time:candles[i].time, position:"belowBar",
                  shape: mapShape(params.buyShape,"arrowUp"), color: params.buyColor,
                  price:candles[i].close, text: params.buyText||"BUY" });
                lastSig.set(z,i);
              } else if(z.side==="resistance" && allowOppDistance(z,a)){
                markers.push({ name:"sell", time:candles[i].time, position:"aboveBar",
                  shape: mapShape(params.sellShape,"arrowDown"), color: params.sellColor,
                  price:candles[i].close, text: params.sellText||"SELL" });
                lastSig.set(z,i);
              }
            }
          } else if(params.mode==="Пробой"){
            if(isBreakAt(candles,i,z,cfg,atr) && allowOppDistance(z,a)){
              if(z.side==="resistance"){
                markers.push({ name:"buy", time:candles[i].time, position:"belowBar",
                  shape: mapShape(params.buyShape,"arrowUp"), color: params.buyColor,
                  price:candles[i].close, text: params.buyText||"BUY" });
              } else {
                markers.push({ name:"sell", time:candles[i].time, position:"aboveBar",
                  shape: mapShape(params.sellShape,"arrowDown"), color: params.sellColor,
                  price:candles[i].close, text: params.sellText||"SELL" });
              }
              lastSig.set(z,i);
            }
          } else if(params.mode==="Ретест"){
            let broke=false; for(let j=Math.max(start,i-5); j<i; j++){ if(isBreakAt(candles,j,z,cfg,atr)){ broke=true; break; } }
            if(broke && isTouchAt(candles,i,z,cfg) && allowOppDistance(z,a)){
              if(z.side==="resistance"){
                markers.push({ name:"buy", time:candles[i].time, position:"belowBar",
                  shape: mapShape(params.buyShape,"arrowUp"), color: params.buyColor,
                  price:candles[i].close, text: params.buyText||"BUY" });
              } else {
                markers.push({ name:"sell", time:candles[i].time, position:"aboveBar",
                  shape: mapShape(params.sellShape,"arrowDown"), color: params.sellColor,
                  price:candles[i].close, text: params.sellText||"SELL" });
              }
              lastSig.set(z,i);
            }
          }
          continue;
        }

        // «С фильтрами»
        if(params.mode==="Отскок"){
          if(isTouchAt(candles,prev,z,cfg)){
            if(z.side==="support"){
              if(candles[i].close >= (midPrice(z)+awayBuf) && allowOppDistance(z,a)){
                markers.push({ name:"buy", time:candles[i].time, position:"belowBar",
                  shape: mapShape(params.buyShape,"arrowUp"), color: params.buyColor,
                  price:candles[i].close, text: params.buyText||"BUY" });
                lastSig.set(z,i);
              }
            } else {
              if(candles[i].close <= (midPrice(z)-awayBuf) && allowOppDistance(z,a)){
                markers.push({ name:"sell", time:candles[i].time, position:"aboveBar",
                  shape: mapShape(params.sellShape,"arrowDown"), color: params.sellColor,
                  price:candles[i].close, text: params.sellText||"SELL" });
                lastSig.set(z,i);
              }
            }
          }
        } else if(params.mode==="Пробой"){
          if(isBreakAt(candles,i,z,cfg,atr) && allowOppDistance(z,a)){
            if(z.side==="resistance"){
              markers.push({ name:"buy", time:candles[i].time, position:"belowBar",
                shape: mapShape(params.buyShape,"arrowUp"), color: params.buyColor,
                price:candles[i].close, text: params.buyText||"BUY" });
            } else {
              markers.push({ name:"sell", time:candles[i].time, position:"aboveBar",
                shape: mapShape(params.sellShape,"arrowDown"), color: params.sellColor,
                price:candles[i].close, text: params.sellText||"SELL" });
            }
            lastSig.set(z,i);
          }
        } else if(params.mode==="Ретест"){
          const window=3; let brokeUp=false, brokeDn=false, jBreak=-1;
          for(let j=Math.max(start,i-window); j<i; j++){
            if(isBreakAt(candles,j,z,cfg,atr)){ if(z.side==="resistance") brokeUp=true; if(z.side==="support") brokeDn=true; jBreak=j; }
          }
          if(jBreak>=0 && isTouchAt(candles,prev,z,cfg)){
            if(z.side==="resistance" && brokeUp){
              if(candles[i].close >= (midPrice(z)+awayBuf) && allowOppDistance(z,a)){
                markers.push({ name:"buy", time:candles[i].time, position:"belowBar",
                  shape: mapShape(params.buyShape,"arrowUp"), color: params.buyColor,
                  price:candles[i].close, text: params.buyText||"BUY" });
                lastSig.set(z,i);
              }
            } else if(z.side==="support" && brokeDn){
              if(candles[i].close <= (midPrice(z)-awayBuf) && allowOppDistance(z,a)){
                markers.push({ name:"sell", time:candles[i].time, position:"aboveBar",
                  shape: mapShape(params.sellShape,"arrowDown"), color: params.sellColor,
                  price:candles[i].close, text: params.sellText||"SELL" });
                lastSig.set(z,i);
              }
            }
          }
        }
      }
    }
    return markers;
  }

  function update(candles){
    const N=candles?.length||0;
    if(N<50){ prim.setBoxes([]); markersApi.setMarkers([]); return []; }

    const atr = atrArr(candles, Math.max(5, params.atrPeriod|0));
    const atrRef = atr[N-1] || atr.filter(Number.isFinite).slice(-1)[0] || (candles[N-1].high - candles[N-1].low);
    if((params.minVolAtr||0)>0 && atrRef<params.minVolAtr){ prim.setBoxes([]); markersApi.setMarkers([]); return []; }

    const { zones } = buildZones(candles, atr);
    drawZones(candles, zones);

    const markers = buildSignals(candles, zones, atr);
    const mapped = markers.map(m=>({
      ...m,
      text: m.name==='buy' ? (params.buyText||'BUY') : (params.sellText||'SELL'),
      color: m.name==='buy' ? (params.buyColor||'#16a34a') : (params.sellColor||'#ef4444'),
      textColor: m.name==='buy' ? (params.buyTextColor||'#ffffff') : (params.sellTextColor||'#ffffff')
    }));
    markersApi.setMarkers(mapped);
    return mapped;
  }

  function destroy(){
    try{
      prim?.setBoxes([]);
      if(typeof candleSeries.detachPrimitive==="function") candleSeries.detachPrimitive(prim);
      else if(typeof candleSeries.removePrimitive==="function") candleSeries.removePrimitive(prim);
    }catch(_){}
    prim=null; markersApi.setMarkers([]);
  }

  return { update, destroy };
}

После отправки этого блока GPT должен подтвердить, что всё понял и ждет следующих задач. На этом обучение можно считать завершенным и переходить к практическим задачам.


Что делать после обучения: ставим задачи GPT

После того как вы отправили в чат оба кода и получили подтверждение от модели, можно начинать формулировать собственные задачи по созданию визуальных инструментов под ваши нужды.

Примеры запросов:

  1. Классический индикатор из двух скользящих средних

    Создай классический визуальный индикатор состоящий из двух скользящих средних.
    Первая линия с настройкой 20, вторая с настройкой 50.
    При пересечении линий снизу вверх — сигнал BUY, при пересечении сверху вниз — сигнал SELL.

  2. Классические уровни поддержки и сопротивления

    Создай классические уровни поддержки и сопротивления без сигналов.
    Мне нужны только уровни и зоны, без стрелок BUY и SELL.

  3. Индикатор KAMA

    Создай индикатор KAMA для графика, который будет рисоваться поверх цены.
    Добавь параметры для настройки периода и сглаживания.

  4. Любые другие вариации
    Вы можете комбинировать идеи:
    • трендовые линии плюс уровни;
    • фильтры по времени торговли;
    • визуализация зон, где вы чаще получаете прибыльные сделки на бинарных опционах.

Небольшой чек-лист:

Подготовьте новый чат

Создайте новый чистый чат с GPT, чтобы никакие старые инструкции не мешали обучению.

Отправьте первый обучающий промпт и код

Вставьте текст с фразой «Обучающий промпт» и замените «ТУТ БУДЕТ КОД» на реальный каркас первого индикатора. Дождитесь ответа, что всё понятно.

Отправьте второй код с уровнями и зонами

Вставьте текст второго промпта и замените вторую заглушку «ТУТ БУДЕТ КОД» на каркас инструмента уровней поддержки и сопротивления.

Сформулируйте свою задачу

Опишите простыми словами, какой визуальный индикатор или инструмент вам нужен для Spectra Charts и торговли на Pocket Option. Попросите нейросеть выдать готовый код целиком.


Важно понимать ограничения

Есть один принципиальный момент, который нужно усвоить заранее.
  • Данные промпты обучают нейросеть создавать графические индикаторы, которые рисуются поверх графика цены.
  • Индикаторы-осцилляторы, которые обычно находятся в нижнем дополнительном окне, создаваться НЕ БУДУТ.
    Речь идёт именно о визуальных инструментах, которые живут на основном графике свечей.
Это хорошо подходит для:
  • уровней поддержки и сопротивления;
  • скользящих средних и трендовых линий;
  • визуальных зон входа и выхода;
  • стрелок BUY/SELL для бинарных опционов.

Пример визуализации на графике

Так может выглядеть результат работы обученного GPT и вашего индикатора в Spectra Charts (простой пример уровней и скользящей средней):

Вы видите:
  • аккуратные линии и зоны поверх графика;
  • понятные уровни, от которых можно искать входы;
  • визуальные элементы, которые помогают принимать решения по сделкам на Pocket Option.

Частые проблемы и их решение

Чтобы вам не пришлось гадать, соберём типичные вопросы в один блок.


Итоги

  • Вы обучаете GPT на двух каркасах кода, которые показывают, как рисовать линии, зоны и сигналы прямо на графике Spectra Charts.
  • После этого вы можете формулировать любые задачи по созданию визуальных инструментов под свои стратегии на Pocket Option и других платформах с бинарными опционами.
  • Даже если вы только начинаете и слабо разбираетесь в программировании, грамотная работа с промптами позволяет переложить 99% сложной работы на нейросеть, а самому сосредоточиться на стратегии, управлении риском и отборе качественных сигналов.
Используйте этот подход как конструктор. Сначала — обучение чата, затем — создание базовых визуальных индикаторов, после чего постепенно добавляйте новые идеи и фильтры. Так вы получите удобный набор инструментов, который будет работать именно под ваш стиль торговли и ваш темп работы с бинарными опционами.