Sistema de alertas de RSI de criptomonedas con EODHD, Telegram y gráficos de TradingView

Intermedio

Este es unCrypto Trading, Multimodal AIflujo de automatización del dominio deautomatización que contiene 15 nodos.Utiliza principalmente nodos como If, Set, Code, SplitOut, Telegram. Sistema de alertas de RSI de cripto integrado con EODHD, Telegram y gráficos de TradingView

Requisitos previos
  • Bot Token de Telegram
  • Pueden requerirse credenciales de autenticación para la API de destino
Vista previa del flujo de trabajo
Visualización de las conexiones entre nodos, con soporte para zoom y panorámica
Exportar flujo de trabajo
Copie la siguiente configuración JSON en n8n para importar y usar este flujo de trabajo
{
  "meta": {
    "instanceId": "c2b1f8c1d4a74a0cb8a1f1d3b7b0e7c1b9d2f5a8f3e44c9c93a1e2f4a6b7c8d9",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "m1",
      "name": "Al hacer clic en 'Ejecutar flujo de trabajo'",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -520,
        -160
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "note_overview",
      "name": "Nota adhesiva — Descripción general",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -540,
        -360
      ],
      "parameters": {
        "color": 6,
        "width": 860,
        "height": 260,
        "content": "## Crypto RSI Alert Bot (overview)\n- Runs on a schedule or manual trigger.\n- Iterates a **watchlist** (BTC/ETH/SOL).\n- Fetches **intraday 1h** OHLCV from **EODHD** for each symbol.\n- Code node computes **Wilder's RSI(14)** and detects **30/70** crossings.\n- On signal, sends a **Telegram** alert (HTML) + **View chart** button (TradingView BINANCE/USD).\n\nEnv vars required:\n- `EODHD_TOKEN`\n- `TELEGRAM_CHAT_ID`"
      },
      "typeVersion": 1
    },
    {
      "id": "set1",
      "name": "Editar campos (lista de seguimiento)",
      "type": "n8n-nodes-base.set",
      "position": [
        -300,
        -160
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "sym_arr",
              "name": "symbol",
              "type": "array",
              "value": "[\"BTC-USD.CC\",\"ETH-USD.CC\",\"SOL-USD.CC\"]"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "note_watchlist",
      "name": "Nota adhesiva — Lista de seguimiento",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        -300
      ],
      "parameters": {
        "color": 6,
        "width": 560,
        "height": 160,
        "content": "### Edit Fields (watchlist)\n- Defines the **symbol array**.\n- Make sure the field type is **Array** (String[]), not a single String.\n- Example output: `{ symbol: [\"BTC-USD.CC\",\"ETH-USD.CC\",\"SOL-USD.CC\"] }`"
      },
      "typeVersion": 1
    },
    {
      "id": "split_out",
      "name": "Dividir elementos",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -80,
        -160
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "symbol"
      },
      "typeVersion": 1
    },
    {
      "id": "note_split",
      "name": "Nota adhesiva — Dividir elementos",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -100,
        -300
      ],
      "parameters": {
        "color": 6,
        "width": 520,
        "height": 150,
        "content": "### Split Out\n- Explodes the array into **one item per symbol**.\n- Input: 1 item with array → Output: N items like `{ symbol: \"BTC-USD.CC\" }`."
      },
      "typeVersion": 1
    },
    {
      "id": "loop",
      "name": "Iterar sobre elementos",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        140,
        -160
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "note_loop",
      "name": "Nota adhesiva — Bucle",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        120,
        -300
      ],
      "parameters": {
        "color": 6,
        "width": 560,
        "height": 120,
        "content": "### Loop Over Items\n- Processes **one symbol per pass** to avoid mixing BTC/ETH/SOL candles.\n- Wiring: **Loop → HTTP → Code → back to Loop**. **Done → IF**."
      },
      "typeVersion": 1
    },
    {
      "id": "http",
      "name": "HTTP Solicitud (EODHD intradía 1h)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        360,
        -260
      ],
      "parameters": {
        "url": "=https://eodhd.com/api/intraday/{{ $json.symbol }}",
        "method": "GET",
        "options": {
          "redirect": {
            "redirect": {}
          },
          "response": {
            "response": {}
          },
          "splitIntoItems": true
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "interval",
              "value": "1h"
            },
            {
              "name": "fmt",
              "value": "json"
            },
            {
              "name": "api_token",
              "value": "={{ $env.EODHD_TOKEN }}"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "note_http",
      "name": "Nota adhesiva — HTTP",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        340,
        -420
      ],
      "parameters": {
        "color": 6,
        "width": 560,
        "height": 160,
        "content": "### HTTP (EODHD)\n- Fetches **intraday 1h OHLCV** for current symbol.\n- Token via env var `EODHD_TOKEN` → no secret in JSON.\n- **Split Into Items** enabled: 1 candle = 1 item (~2–3k items)."
      },
      "typeVersion": 1
    },
    {
      "id": "code",
      "name": "Código (RSI + mensaje)",
      "type": "n8n-nodes-base.code",
      "position": [
        580,
        -260
      ],
      "parameters": {
        "jsCode": "// n8n Code node — Run Once for All Items\\n// Computes RSI(14) (Wilder) on 1h candles and raises 30/70 cross alerts.\\n\\nconst PERIOD = 14;\\nconst OVERBOUGHT = 70;\\nconst OVERSOLD = 30;\\n\\n// Collect ALL candles from the HTTP node (1 item = 1 candle)\\nconst inputItems = $input.all();\\nlet candles = [];\\nfor (const it of inputItems) {\\n  if (Array.isArray(it.json)) candles.push(...it.json);\\n  else candles.push(it.json);\\n}\\n\\n// Normalize + sort by time (prefer numeric timestamp, fallback to datetime)\\ncandles = candles\\n  .filter(r => r && r.close !== undefined)\\n  .map(r => ({\\n    t: (Number.isFinite(+r.timestamp) ? +r.timestamp\\n       : (typeof r.datetime === 'number' ? r.datetime : Date.parse(r.datetime)/1000)),\\n    close: +r.close\\n  }))\\n  .filter(r => Number.isFinite(r.t) && Number.isFinite(r.close))\\n  .sort((a,b) => a.t - b.t);\\n\\nif (candles.length < PERIOD + 2) {\\n  return [{ json: { error: 'Not enough candles for RSI', count: candles.length } }];\\n}\\n\\nconst closes = candles.map(c => c.close);\\n\\n// Wilder RSI (full series)\\nfunction rsiSeries(values, period = 14) {\\n  const deltas = [];\\n  for (let i = 1; i < values.length; i++) deltas.push(values[i] - values[i - 1]);\\n  let gain = 0, loss = 0;\\n  for (let i = 0; i < period; i++) { const d = deltas[i]; if (d >= 0) gain += d; else loss -= d; }\\n  let avgGain = gain / period; let avgLoss = loss / period;\\n  const rsis = new Array(values.length).fill(null);\\n  rsis[period] = avgLoss === 0 ? 100 : 100 - (100 / (1 + (avgGain / avgLoss)));\\n  for (let i = period + 1; i < values.length; i++) {\\n    const d = deltas[i - 1];\\n    const up = Math.max(d, 0);\\n    const down = Math.max(-d, 0);\\n    avgGain = ((avgGain * (period - 1)) + up) / period;\\n    avgLoss = ((avgLoss * (period - 1)) + down) / period;\\n    const rs = avgLoss === 0 ? Infinity : (avgGain / avgLoss);\\n    rsis[i] = 100 - (100 / (1 + rs));\\n  }\\n  return rsis;\\n}\\n\\nconst rsis = rsiSeries(closes, PERIOD);\\nconst lastIdx = rsis.length - 1;\\nconst rsiNow = +rsis[lastIdx].toFixed(1);\\nconst rsiPrev = +rsis[lastIdx - 1].toFixed(1);\\nconst lastClose = +closes[lastIdx].toFixed(2);\\nconst lastTs = candles[lastIdx].t;\\n\\n// Signals\\nlet signal = null;\\nif (rsiPrev > OVERSOLD && rsiNow <= OVERSOLD) signal = 'enter_oversold';\\nelse if (rsiPrev < OVERBOUGHT && rsiNow >= OVERBOUGHT) signal = 'enter_overbought';\\nelse if (rsiPrev <= OVERSOLD && rsiNow > OVERSOLD) signal = 'exit_oversold';\\nelse if (rsiPrev >= OVERBOUGHT && rsiNow < OVERBOUGHT) signal = 'exit_overbought';\\n\\n// ======= TEST TOGGLE (set true only to test Telegram delivery) =======\\nconst FORCE_ALERT  = false;                 // keep false in production\\nconst FORCE_SIGNAL = 'enter_overbought';    // 'enter_oversold' | 'exit_oversold' | 'exit_overbought'\\nif (FORCE_ALERT) signal = FORCE_SIGNAL;\\n// =====================================================================\\n\\n// Current symbol from the loop item (fallback to input $json)\\nconst symbol = $('Split Out')?.item?.json?.symbol || $json.symbol || 'UNKNOWN';\\n\\nconst TF = '1h';\\nconst fmt = (n, d=2) => Number(n).toLocaleString('en-US',{minimumFractionDigits:d, maximumFractionDigits:d});\\nconst tsUTC = (ts) => new Date(ts*1000).toISOString().replace('T',' ').slice(0,16) + ' UTC';\\n\\nlet emoji = '🔔', headline = '';\\nif (signal === 'enter_oversold')        { emoji='🔻'; headline = `enters <u>oversold</u> (RSI ${rsiNow} ≤ 30)`; }\\nelse if (signal === 'enter_overbought') { emoji='🚀'; headline = `enters <u>overbought</u> (RSI ${rsiNow} ≥ 70)`; }\\nelse if (signal === 'exit_oversold')    { emoji='✅'; headline = `exits <u>oversold</u> (RSI ${rsiNow})`; }\\nelse if (signal === 'exit_overbought')  { emoji='✅'; headline = `exits <u>overbought</u> (RSI ${rsiNow})`; }\\n\\nconst alertTextHtml = signal ? (\\n  `${emoji} <b>${symbol}</b> ${headline}\\n` +\\n  `Price: <b>$${fmt(lastClose)}</b> · TF: <b>${TF}</b> · ${tsUTC(lastTs)}\\n` +\\n  `RSI: <b>${rsiPrev} → ${rsiNow}</b> (30/70)\\n` +\\n  `— <i>RSI Heatwave</i>`\\n) : null;\\n\\nconst alertText = signal ? `${symbol} | ${headline.replace(/<[^>]*>/g,'')} | Price $${fmt(lastClose)} · TF ${TF} · ${tsUTC(lastTs)} | RSI ${rsiPrev}→${rsiNow}` : null;\\n\\n// TradingView link (BINANCE + USD) from EODHD symbol (e.g., BTC-USD.CC → BTCUSD)\\nconst rawSymbol = $('Split Out')?.item?.json?.symbol ?? $json.symbol ?? symbol;\\nconst sym  = Array.isArray(rawSymbol) ? rawSymbol[0] : rawSymbol; \\nconst base = String(sym).split('-')[0].toUpperCase();\\nconst tradingViewUrl = `https://www.tradingview.com/symbols/${base}USD/?exchange=BINANCE`;\\n\\nreturn [{ json: { symbol, rsi: rsiNow, rsiPrev, period: PERIOD, lastClose, signal, timestamp: lastTs, alertText, alertTextHtml, tradingViewUrl } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "note_code",
      "name": "Nota adhesiva — Código",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        -420
      ],
      "parameters": {
        "color": 6,
        "width": 560,
        "height": 170,
        "content": "### Code (RSI + message)\n- Sorts candles, computes **RSI(14)** (Wilder), detects 30/70 crossings.\n- Builds HTML message + TradingView URL (BINANCE/USD).\n- Testing: set `FORCE_ALERT = true`, then back to `false`."
      },
      "typeVersion": 1
    },
    {
      "id": "if",
      "name": "SI (¿hay señal?)",
      "type": "n8n-nodes-base.if",
      "position": [
        140,
        40
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond1",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.signal }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "tg",
      "name": "Enviar mensaje de texto",
      "type": "n8n-nodes-base.telegram",
      "position": [
        360,
        40
      ],
      "parameters": {
        "text": "={{ $json.alertTextHtml }}",
        "chatId": "={{ $env.TELEGRAM_CHAT_ID }}",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "View chart",
                    "additionalFields": {
                      "url": "={{ $json.tradingViewUrl }}"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {
          "parse_mode": "HTML",
          "disable_web_page_preview": true
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "REPLACE_WITH_YOUR_TELEGRAM_CRED_ID",
          "name": "Telegram account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "note_tg",
      "name": "Nota adhesiva — Telegram",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        340,
        180
      ],
      "parameters": {
        "color": 6,
        "width": 560,
        "height": 140,
        "content": "### Telegram (delivery)\n- Parse Mode: **HTML**.\n- Text: `{{$json.alertTextHtml}}`.\n- Button: **View chart** → `{{$json.tradingViewUrl}}`.\n- Chat ID via env var `TELEGRAM_CHAT_ID`.\n- Bot token stays in Credentials."
      },
      "typeVersion": 1
    }
  ],
  "pinData": {},
  "connections": {
    "split_out": {
      "main": [
        [
          {
            "node": "loop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "loop": {
      "main": [
        [
          {
            "node": "if",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "http",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "if": {
      "main": [
        [
          {
            "node": "tg",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "code": {
      "main": [
        [
          {
            "node": "loop",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "set1": {
      "main": [
        [
          {
            "node": "split_out",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "http": {
      "main": [
        [
          {
            "node": "code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "m1": {
      "main": [
        [
          {
            "node": "set1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Preguntas frecuentes

¿Cómo usar este flujo de trabajo?

Copie el código de configuración JSON de arriba, cree un nuevo flujo de trabajo en su instancia de n8n y seleccione "Importar desde JSON", pegue la configuración y luego modifique la configuración de credenciales según sea necesario.

¿En qué escenarios es adecuado este flujo de trabajo?

Intermedio - Comercio de criptomonedas, IA Multimodal

¿Es de pago?

Este flujo de trabajo es completamente gratuito, puede importarlo y usarlo directamente. Sin embargo, tenga en cuenta que los servicios de terceros utilizados en el flujo de trabajo (como la API de OpenAI) pueden requerir un pago por su cuenta.

Información del flujo de trabajo
Nivel de dificultad
Intermedio
Número de nodos15
Categoría2
Tipos de nodos9
Descripción de la dificultad

Adecuado para usuarios con experiencia intermedia, flujos de trabajo de complejidad media con 6-15 nodos

Enlaces externos
Ver en n8n.io

Compartir este flujo de trabajo

Categorías

Categorías: 34