Système d'alerte RSI de cryptomonnaie avec EODHD, Telegram et graphiques TradingView

Intermédiaire

Ceci est unCrypto Trading, Multimodal AIworkflow d'automatisation du domainecontenant 15 nœuds.Utilise principalement des nœuds comme If, Set, Code, SplitOut, Telegram. Système d'alerte RSI de cryptomonnaie intégrant EODHD, Telegram et les graphiques TradingView

Prérequis
  • Token Bot Telegram
  • Peut nécessiter les informations d'identification d'authentification de l'API cible
Aperçu du workflow
Visualisation des connexions entre les nœuds, avec support du zoom et du déplacement
Exporter le workflow
Copiez la configuration JSON suivante dans n8n pour importer et utiliser ce workflow
{
  "meta": {
    "instanceId": "c2b1f8c1d4a74a0cb8a1f1d3b7b0e7c1b9d2f5a8f3e44c9c93a1e2f4a6b7c8d9",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "m1",
      "name": "Lors du clic sur 'Exécuter le workflow'",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -520,
        -160
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "note_overview",
      "name": "Note adhésive — Aperçu",
      "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": "Modifier les champs (watchlist)",
      "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": "Note adhésive — Watchlist",
      "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": "Diviser les données",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -80,
        -160
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "symbol"
      },
      "typeVersion": 1
    },
    {
      "id": "note_split",
      "name": "Note adhésive — Diviser les données",
      "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": "Boucler sur les éléments",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        140,
        -160
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "note_loop",
      "name": "Note adhésive — Boucle",
      "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 Requête (EODHD intraday 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": "Note adhésive — 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": "Code (RSI + message)",
      "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": "Note adhésive — Code",
      "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 (signal détecté ?)",
      "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": "Envoyer un message texte",
      "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": "Note adhésive — 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
          }
        ]
      ]
    }
  }
}
Foire aux questions

Comment utiliser ce workflow ?

Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.

Dans quelles scénarios ce workflow est-il adapté ?

Intermédiaire - Trading crypto, IA Multimodale

Est-ce payant ?

Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.

Informations sur le workflow
Niveau de difficulté
Intermédiaire
Nombre de nœuds15
Catégorie2
Types de nœuds9
Description de la difficulté

Adapté aux utilisateurs expérimentés, avec des workflows de complexité moyenne contenant 6-15 nœuds

Liens externes
Voir sur n8n.io

Partager ce workflow

Catégories

Catégories: 34