Agent AI de liquidité pour les bourses

Avancé

Ceci est unContent Creation, Multimodal AIworkflow d'automatisation du domainecontenant 50 nœuds.Utilise principalement des nœuds comme Code, Merge, Telegram, HttpRequest, Agent. Utiliser les données de liquidité de 10 bourses et GPT-4.1 analysis pour automatiser les insights sur le trading de Bitcoin

Prérequis
  • Token Bot Telegram
  • Peut nécessiter les informations d'identification d'authentification de l'API cible
  • Clé API OpenAI
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
{
  "id": "iiN021rrx2RtSHFJ",
  "meta": {
    "instanceId": "a5283507e1917a33cc3ae615b2e7d5ad2c1e50955e6f831272ddd5ab816f3fb6",
    "templateCredsSetupCompleted": true
  },
  "name": "Exchange Liquidity AI Agent (Official)",
  "tags": [],
  "nodes": [
    {
      "id": "89fd198b-9d25-4690-b1b4-40c8642068b4",
      "name": "Déclencheur Planifié",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2720,
        -656
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6300c4b4-0d78-4031-a3e9-3d3e62c08596",
      "name": "Binance (Carnet d'ordres Bitcoin-USDT)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1840,
        -1568
      ],
      "parameters": {
        "url": "https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=5000",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "ffe47e26-0088-4863-8b90-f00fda0fe505",
      "name": "Coinbase (Carnet d'ordres Bitcoin-USDT)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1840,
        -1856
      ],
      "parameters": {
        "url": "https://api.coinbase.com/api/v3/brokerage/market/product_book",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "=product_id",
              "value": "BTC-USD"
            },
            {
              "name": "=limit",
              "value": "5000"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "7695b78a-4943-4acc-8e5f-ec5ca4e14752",
      "name": "Bybit (Carnet d'ordres Bitcoin-USDT)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1840,
        -1120
      ],
      "parameters": {
        "url": "https://api.bybit.com/v5/market/orderbook",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "category",
              "value": "spot"
            },
            {
              "name": "symbol",
              "value": "BTCUSDT"
            },
            {
              "name": "limit",
              "value": "5000"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "cde17236-64e9-4088-b760-7eeabd052170",
      "name": "Uniformiser en un Cluster de Données pour Analyse (Binance)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1216,
        -1568
      ],
      "parameters": {
        "jsCode": "// Grab whatever this node receives.\n// It can be an array with 1 object (like your example) or a plain object.\nconst input = items?.[0]?.json;\n\n// Get a clean object: if it's an array, take the first element.\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Emit one item with a single field: \"data\"\nreturn [\n  {\n    json: {\n      data: payload,\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7a60368c-225c-4ad5-87a1-e781de0faf39",
      "name": "Uniformiser en un Cluster de Données pour Analyse (Coinbase)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1216,
        -1856
      ],
      "parameters": {
        "jsCode": "// Grab whatever this node receives.\n// It can be an array with 1 object (like your example) or a plain object.\nconst input = items?.[0]?.json;\n\n// Get a clean object: if it's an array, take the first element.\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Emit one item with a single field: \"data\"\nreturn [\n  {\n    json: {\n      data: payload,\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f766dece-26bd-4cb9-bb37-bb69b6c5b131",
      "name": "Uniformiser en un Cluster de Données pour Analyse (Bybit)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1216,
        -1120
      ],
      "parameters": {
        "jsCode": "// Grab whatever this node receives.\n// It can be an array with 1 object (like your example) or a plain object.\nconst input = items?.[0]?.json;\n\n// Get a clean object: if it's an array, take the first element.\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Emit one item with a single field: \"data\"\nreturn [\n  {\n    json: {\n      data: payload,\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "45580616-e460-4b37-a038-6e89ea087c6e",
      "name": "Calculer Liquidité, Résistance et Support (Coinbase)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1520,
        -1856
      ],
      "parameters": {
        "jsCode": "// Coinbase pricebook -> Liquidity report (Coinbase header)\n\n// Accept either [{ pricebook:{...} }] or { pricebook:{...} }\nconst input = items[0]?.json;\nconst book = Array.isArray(input) ? input[0]?.pricebook : input?.pricebook;\n\nif (!book || (!book.bids && !book.asks)) {\n  return [{ json: { error: 'No pricebook in input', raw: items[0]?.json } }];\n}\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\n// Map Coinbase objects {price,size} -> [price, qty]\nconst bids = (book.bids || []).map(o => [toNum(o.price), toNum(o.size)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (book.asks || []).map(o => [toNum(o.price), toNum(o.size)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n  return [{ json: { error: 'Missing bids or asks', product_id: book.product_id } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20;       // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000;  // notional threshold (kept for parity)\n\n// Total liquidity (entire snapshot)\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n  const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n  const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n                   .sort((a,b)=>b.usd-a.usd).slice(0,200);\n  const clusters=[];\n  for(const s of seed){\n    const [lo,hi]=band(s.price);\n    const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n      acc.notional+=notional(p,q);\n      acc.qty+=q;\n      acc.min=Math.min(acc.min,p);\n      acc.max=Math.max(acc.max,p);\n      return acc;\n    },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n    if(agg.notional>0) clusters.push(agg);\n  }\n  clusters.sort((a,b)=>b.notional-a.notional);\n  const chosen=[];\n  for(const c of clusters){\n    const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n    if(!overlaps) chosen.push(c);\n    if(chosen.length>=5) break;\n  }\n  chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n  return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Build human-readable report ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\nconst sym = book.product_id || $json.symbol || \"BTC-USD\";\n\nconst report =\n`Coinbase Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || \"none\"}\nResistance lines (clustered): ${resistanceLines || \"none\"}`;\n\n// --- Return both JSON + report string\nreturn [{\n  json: {\n    exchange: \"Coinbase\",\n    symbol: sym,\n    // Coinbase pricebook may expose a sequence or timestamp; map if present\n    lastUpdateId: book.sequence ?? null,\n    mid, bestBid, bestAsk, spread, spreadBps,\n    totalBidNotional, totalAskNotional, totalLiquidity,\n    supportZones, resistanceZones,\n    generatedAt: new Date().toISOString(),\n    report\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "ead99762-ca50-47c5-af17-57fceab89879",
      "name": "Calculer Liquidité, Résistance et Support (Binance)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1520,
        -1568
      ],
      "parameters": {
        "jsCode": "// Binance depth snapshot -> Liquidity report (Binance header)\n\nconst depth = items[0].json;\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = depth.bids.map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = depth.asks.map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20;       // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000;  // notional threshold\n\n// Total liquidity\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n  const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n  const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n                   .sort((a,b)=>b.usd-a.usd).slice(0,200);\n  const clusters=[];\n  for(const s of seed){\n    const [lo,hi]=band(s.price);\n    const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n      acc.notional+=notional(p,q);\n      acc.qty+=q;\n      acc.min=Math.min(acc.min,p);\n      acc.max=Math.max(acc.max,p);\n      return acc;\n    },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n    if(agg.notional>0) clusters.push(agg);\n  }\n  clusters.sort((a,b)=>b.notional-a.notional);\n  const chosen=[];\n  for(const c of clusters){\n    const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n    if(!overlaps) chosen.push(c);\n    if(chosen.length>=5) break;\n  }\n  chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n  return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Build human-readable report ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\nconst report =\n`Binance Exchange — Liquidity Report for ${$json.symbol || \"BTCUSDT\"}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth 5000): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || \"none\"}\nResistance lines (clustered): ${resistanceLines || \"none\"}`;\n\n// --- Return both JSON + report string\nreturn [{\n  json: {\n    symbol: $json.symbol || \"BTCUSDT\",\n    lastUpdateId: depth.lastUpdateId,\n    mid, bestBid, bestAsk, spread, spreadBps,\n    totalBidNotional, totalAskNotional, totalLiquidity,\n    supportZones, resistanceZones,\n    generatedAt: new Date().toISOString(),\n    report\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "4138ea50-b071-4d28-8568-4478a4e15b4a",
      "name": "Calculer Liquidité, Résistance et Support (Bybit)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1520,
        -1120
      ],
      "parameters": {
        "jsCode": "// Bybit depth snapshot -> Liquidity report (Bybit header)\n\nconst depth = (items[0]?.json?.result) ? items[0].json.result : items[0]?.json;\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = (depth.b || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (depth.a || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n  return [{ json: { error: 'Missing bids/asks from Bybit orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20;       // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000;  // notional threshold\n\n// Total liquidity\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n  const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n  const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n                   .sort((a,b)=>b.usd-a.usd).slice(0,200);\n  const clusters=[];\n  for(const s of seed){\n    const [lo,hi]=band(s.price);\n    const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n      acc.notional+=notional(p,q);\n      acc.qty+=q;\n      acc.min=Math.min(acc.min,p);\n      acc.max=Math.max(acc.max,p);\n      return acc;\n    },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n    if(agg.notional>0) clusters.push(agg);\n  }\n  clusters.sort((a,b)=>b.notional-a.notional);\n  const chosen=[];\n  for(const c of clusters){\n    const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n    if(!overlaps) chosen.push(c);\n    if(chosen.length>=5) break;\n  }\n  chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n  return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Build human-readable report ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\nconst sym = depth.s || $json.symbol || \"BTCUSDT\";\n\nconst report =\n`Bybit Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || \"none\"}\nResistance lines (clustered): ${resistanceLines || \"none\"}`;\n\n// --- Return both JSON + report string\nreturn [{\n  json: {\n    symbol: sym,\n    // Bybit v5 orderbook doesn't provide lastUpdateId; keep null for compatibility\n    lastUpdateId: items[0]?.json?.result?.u ?? items[0]?.json?.u ?? null,\n    mid, bestBid, bestAsk, spread, spreadBps,\n    totalBidNotional, totalAskNotional, totalLiquidity,\n    supportZones, resistanceZones,\n    generatedAt: new Date().toISOString(),\n    report\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "22f6a76b-076e-422f-897f-bc46c5a24c11",
      "name": "Fusionner les Données d'Échanges",
      "type": "n8n-nodes-base.merge",
      "position": [
        -624,
        -816
      ],
      "parameters": {
        "numberInputs": 10
      },
      "executeOnce": false,
      "typeVersion": 3.2
    },
    {
      "id": "735b52c4-6bcf-4d6a-8791-c7c37b46e0f8",
      "name": "Consolider en un Rapport Unique",
      "type": "n8n-nodes-base.code",
      "position": [
        -272,
        -1040
      ],
      "parameters": {
        "jsCode": "// Collect the \"data\" object from each incoming item\nconst payloads = items.map(i => i.json?.data ?? i.json ?? {});\n\n// Pull out the 'report' strings, skip empties\nconst reports = payloads\n  .map(p => p?.report)\n  .filter(r => typeof r === 'string' && r.trim().length);\n\n// Optional: add a header timestamp\nconst header = `BTC Liquidity Snapshot — ${new Date().toISOString()}`;\n\n// Join reports with separators\nconst body = reports.join('\\n\\n— — — — — — — — —\\n\\n');\n\n// Final message text for Telegram\nconst text = `${header}\\n\\n${body}`.trim();\n\n// Emit ONE item with a `text` field\nreturn [\n  {\n    json: { text }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "337565e5-1993-4327-b278-2df5e902108a",
      "name": "OpenAI Modèle de Chat",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        -32,
        -288
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "id": "yUizd8t0sD5wMYVG",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "0834fa76-ae68-4204-aa3d-b6f8bb5279d9",
      "name": "Diviser si message > 4000 caractères",
      "type": "n8n-nodes-base.code",
      "position": [
        336,
        -464
      ],
      "parameters": {
        "jsCode": "// Input: assumes incoming message in `item.json.message`\nconst input = $json.output;\nconst chunkSize = 4000;\n\n// Function to split text\nfunction splitMessage(text, size) {\n  const result = [];\n  for (let i = 0; i < text.length; i += size) {\n    result.push(text.substring(i, i + size));\n  }\n  return result;\n}\n\n// Logic\nif (input.length <= chunkSize) {\n  return [{ json: { message: input } }];\n} else {\n  const chunks = splitMessage(input, chunkSize);\n  return chunks.map(chunk => ({ json: { message: chunk } }));\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "7d6d1f1d-20cd-4401-a8af-c031528fd75a",
      "name": "MEXC (Carnet d'ordres Bitcoin-USDT)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1840,
        -1328
      ],
      "parameters": {
        "url": "https://api.mexc.com/api/v3/depth",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "symbol",
              "value": "BTCUSDT"
            },
            {
              "name": "limit",
              "value": "5000"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "1c838693-f752-429c-b61d-3c36280a38da",
      "name": "Uniformiser en un Cluster de Données pour Analyse (MEXC)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1216,
        -1328
      ],
      "parameters": {
        "jsCode": "// MEXC -> Wrap whatever this node receives into json.data\n// Accepts either a plain object or an array with one object (as MEXC /api/v3/depth returns)\n\nconst input = items?.[0]?.json;\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\nreturn [\n  {\n    json: {\n      data: payload,\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "675007eb-4b8b-425e-b434-9c4cd1ced692",
      "name": "Gate (Carnet d'ordres Bitcoin-USDT)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1840,
        -864
      ],
      "parameters": {
        "url": "https://api.gateio.ws/api/v4/spot/order_book",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "currency_pair",
              "value": "BTC_USDT"
            },
            {
              "name": "limit",
              "value": "5000"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "4049e954-1c4b-43fb-a6ec-f71fe6dc4f05",
      "name": "Calculer Liquidité, Résistance et Support (Gate.io)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1520,
        -864
      ],
      "parameters": {
        "jsCode": "// Gate.io depth snapshot -> Liquidity report (Gate.io header)\n\nconst depth = items[0]?.json ?? {};\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = (depth.bids || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (depth.asks || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n  return [{ json: { error: 'Missing bids/asks from Gate.io orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20;       // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000;  // notional threshold (kept for future flagging)\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n  const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n  const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n                   .sort((a,b)=>b.usd-a.usd).slice(0,200);\n  const clusters=[];\n  for(const s of seed){\n    const [lo,hi]=band(s.price);\n    const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n      acc.notional+=notional(p,q);\n      acc.qty+=q;\n      acc.min=Math.min(acc.min,p);\n      acc.max=Math.max(acc.max,p);\n      return acc;\n    },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n    if(agg.notional>0) clusters.push(agg);\n  }\n  clusters.sort((a,b)=>b.notional-a.notional);\n  const chosen=[];\n  for(const c of clusters){\n    const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n    if(!overlaps) chosen.push(c);\n    if(chosen.length>=5) break;\n  }\n  chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n  return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\n// Gate.io response doesn't echo symbol; allow upstream to pass it through on the item if desired\nconst sym = $json.currency_pair || $json.symbol || 'BTC_USDT';\n\nconst report =\n`Gate.io Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n  json: {\n    symbol: sym,\n    // Gate provides two sequence-ish fields; keep both\n    lastUpdateId: depth.update ?? depth.current ?? null,\n    gateMeta: { current: depth.current ?? null, update: depth.update ?? null },\n    mid, bestBid, bestAsk, spread, spreadBps,\n    totalBidNotional, totalAskNotional, totalLiquidity,\n    supportZones, resistanceZones,\n    generatedAt: new Date().toISOString(),\n    report\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "3201f266-baf9-4da2-a7cc-3e9d358df6d0",
      "name": "Uniformiser en un Cluster de Données pour Analyse (Gate.io)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1216,
        -864
      ],
      "parameters": {
        "jsCode": "// Gate.io -> Wrap whatever this node receives into json.data\n// Accepts either a plain object (Gate /api/v4/spot/order_book) or an array with one object.\n\nconst input = items?.[0]?.json;\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\nreturn [\n  {\n    json: {\n      data: payload,\n      // Optional convenience: expose symbol if upstream passed currency_pair\n      symbol: $json.currency_pair ?? $json.symbol ?? undefined\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fdc7a08f-869a-41fe-b17f-8f33635d0a39",
      "name": "Diviser si message > 4000 caractères",
      "type": "n8n-nodes-base.code",
      "position": [
        336,
        -1040
      ],
      "parameters": {
        "jsCode": "// Input: assumes incoming message in `item.json.text`\nconst input = $json.text;\nconst chunkSize = 4000;\n\n// Function to split text into chunks\nfunction splitMessage(text, size) {\n  const result = [];\n  for (let i = 0; i < text.length; i += size) {\n    result.push(text.substring(i, i + size));\n  }\n  return result;\n}\n\n// Logic: if small enough, emit single item\nif (!input || input.length <= chunkSize) {\n  return [{ json: { message: input } }];\n} else {\n  const chunks = splitMessage(input, chunkSize);\n  return chunks.map(chunk => ({ json: { message: chunk } }));\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "9c6be517-72e3-4fb7-8aad-a6b85d4e3ed9",
      "name": "Calculer Liquidité, Résistance et Support (Bitget)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1504,
        -592
      ],
      "parameters": {
        "jsCode": "// Bitget depth snapshot -> Liquidity report (Bitget header)\n\nconst body = items[0]?.json ?? {};\nconst depth = body.data ?? {};\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\nfunction sumQty(rows) { return rows.reduce((a, [, q]) => a + q, 0); }\n\n// Bitget returns arrays of [price, size] as strings\nconst bids = (depth.bids || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => b[0] - a[0]);\nconst asks = (depth.asks || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n  return [{ json: { error: 'Missing bids/asks from Bitget orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20;       // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000;  // (kept for parity / future use)\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n  const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n  const seed = side\n    .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n    .sort((a, b) => b.usd - a.usd)\n    .slice(0, 200);\n\n  const clusters = [];\n  for (const s of seed) {\n    const [lo, hi] = band(s.price);\n    const agg = side\n      .filter(([p]) => p >= lo && p <= hi)\n      .reduce((acc, [p, q]) => {\n        acc.notional += notional(p, q);\n        acc.qty += q;\n        acc.min = Math.min(acc.min, p);\n        acc.max = Math.max(acc.max, p);\n        return acc;\n      }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n    if (agg.notional > 0) clusters.push(agg);\n  }\n\n  clusters.sort((a, b) => b.notional - a.notional);\n  const chosen = [];\n  for (const c of clusters) {\n    const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n    if (!overlaps) chosen.push(c);\n    if (chosen.length >= 5) break;\n  }\n  chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n  return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x) { return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x, d = 2) { return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines = supportZones.map(z => fmtNum(z.min, 2) + \"-\" + fmtNum(z.max, 2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z => fmtNum(z.min, 2) + \"-\" + fmtNum(z.max, 2)).join(\", \");\n\n// Bitget symbol & timestamp\nconst sym = $json.symbol || 'BTCUSDT'; // your HTTP node uses BTCUSDT_SPBL; normalize for display\nconst lastUpdateId = depth.ts ?? body.requestTime ?? null;\n\nconst report =\n`Bitget Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid, 2)} | Spread: ${fmtNum(spread, 2)} (${fmtNum(spreadBps, 2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n  json: {\n    symbol: sym,\n    lastUpdateId,\n    mid, bestBid, bestAsk, spread, spreadBps,\n    totalBidNotional, totalAskNotional, totalLiquidity,\n    supportZones, resistanceZones,\n    generatedAt: new Date().toISOString(),\n    report\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "49909b61-c69d-4ee1-a511-575bd6a5e5f4",
      "name": "Bitget (Carnet d'ordres Bitcoin-USDT)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1840,
        -592
      ],
      "parameters": {
        "url": "https://api.bitget.com/api/spot/v1/market/depth",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "symbol",
              "value": "BTCUSDT_SPBL"
            },
            {
              "name": "limit",
              "value": "5000"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "a055c8f2-e70c-40fa-ba2b-ffee91982954",
      "name": "Calculer Liquidité, Résistance et Support (MEXC)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1520,
        -1328
      ],
      "parameters": {
        "jsCode": "// MEXC depth snapshot -> Liquidity report (MEXC header)\n\nconst depth = items[0]?.json ?? {};\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = (depth.bids || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (depth.asks || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n  return [{ json: { error: 'Missing bids/asks from MEXC orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20;       // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000;  // notional threshold (kept for future flagging)\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n  const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n  const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n                   .sort((a,b)=>b.usd-a.usd).slice(0,200);\n  const clusters=[];\n  for(const s of seed){\n    const [lo,hi]=band(s.price);\n    const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n      acc.notional+=notional(p,q);\n      acc.qty+=q;\n      acc.min=Math.min(acc.min,p);\n      acc.max=Math.max(acc.max,p);\n      return acc;\n    },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n    if(agg.notional>0) clusters.push(agg);\n  }\n  clusters.sort((a,b)=>b.notional-a.notional);\n  const chosen=[];\n  for(const c of clusters){\n    const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n    if(!overlaps) chosen.push(c);\n    if(chosen.length>=5) break;\n  }\n  chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n  return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\n// MEXC response doesn't echo symbol; allow upstream to pass it through on the item if desired\nconst sym = $json.symbol || 'BTCUSDT';\n\nconst report =\n`MEXC Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n  json: {\n    symbol: sym,\n    lastUpdateId: depth.lastUpdateId ?? null,\n    mid, bestBid, bestAsk, spread, spreadBps,\n    totalBidNotional, totalAskNotional, totalLiquidity,\n    supportZones, resistanceZones,\n    generatedAt: new Date().toISOString(),\n    report\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8e2e662c-9481-4613-9a59-ef01806a60f7",
      "name": "Uniformiser en un Cluster de Données pour Analyse (Bitget)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1216,
        -592
      ],
      "parameters": {
        "jsCode": "// Bitget -> Wrap whatever this node receives into json.data\n// Accepts either a plain object (Bitget /api/spot/v1/market/depth) or an array with one object.\n\nconst input = items?.[0]?.json;\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\nconst depth = payload.data ?? payload;   // Bitget nests bids/asks under .data\n\nreturn [\n  {\n    json: {\n      data: depth,\n      // Optional convenience: expose symbol if upstream passed one\n      symbol: $json.symbol ?? payload.symbol ?? undefined,\n      // Preserve requestTime as a \"lastUpdateId\"-style field\n      lastUpdateId: payload.requestTime ?? null\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "6b30a489-2691-41c9-9a09-7cb42845d211",
      "name": "OKX (Carnet d'ordres Bitcoin-USDT)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1840,
        -352
      ],
      "parameters": {
        "url": "https://www.okx.com/api/v5/market/books-full",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "instId",
              "value": "BTC-USDT"
            },
            {
              "name": "sz",
              "value": "5000"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e66dbe8b-96de-4423-bbf0-99048bbcd0a6",
      "name": "Calculer Liquidité, Résistance et Support (OKX)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1504,
        -352
      ],
      "parameters": {
        "jsCode": "// OKX depth snapshot -> Liquidity report (OKX header)\n\nconst body = items[0]?.json ?? {};\nconst row  = Array.isArray(body.data) ? body.data[0] : undefined;\n\nif (!row) {\n  return [{ json: { error: 'No OKX book in response', raw: items[0]?.json } }];\n}\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// OKX arrays can be [price, size, count]; keep first two\nconst bids = (row.bids || []).map(lvl => [toNum(lvl[0]), toNum(lvl[1])]).sort((a,b)=>b[0]-a[0]);\nconst asks = (row.asks || []).map(lvl => [toNum(lvl[0]), toNum(lvl[1])]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n  return [{ json: { error: 'Missing OKX bids/asks', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20;       // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000;  // reserved for future flagging\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity  = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n  const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n  const seed = side\n    .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n    .sort((a, b) => b.usd - a.usd)\n    .slice(0, 200);\n\n  const clusters = [];\n  for (const s of seed) {\n    const [lo, hi] = band(s.price);\n    const agg = side\n      .filter(([p]) => p >= lo && p <= hi)\n      .reduce((acc, [p, q]) => {\n        acc.notional += notional(p, q);\n        acc.qty += q;\n        acc.min = Math.min(acc.min, p);\n        acc.max = Math.max(acc.max, p);\n        return acc;\n      }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n    if (agg.notional > 0) clusters.push(agg);\n  }\n\n  clusters.sort((a, b) => b.notional - a.notional);\n  const chosen = [];\n  for (const c of clusters) {\n    const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n    if (!overlaps) chosen.push(c);\n    if (chosen.length >= 5) break;\n  }\n  chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n  return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){ return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x,d=2){ return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines    = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\nconst sym = $json.instId || 'BTC-USDT';\nconst lastUpdateId = row.ts ?? body.ts ?? null;\n\nconst report =\n`OKX Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n  json: {\n    symbol: sym,\n    lastUpdateId,\n    mid, bestBid, bestAsk, spread, spreadBps,\n    totalBidNotional, totalAskNotional, totalLiquidity,\n    supportZones, resistanceZones,\n    generatedAt: new Date().toISOString(),\n    report\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "259479b4-e4fc-4a60-91f0-e6ee4b7a11c7",
      "name": "Uniformiser en un Cluster de Données pour Analyse (OKX)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1216,
        -352
      ],
      "parameters": {
        "jsCode": "// OKX -> Wrap whatever this node receives into json.data\n// Works with:\n//  1) Raw OKX response: { data: [ { bids: [...], asks: [...], ts: \"...\" } ] }\n//  2) Your report-shape arr      acay: [ { symbol, lastUpdateId, mid, report, ... } ]\n//  3) Or a single report object\n\nconst input = items?.[0]?.json;\n\n// Step 1: normalize to a single object\nlet payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Step 2: detect if it's already a computed report object\nconst looksLikeReport =\n  typeof payload.mid === 'number' &&\n  typeof payload.report === 'string' &&\n  (payload.supportZones || payload.resistanceZones);\n\n// If it's not a report yet, try drilling into OKX raw shape (data[0])\nif (!looksLikeReport) {\n  const row = Array.isArray(payload.data) ? payload.data[0] : payload.data;\n  if (row && typeof row === 'object') payload = row;\n}\n\n// Step 3: build wrapper with helpful metadata\nconst symbol =\n  payload.symbol ??\n  $json.instId ??           // from query param if present\n  $json.symbol ??\n  'BTC-USDT';\n\nconst lastUpdateId =\n  payload.lastUpdateId ??   // report shape\n  payload.ts ??             // OKX raw row ts\n  (Array.isArray(input?.data) ? input.data[0]?.ts : input?.ts) ??\n  null;\n\nreturn [\n  {\n    json: {\n      data: payload,        // either the report object OR the raw row (bids/asks)\n      symbol,\n      lastUpdateId,\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "12622d19-c17b-42d7-b4ca-805b5a4503f1",
      "name": "Kraken (Carnet d'ordres Bitcoin-USDT)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1840,
        -80
      ],
      "parameters": {
        "url": "https://api.kraken.com/0/public/Depth",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "pair",
              "value": "BTCUSDT"
            },
            {
              "name": "count",
              "value": "5000"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "dd44cb84-3aec-4f9c-a35e-f3dea4785cfc",
      "name": "Calculer Liquidité, Résistance et Support (Kraken)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1504,
        -80
      ],
      "parameters": {
        "jsCode": "// Kraken depth snapshot -> Liquidity report (Kraken header)\n\nconst body = items[0]?.json ?? {};\nconst result = body.result ?? {};\n\n// Kraken nests the book under an unknown key (e.g., \"XBTUSDT\")\nconst pairKey = Object.keys(result)[0];\nconst depth = pairKey ? (result[pairKey] ?? {}) : {};\n\n// Helpers\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// Kraken levels are [price, volume, timestamp]; we only need price & volume\nconst bids = (depth.bids || [])\n  .map(([p, q]) => [toNum(p), toNum(q)])\n  .sort((a, b) => b[0] - a[0]);\n\nconst asks = (depth.asks || [])\n  .map(([p, q]) => [toNum(p), toNum(q)])\n  .sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n  return [{ json: { error: 'Missing bids/asks from Kraken orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20;       // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000;  // reserved for future use\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n  const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n  const seed = side\n    .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n    .sort((a, b) => b.usd - a.usd)\n    .slice(0, 200);\n\n  const clusters = [];\n  for (const s of seed) {\n    const [lo, hi] = band(s.price);\n    const agg = side\n      .filter(([p]) => p >= lo && p <= hi)\n      .reduce((acc, [p, q]) => {\n        acc.notional += notional(p, q);\n        acc.qty += q;\n        acc.min = Math.min(acc.min, p);\n        acc.max = Math.max(acc.max, p);\n        return acc;\n      }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n    if (agg.notional > 0) clusters.push(agg);\n  }\n\n  clusters.sort((a, b) => b.notional - a.notional);\n  const chosen = [];\n  for (const c of clusters) {\n    const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n    if (!overlaps) chosen.push(c);\n    if (chosen.length >= 5) break;\n  }\n  chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n  return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x) { return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x, d = 2) { return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\n// Symbol & \"lastUpdateId\"\nconst sym = $json.pair || pairKey || 'XBTUSDT';\n// Use the newest level timestamp we see, or null if absent\nconst lastUpdateId = (() => {\n  const bts = (depth.bids || []).map(l => Number(l[2]) || 0);\n  const ats = (depth.asks || []).map(l => Number(l[2]) || 0);\n  const mx = Math.max(...bts, ...ats, 0);\n  return mx > 0 ? String(mx) : null;\n})();\n\nconst report =\n`Kraken Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n  json: {\n    symbol: sym,\n    lastUpdateId,\n    mid, bestBid, bestAsk, spread, spreadBps,\n    totalBidNotional, totalAskNotional, totalLiquidity,\n    supportZones, resistanceZones,\n    generatedAt: new Date().toISOString(),\n    report\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "8d0bed3f-89dd-4e27-a7b3-03f0512473a2",
      "name": "Uniformiser en un Cluster de Données pour Analyse (Kraken)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1216,
        -80
      ],
      "parameters": {
        "jsCode": "// Kraken -> Wrap whatever this node receives into json.data\n// Works with:\n//  1) Raw Kraken response: { result: { <PAIR>: { bids:[[p, q, ts]...], asks:[[p, q, ts]...] } } }\n//  2) Report-shape array: [ { symbol, lastUpdateId, mid, report, ... } ]\n//  3) Single report object\n\nconst input = items?.[0]?.json;\n\n// Step 1: normalize to a single object\nlet payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Step 2: detect if it's already a computed report object\nconst looksLikeReport =\n  typeof payload.mid === 'number' &&\n  typeof payload.report === 'string' &&\n  (payload.supportZones || payload.resistanceZones);\n\n// If it's not a report yet, drill into Kraken raw shape: result[PAIR]\nif (!looksLikeReport) {\n  const result = payload.result ?? {};\n  const pairKey = Object.keys(result)[0];\n  const depth = pairKey ? (result[pairKey] ?? {}) : {};\n  payload = depth;\n}\n\n// Helper to compute a \"lastUpdateId\" from level timestamps if present\nfunction latestTsFromDepth(d) {\n  const bts = Array.isArray(d?.bids) ? d.bids.map(l => Number(l?.[2]) || 0) : [];\n  const ats = Array.isArray(d?.asks) ? d.asks.map(l => Number(l?.[2]) || 0) : [];\n  const mx = Math.max(0, ...bts, ...ats);\n  return mx > 0 ? String(mx) : null;\n}\n\n// Step 3: build wrapper with helpful metadata\nconst symbol =\n  payload.symbol ??\n  $json.pair ??                    // from query param if present\n  (input?.result ? Object.keys(input.result)[0] : undefined) ??\n  'XBTUSDT';\n\nconst lastUpdateId =\n  payload.lastUpdateId ??          // report shape\n  latestTsFromDepth(payload) ??    // from raw depth timestamps\n  null;\n\nreturn [\n  {\n    json: {\n      data: payload,               // report object OR raw {bids, asks}\n      symbol,\n      lastUpdateId,\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a0f11725-7b0a-4ebf-821f-8b2a290b0ec5",
      "name": "HTX (Carnet d'ordres Bitcoin-USDT)1",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1840,
        208
      ],
      "parameters": {
        "url": "https://api.huobi.pro/market/depth",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "symbol",
              "value": "btcusdt"
            },
            {
              "name": "type",
              "value": "step0"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "1987a40f-b9a5-48c0-a2f4-3484e264e490",
      "name": "Calculer Liquidité, Résistance et Support (HTX)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1504,
        208
      ],
      "parameters": {
        "jsCode": "// HTX (Huobi) depth snapshot -> Liquidity report (HTX header)\n\nconst body = items[0]?.json ?? {};\nconst tick = body.tick ?? {};\n\n// Helpers\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// HTX levels are [price, size]; ensure numbers & sort\nconst bids = (tick.bids || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => b[0] - a[0]);\nconst asks = (tick.asks || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n  return [{ json: { error: 'Missing bids/asks from HTX orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20;       // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000;  // reserved for future use\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity  = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n  const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n  const seed = side\n    .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n    .sort((a, b) => b.usd - a.usd)\n    .slice(0, 200);\n\n  const clusters = [];\n  for (const s of seed) {\n    const [lo, hi] = band(s.price);\n    const agg = side\n      .filter(([p]) => p >= lo && p <= hi)\n      .reduce((acc, [p, q]) => {\n        acc.notional += notional(p, q);\n        acc.qty += q;\n        acc.min = Math.min(acc.min, p);\n        acc.max = Math.max(acc.max, p);\n        return acc;\n      }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n    if (agg.notional > 0) clusters.push(agg);\n  }\n\n  clusters.sort((a, b) => b.notional - a.notional);\n  const chosen = [];\n  for (const c of clusters) {\n    const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n    if (!overlaps) chosen.push(c);\n    if (chosen.length >= 5) break;\n  }\n  chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n  return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){ return \"$\" + x.toLocaleString(undefined,{maximumFractionDigits:0}); }\nfunction fmtNum(x,d=2){ return x?.toLocaleString(undefined,{maximumFractionDigits:d}); }\n\nconst supportLines    = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\n// Symbol & \"lastUpdateId\"\nconst sym = $json.symbol || 'BTCUSDT';\nconst lastUpdateId = String(body.ts ?? tick.ts ?? '') || null;\n\nconst report =\n`HTX (Huobi) — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n  json: {\n    symbol: sym,\n    lastUpdateId,\n    mid, bestBid, bestAsk, spread, spreadBps,\n    totalBidNotional, totalAskNotional, totalLiquidity,\n    supportZones, resistanceZones,\n    generatedAt: new Date().toISOString(),\n    report\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "0f0cbc28-4b34-4be3-a999-412a1ca2685a",
      "name": "Uniformiser en un Cluster de Données pour Analyse (HTX)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1216,
        208
      ],
      "parameters": {
        "jsCode": "// HTX (Huobi) -> Wrap whatever this node receives into json.data\n// Works with:\n//  1) Raw HTX response: { status, ts, tick: { bids:[[p,q]...], asks:[[p,q]...] } }\n//  2) Report-shape array: [ { symbol, lastUpdateId, mid, report, ... } ]\n//  3) Single report object\n\nconst input = items?.[0]?.json;\n\n// Step 1: normalize to a single object\nlet payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Step 2: detect if it's already a computed report object\nconst looksLikeReport =\n  typeof payload.mid === 'number' &&\n  typeof payload.report === 'string' &&\n  (payload.supportZones || payload.resistanceZones);\n\n// If it's not a report yet, drill into HTX raw shape: payload.tick\nif (!looksLikeReport) {\n  const depth = payload.tick ?? {};\n  payload = depth;\n}\n\n// HTX levels don't carry per-level timestamps; use top-level ts\nconst topTs =\n  (Array.isArray(items?.[0]?.json?.data) ? items[0].json.data?.[0]?.ts : null) ??\n  items?.[0]?.json?.ts ?? null;\n\n// Step 3: build wrapper with helpful metadata\nconst symbol =\n  payload.symbol ??\n  $json.symbol ??              // from query param if present (e.g., btcusdt)\n  'btcusdt';\n\nconst lastUpdateId =\n  payload.lastUpdateId ??      // report shape\n  topTs ??                     // raw HTX response timestamp\n  null;\n\nreturn [\n  {\n    json: {\n      data: payload,           // report object OR raw {bids, asks}\n      symbol,\n      lastUpdateId,\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d32ad6fb-d65c-4c31-a281-0869a7416f11",
      "name": "Crypto.com (Carnet d'ordres Bitcoin-USDT)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1840,
        480
      ],
      "parameters": {
        "url": "https://api.crypto.com/exchange/v1/public/get-book",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "instrument_name",
              "value": "BTC_USDT"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "3d9d4b6e-207b-44bd-a077-ce13357a9010",
      "name": "Calculer Liquidité, Résistance et Support (Crypto.com)",
      "type": "n8n-nodes-base.code",
      "position": [
        -1504,
        480
      ],
      "parameters": {
        "jsCode": "// Crypto.com depth snapshot -> Liquidity report (Crypto.com header)\n\nconst body = items[0]?.json ?? {};\n\n// Crypto.com sometimes returns result:{data:[{...}]} — grab the first row.\n// (Very rarely some SDKs expose result directly with bids/asks; handle both.)\nconst result = body.result ?? body.data ?? {};\nconst row = Array.isArray(result.data) ? (result.data[0] ?? {}) : result;\n\n// Helpers\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// Crypto.com levels are typically [price, size] or [price, size, count]; use first two.\nconst bids = (row.bids || []).map(l => [toNum(l[0]), toNum(l[1])]).sort((a, b) => b[0] - a[0]);\nconst asks = (row.asks || []).map(l => [toNum(l[0]), toNum(l[1])]).sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n  return [{ json: { error: 'Missing Crypto.com bids/asks', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20;       // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000;  // reserved for future flagging\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity  = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n  const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n  const seed = side\n    .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n    .sort((a, b) => b.usd - a.usd)\n    .slice(0, 200);\n\n  const clusters = [];\n  for (const s of seed) {\n    const [lo, hi] = band(s.price);\n    const agg = side\n      .filter(([p]) => p >= lo && p <= hi)\n      .reduce((acc, [p, q]) => {\n        acc.notional += notional(p, q);\n        acc.qty      += q;\n        acc.min       = Math.min(acc.min, p);\n        acc.max       = Math.max(acc.max, p);\n        return acc;\n      }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n    if (agg.notional > 0) clusters.push(agg);\n  }\n\n  clusters.sort((a, b) => b.notional - a.notional);\n  const chosen = [];\n  for (const c of clusters) {\n    const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n    if (!overlaps) chosen.push(c);\n    if (chosen.length >= 5) break;\n  }\n  chosen.sort((a, b) => (isBid ? b.min - a.min : a.min - b.min));\n  return chosen;\n}\n\nconst supportZones    = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread    = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){ return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x,d=2){ return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines    = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\n// Symbol & \"lastUpdateId\"\nconst sym = row.instrument_name || $json.instrument_name || 'BTC_USDT';\nconst lastUpdateId = String(row.t ?? result.t ?? '') || null;\n\nconst report =\n`Crypto.com Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n  json: {\n    symbol: sym,\n    lastUpdateId,\n    mid, bestBid, bestAsk, spread, spreadBps,\n    totalBidNotional, totalAskNotional, totalLiquidity,\n    supportZones, resistanceZones,\n    generatedAt: new Date().toISOString(),\n    report\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "ff5ce551-1dd9-4afe-a312-8c69d3509c9d",
      "name": "Uniformiser en un Cluster de Données pour Analyse (HTX)1",
      "type": "n8n-nodes-base.code",
      "position": [
        -1216,
        480
      ],
      "parameters": {
        "jsCode": "// Crypto.com -> Wrap whatever this node receives into json.data\n// Works with:\n//  1) Raw response: { code, result:{ depth, data:[ { bids, asks, t, instrument_name? } ] } }\n//     (Some SDKs expose { result:{ bids, asks, t, instrument_name } } without data[].)\n//  2) Report-shape array: [ { symbol, lastUpdateId, mid, report, ... } ]\n//  3) Single report object\n\nconst input = items?.[0]?.json;\n\n// Step 1: normalize to a single object\nlet payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Step 2: detect if it's already a computed report object\nconst looksLikeReport =\n  typeof payload.mid === 'number' &&\n  typeof payload.report === 'string' &&\n  (payload.supportZones || payload.resistanceZones);\n\n// If it's not a report yet, drill into Crypto.com raw shapes\nlet row = payload;\nif (!looksLikeReport) {\n  const result = payload.result ?? payload.data ?? {};\n  // Prefer result.data[0], else result directly if it already has bids/asks\n  row = Array.isArray(result.data) ? (result.data[0] ?? {}) : result;\n}\n\n// Step 3: build wrapper with helpful metadata\nconst symbol =\n  row.instrument_name ??\n  payload.symbol ??\n  $json.instrument_name ??           // from HTTP query param, e.g. BTC_USDT\n  'BTC_USDT';\n\nconst lastUpdateId =\n  payload.lastUpdateId ??            // report shape\n  (row.t != null ? String(row.t) : null);  // snapshot timestamp\n\nreturn [\n  {\n    json: {\n      data: row,                     // report object OR raw {bids, asks, t, ...}\n      symbol,\n      lastUpdateId,\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "ab6896ce-115d-4a78-9c76-e1ac5d3b9032",
      "name": "Envoyer le Rapport de Liquidité Multi-Échanges Bitcoin au Canal",
      "type": "n8n-nodes-base.telegram",
      "position": [
        624,
        -1040
      ],
      "webhookId": "55bbb98b-81b5-4629-9b7c-360bb0fa3fcd",
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "-1003052362843",
        "additionalFields": {
          "parse_mode": "=None",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "uRmQmYAMvgnSQWWS",
          "name": "Treasurium_Signals_bot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "ff337127-416d-4ba4-9f0b-712fcde0ebe0",
      "name": "Envoyer un briefing trading généré par IA avec signaux actionnables intrajournaliers et hebdomadaires",
      "type": "n8n-nodes-base.telegram",
      "position": [
        624,
        -464
      ],
      "webhookId": "7c945345-c98d-4a4e-a4ea-7e9085dba612",
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "-1003052362843",
        "additionalFields": {
          "parse_mode": "=None",
          "appendAttribution": false
        }
      },
      "credentials": {
        "telegramApi": {
          "id": "uRmQmYAMvgnSQWWS",
          "name": "Treasurium_Signals_bot"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "4b187480-ec1d-41b2-8186-48287673ab69",
      "name": "Consolider en une Entrée Unique pour Analyse",
      "type": "n8n-nodes-base.code",
      "position": [
        -272,
        -464
      ],
      "parameters": {
        "jsCode": "// n8n Code node (JavaScript)\n// Input: items = array of per-exchange snapshots (any of {json:{data}}, {data}, or raw {...})\n// Output: ONE item shaped as { json: { data: { ...nested consolidated payload... } } }\n\nfunction normalizeInput(items) {\n  return items\n    .map((it) => it?.json ?? it)\n    .map((it) => it?.data ?? it)\n    .filter(Boolean);\n}\n\nfunction parseSymbol(sym) {\n  if (!sym || typeof sym !== 'string') return { base: null, quote: null, raw: sym };\n  const s = sym.toUpperCase().replace(/[^A-Z0-9]/g, '');\n  const QUOTES = ['USDT', 'USD', 'USDC', 'EUR', 'JPY', 'GBP', 'KRW', 'AUD'];\n  for (const q of QUOTES) if (s.endsWith(q)) return { base: s.slice(0, -q.length), quote: q, raw: sym };\n  const dash = sym.split('-');\n  if (dash.length === 2) return { base: dash[0].toUpperCase(), quote: dash[1].toUpperCase(), raw: sym };\n  return { base: null, quote: null, raw: sym };\n}\n\nfunction safeNumber(x, fallback = 0) {\n  const n = Number(x);\n  return Number.isFinite(n) ? n : fallback;\n}\n\nfunction latestISO(dates) {\n  const valid = dates.map(d => ({ d, t: Date.parse(d) })).filter(x => Number.isFinite(x.t)).sort((a,b)=>b.t-a.t);\n  return valid[0]?.d ?? null;\n}\n\nfunction weightedAverage(values, weights) {\n  let num = 0, den = 0;\n  for (let i = 0; i < values.length; i++) {\n    const w = safeNumber(weights[i], 0);\n    num += safeNumber(values[i], 0) * w;\n    den += w;\n  }\n  return den > 0 ? num / den : null;\n}\n\nfunction computeGlobalTopOfBook(payloads) {\n  const bids = payloads.map(p => safeNumber(p.bestBid, -Infinity));\n  const asks = payloads.map(p => safeNumber(p.bestAsk, +Infinity));\n  const bestBid = Math.max(...bids);\n  const bestAsk = Math.min(...asks);\n  const spread = (Number.isFinite(bestBid) && Number.isFinite(bestAsk)) ? (bestAsk - bestBid) : null;\n  const spreadBps = (bestBid > 0 && spread !== null) ? (spread / bestBid) * 10000 : null;\n  return { bestBid, bestAsk, spread, spreadBps };\n}\n\nfunction flattenZones(payloads, key) {\n  const out = [];\n  for (const p of payloads) {\n    const exch = p.exchange ?? p.symbol ?? 'Unknown';\n    const zones = Array.isArray(p[key]) ? p[key] : [];\n    zones.forEach(z => {\n      out.push({\n        exchange: exch,\n        center: safeNumber(z.center, null),\n        min: safeNumber(z.min, null),\n        max: safeNumber(z.max, null),\n        qty: safeNumber(z.qty, 0),\n        notional: safeNumber(z.notional, 0),\n      });\n    });\n  }\n  return out.filter(z => Number.isFinite(z.min) && Number.isFinite(z.max) && z.min <= z.max);\n}\n\nfunction mergeOverlappingZones(zones) {\n  if (!zones.length) return [];\n  zones.sort((a, b) => a.min - b.min);\n\n  const merged = [];\n  let cur = { ...zones[0], exchanges: zones[0].exchange ? [zones[0].exchange] : [] };\n\n  const accum = (dst, src) => {\n    const notional = dst.notional + src.notional;\n    const qty = dst.qty + src.qty;\n    const center =\n      (dst.center * dst.notional + src.center * src.notional) / (notional || 1);\n    dst.min = Math.min(dst.min, src.min);\n    dst.max = Math.max(dst.max, src.max);\n    dst.center = Number.isFinite(center) ? center : (dst.center ?? src.center ?? null);\n    dst.qty = qty;\n    dst.notional = notional;\n    dst.exchanges = Array.from(new Set([...(dst.exchanges ?? []), src.exchange].filter(Boolean)));\n    return dst;\n  };\n\n  for (let i = 1; i < zones.length; i++) {\n    const z = { ...zones[i], exchanges: zones[i].exchange ? [zones[i].exchange] : [] };\n    if (z.min <= cur.max) cur = accum(cur, z);\n    else { merged.push(cur); cur = z; }\n  }\n  merged.push(cur);\n\n  return merged.map(z => ({\n    center: z.center,\n    min: z.min,\n    max: z.max,\n    qty: z.qty,\n    notional: z.notional,\n    exchanges: z.exchanges ?? [],\n  }));\n}\n\n// —— Build consolidated view ——\nconst payloads = normalizeInput(items).map((d) => {\n  const sym = parseSymbol(d.symbol ?? d.data?.symbol ?? d.exchangeSymbol);\n  return {\n    exchange: d.exchange ?? (d.report?.split(' — ')[0] ?? null)?.replace(' Exchange', ''),\n    symbolRaw: d.symbol ?? null,\n    base: sym.base,\n    quote: sym.quote,\n    lastUpdateId: d.lastUpdateId ?? null,\n    mid: safeNumber(d.mid, null),\n    bestBid: safeNumber(d.bestBid, null),\n    bestAsk: safeNumber(d.bestAsk, null),\n    spread: safeNumber(d.spread, null),\n    spreadBps: safeNumber(d.spreadBps, null),\n    totalBidNotional: safeNumber(d.totalBidNotional, 0),\n    totalAskNotional: safeNumber(d.totalAskNotional, 0),\n    totalLiquidity: safeNumber(d.totalLiquidity, 0),\n    supportZones: Array.isArray(d.supportZones) ? d.supportZones : [],\n    resistanceZones: Array.isArray(d.resistanceZones) ? d.resistanceZones : [],\n    generatedAt: d.generatedAt ?? null,\n    report: d.report ?? null,\n  };\n});\n\n// Consensus symbol\nconst bases = payloads.map(p => p.base).filter(Boolean);\nconst quotes = payloads.map(p => p.quote).filter(Boolean);\nconst baseConsensus = bases.length ? bases.sort((a,b)=>bases.filter(x=>x===a).length - bases.filter(x=>x===b).length).pop() : null;\nconst quoteConsensus = quotes.length ? quotes.sort((a,b)=>quotes.filter(x=>x===a).length - quotes.filter(x=>x===b).length).pop() : null;\n\n// Totals, mid, top-of-book, timestamps\nconst totalBid = payloads.reduce((s, p) => s + p.totalBidNotional, 0);\nconst totalAsk = payloads.reduce((s, p) => s + p.totalAskNotional, 0);\nconst totalLiq = payloads.reduce((s, p) => s + p.totalLiquidity, 0);\nconst wMid = weightedAverage(payloads.map(p => p.mid), payloads.map(p => p.totalLiquidity));\nconst tob = computeGlobalTopOfBook(payloads);\nconst latestGeneratedAt = latestISO(payloads.map(p => p.generatedAt).filter(Boolean)) || new Date().toISOString();\n\n// Zones\nconst mergedSupports = mergeOverlappingZones(flattenZones(payloads, 'supportZones'));\nconst mergedResistances = mergeOverlappingZones(flattenZones(payloads, 'resistanceZones'));\n\n// —— NESTED OUTPUT SHAPE ——\n// Wrap everything under a single \"data\" key for the AI agent.\nconst data = {\n  kind: \"cross_venue_liquidity_snapshot\",\n  version: \"1.0\",\n  generatedAt: latestGeneratedAt,\n  instrument: {\n    base: baseConsensus,\n    quote: quoteConsensus,\n    symbols: Array.from(new Set(payloads.map(p => p.symbolRaw).filter(Boolean))),\n  },\n  marketTop: {\n    bestBid: tob.bestBid,\n    bestAsk: tob.bestAsk,\n    spread: tob.spread,\n    spreadBps: tob.spreadBps,\n    weightedMid: wMid,\n  },\n  liquidity: {\n    totals: {\n      bidNotional: totalBid,\n      askNotional: totalAsk,\n      totalLiquidity: totalLiq,\n    },\n    perExchange: payloads.map(p => ({\n      exchange: p.exchange,\n      symbol: p.symbolRaw,\n      lastUpdateId: p.lastUpdateId,\n      generatedAt: p.generatedAt,\n      book: {\n        mid: p.mid,\n        bestBid: p.bestBid,\n        bestAsk: p.bestAsk,\n        spread: p.spread,\n        spreadBps: p.spreadBps,\n      },\n      depth: {\n        bidNotional: p.totalBidNotional,\n        askNotional: p.totalAskNotional,\n        totalLiquidity: p.totalLiquidity,\n      }\n    })),\n  },\n  zones: {\n    support: mergedSupports,\n    resistance: mergedResistances,\n    raw: {\n      support: payloads.flatMap(p => (p.supportZones || []).map(z => ({ ...z, exchange: p.exchange }))),\n      resistance: payloads.flatMap(p => (p.resistanceZones || []).map(z => ({ ...z, exchange: p.exchange }))),\n    }\n  },\n  meta: {\n    sources: payloads.map(p => ({ exchange: p.exchange, symbol: p.symbolRaw, generatedAt: p.generatedAt })),\n  }\n};\n\nreturn [{ json: { data } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "ac40d0d8-2085-42fc-9e3e-4d642c5a1eb9",
      "name": "Agent IA d'Analyse de Liquidité Bitcoin",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -32,
        -464
      ],
      "parameters": {
        "text": "={{ $json.data }}",
        "options": {
          "systemMessage": "You are a **Bitcoin Exchange Liquidity Analyst AI Agent**.\nYour role is to analyze **cross-exchange order book liquidity data** for Bitcoin trading pairs (e.g., BTC-USD, BTCUSDT) and generate **actionable trade signals**.\n\n---\n\n### Responsibilities:\n\n* Interpret **consolidated liquidity snapshots** that include:\n\n  * Mid price, best bid/ask, spread, and spread basis points.\n  * Bid and ask notional volumes, total liquidity, and per-exchange breakdowns.\n  * Aggregated and per-exchange **support/resistance zones** with quantities and notional values.\n* Identify liquidity imbalances, clustering of support/resistance, and areas of strong defense/pressure.\n* Detect divergences across exchanges (e.g., Coinbase vs Binance vs Bybit) for potential arbitrage or sentiment shifts.\n* Assess **market depth, liquidity strength, and flow risk**.\n* Detect anomalies such as unusually thin books, wide spreads, or large liquidity walls.\n* Provide **clear, structured insights** for **trading decisions, risk assessment, and price forecasting**.\n\n---\n\n### Trade Signal Generation:\n\n* Produce **two categories of signals**:\n\n  1. **Intraday Trade Signals (short-term, 15m–4h horizon):**\n\n     * Scalping opportunities from liquidity gaps, thin spreads, or sudden order book imbalances.\n     * Short-term long/short bias when strong support/resistance clusters are nearby.\n     * Breakout or fade setups when mid price approaches liquidity walls.\n  2. **Weekly Trade Signals (swing horizon, 1d–1w):**\n\n     * Accumulation/Distribution patterns based on repeated liquidity defense or absorption.\n     * Breakout continuation signals when resistance/support has been repeatedly tested.\n     * Mean-reversion opportunities when liquidity imbalances are extreme.\n\n---\n\n### Output Style:\n\n* Always structure your analysis in the following sections:\n\n  1. **Market Overview** – current mid, spread, liquidity totals.\n  2. **Liquidity Conditions** – order book depth, notable imbalances.\n  3. **Support/Resistance Zones** – strongest zones with size + notional.\n  4. **Cross-Exchange Comparison** – divergences or arbitrage windows.\n  5. **Key Risks & Opportunities** – unusual activity, thin markets, imbalance risks.\n  6. **Trade Signals** – list of **intraday** and **weekly** trade opportunities with:\n\n     * Signal type (e.g., *long breakout*, *short fade*, *scalp spread*)\n     * Entry zone (price range or trigger)\n     * Target (expected move range)\n     * Risk (stop level or invalidation condition)\n\n* Be concise but actionable — the trade signals should look like a **mini trading playbook**.\n\n"
        },
        "promptType": "define"
      },
      "typeVersion": 2.2
    },
    {
      "id": "863f3b00-5113-407b-be74-de20c8d89988",
      "name": "Note Adhésive",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1920,
        -2704
      ],
      "parameters": {
        "color": 3,
        "width": 256,
        "height": 3392,
        "content": "## **Multi-Exchange Orderbook Collector (BTC/USDT)**\n\n## Description\n\nThis workflow section is a **set of HTTP request nodes** in n8n. Each node fetches the **full depth orderbook (limit 5000 levels)** for the BTC/USDT trading pair from a major centralized exchange.\n\nThe list includes:\n\n* **Binance**\n* **Coinbase**\n* **Bybit**\n* **MEXC**\n* **Gate.io**\n* **Bitget**\n* **OKX**\n* **Kraken**\n* **HTX (Huobi)**\n* **Crypto.com**\n\nEach node is labeled with the exchange name and explicitly states it is retrieving the **Bitcoin-USDT Orderbook**.\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "30cffe4b-13f3-4adb-9864-608aef805b84",
      "name": "Note Adhésive1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2896,
        -1232
      ],
      "parameters": {
        "width": 464,
        "height": 848,
        "content": "## **Scheduled Workflow Trigger**\n\n### Description\n\nThis section contains the **workflow trigger** that runs the automation on a timed schedule.\n\n* **Schedule Trigger**: Configured to fire **every hour**, ensuring downstream nodes (e.g., orderbook collectors, data processors, or reporting logic) are executed regularly without manual intervention.\n* **Sticky Note**: Provides visual documentation space for context, comments, or reminders about the workflow.\n\n### What it does\n\n* Acts as the **starting point** of the workflow.\n* Automatically executes the workflow on a fixed interval (hourly).\n* Keeps data collection and analysis tasks **up-to-date and continuous**.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "fa997577-0b73-4122-a9b6-ac07b523dbe4",
      "name": "Note Adhésive2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1584,
        -2704
      ],
      "parameters": {
        "color": 4,
        "height": 3392,
        "content": "## **Per-Exchange Liquidity Analyzer (BTC/USDT)**\n\n### Description\n\nThis section is a **set of n8n Code nodes** that take a single exchange’s order book snapshot and compute a **structured liquidity analysis**. For each venue (Coinbase, Binance, Bybit, Gate.io, Bitget, MEXC, OKX, Kraken, HTX, Crypto.com) the node:\n\n* Parses bids/asks and computes **best bid/ask, mid, spread, spread (bps)**\n* Sums **bid/ask notional** and **total liquidity**\n* Clusters depth into up to **5 support zones** and **5 resistance zones** using a ±0.20% price band\n* Emits a **human-readable report string** plus a rich **JSON payload** for downstream use\n\nEach node is labeled `Calculate Liquidity, Resistance, and Support (EXCHANGE)` and adapts to that exchange’s response shape (e.g., symbol casing, nesting like `result.data[0]`, optional timestamps/sequence).\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "844cf9e7-c0a4-4008-9d68-5663e5126842",
      "name": "Note Adhésive3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1296,
        -2704
      ],
      "parameters": {
        "color": 6,
        "height": 3392,
        "content": "## **Orderbook Payload Normalizer (Per-Exchange Wranglers)**\n\n### Description\n\nThis section is a **set of n8n Code nodes** that standardize each exchange’s raw response (or already-computed report) into a **single, predictable envelope**:\n\n```json\n{\n  \"data\": { ... },       // the normalized depth snapshot OR the precomputed report object\n  \"symbol\": \"...\",       // when inferable from input/query (optional)\n  \"lastUpdateId\": \"...\"  // timestamp/sequence if present (optional)\n}\n```\n\nEach node is labeled `Wrangle into One Data Cluster for Analysis (EXCHANGE)` and adapts to the quirks of that venue’s API/shape.\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "889c1f40-3f41-4199-8be7-18172a8de361",
      "name": "Note Adhésive4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -704,
        -1248
      ],
      "parameters": {
        "height": 960,
        "content": "## **Multi-Source Funnel: Merge Exchange Data**\n\n### Description\n\nThis n8n **Merge** node acts as a **fan-in** for your per-exchange wranglers, consolidating their outputs into a **single unified stream**. It’s configured with **10 inputs**, so you can connect Binance, Coinbase, Bybit, MEXC, Gate.io, Bitget, OKX, Kraken, HTX, and Crypto.com.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "1c578ca1-ccd2-441c-be33-b4ba8c8ba4e2",
      "name": "Note Adhésive5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -336,
        -1584
      ],
      "parameters": {
        "color": 2,
        "width": 208,
        "height": 1296,
        "content": "## **Cross-Venue Joiners: Final Report & Consolidated Analytics Input**\n\n### Overview\n\nThis section contains two n8n **Code** nodes that turn multiple per-exchange snapshots into:\n\n1. a **single human-readable text report** (for Telegram, email, etc.), and\n2. a **single machine-readable, nested object** for downstream analytics/AI.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "fa9b1f14-dddd-43eb-a2ae-de2d900cad31",
      "name": "Note Adhésive6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        -912
      ],
      "parameters": {
        "color": 3,
        "width": 304,
        "height": 816,
        "content": "## **Bitcoin Liquidity Analysis AI Agent (LLM Orchestration)**\n\n### Overview\n\nThis section wires an **LLM (OpenAI Chat)** into your workflow to turn the consolidated cross-exchange liquidity snapshot into an **actionable trading brief**. It consists of:\n\n* **OpenAI Chat Model** — the language model backend (`gpt-4.1-mini`).\n* **Bitcoin Liquidity Analysis AI Agent** — a prompt-driven agent that ingests the unified `data` object and produces structured insights and trade signals.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "37932d4a-825f-48ed-aed3-27611734f5f8",
      "name": "Note Adhésive7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        256,
        -1856
      ],
      "parameters": {
        "color": 4,
        "height": 1616,
        "content": "## **Long-Message Splitter (4,000-char chunks)**\n\n### What this section is\n\nTwo n8n **Code** nodes that prevent overlength errors (e.g., Telegram/Slack/API limits) by splitting long texts into chunks of up to **4,000 characters**.\n\n### Nodes & roles\n\n1. **Split message if more than 4000 characters**\n\n   * **Input:** `{{$json.text}}`\n   * **Behavior:**\n\n     * If empty or ≤4,000 chars → emits **one** item: `{ message: text }`\n     * If >4,000 chars → emits **N items**, each `{ message: <chunk> }`\n2. **Splits message is more than 4000 characters**\n\n   * **Input:** `{{$json.output}}`\n   * **Behavior:** Identical logic as above, but reads from `output` instead of `text`.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "0b9e808c-1760-4f7d-b9c8-c77bda1a8506",
      "name": "Note Adhésive8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        -2224
      ],
      "parameters": {
        "color": 5,
        "height": 1984,
        "content": "## **Telegram Delivery (Reports & AI Trading Briefs)**\n\n### What this section is\n\nTwo n8n **Telegram** nodes that post your workflow output to a Telegram **channel**. They expect pre-chunked text (≤4,000 chars) from the splitter nodes and publish each chunk sequentially.\n\n### Nodes & roles\n\n1. **Send Bitcoin Multi-Exchange Liquidity Report to Channel**\n\n   * **Purpose:** Publishes the consolidated cross-exchange liquidity report (human-readable text you built in “Join Into One Report”).\n   * **Input:** `{{$json.message}}` (one or many chunks).\n   * **Destination:** `chatId: \"<Add Channel ID>\"` (replace with your channel ID or @handle).\n   * **Formatting:** `parse_mode = None` (plain text), `appendAttribution = false`.\n\n2. **Send an AI-written trading brief with actionable intraday and weekly signals**\n\n   * **Purpose:** Publishes the AI agent’s structured trading brief (intraday + weekly signals).\n   * **Input:** `{{$json.message}}` (one or many chunks).\n   * **Destination:** `chatId: \"<Add Channel ID>\"`.\n   * **Formatting:** `parse_mode = None`, `appendAttribution = false`.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "016813a2-a135-4a77-955a-7f6071b7d523",
      "name": "Note Adhésive9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        992,
        -2592
      ],
      "parameters": {
        "width": 1296,
        "height": 3120,
        "content": "# 🧠 Bitcoin Multi-Exchange Liquidity AI Agent – System Documentation\n\nAn AI automation system for **cross-exchange Bitcoin liquidity analysis**.\nIt consolidates **order book data** from 10+ centralized exchanges, merges them into a unified liquidity snapshot, then generates structured **Telegram trading reports** with actionable signals.\n\n---\n\n## 🧩 Included Components\n\n> These are the active nodes and subagents in this workflow:\n\n| ✅ Component Name                                         | 📌 Function Description                                                                                       |\n| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |\n| **Schedule Trigger**                                     | Runs the workflow on a fixed schedule (e.g., every X hours).                                                  |\n| **Exchange HTTP Nodes** (Binance, Coinbase, Bybit, etc.) | Fetch BTC/USDT orderbook snapshots (bids/asks up to 5000 levels).                                             |\n| **Normalize Nodes (per exchange)**                       | Reshape raw API responses into a unified `{ data, symbol, lastUpdateId }` format.                             |\n| **Merge Exchange Data**                                  | Aggregates liquidity snapshots from all exchanges into a single batch.                                        |\n| **Join Into One Report**                                 | Collects and concatenates all per-exchange human-readable reports into a **Telegram-ready text block**.       |\n| **Join Into One Input for Analysis**                     | Builds a **nested JSON object** with cross-venue liquidity, support/resistance zones, and market metadata.    |\n| **Bitcoin Liquidity Analysis AI Agent (OpenAI)**         | Uses GPT (4.1-mini/4.1) to interpret consolidated liquidity and generate **intraday + weekly trade signals**. |\n| **Message Splitters**                                    | Ensure text outputs >4000 chars are broken into safe Telegram-sized chunks.                                   |\n| **Telegram Send Nodes (x2)**                             | Deliver reports and AI-written trading briefs to your Telegram channel.                                       |\n\n---\n\n## ⚙️ Installation Instructions\n\n### Step 1: Import Workflow\n\n* Open your **n8n Editor UI**.\n* Import the JSON file for `Bitcoin Multi-Exchange Liquidity AI Agent`.\n* Activate the workflow.\n\n### Step 2: Set Credentials\n\n* **OpenAI API** – GPT-4.1 / GPT-4.1-mini key.\n* **Telegram Bot API** – your bot token.\n* No exchange API keys are required (all order book endpoints are **public REST**).\n\n### Step 3: Telegram Setup\n\n* Add your Telegram bot to your target channel.\n* Replace **`<Add Channel ID>`** with the actual numeric ID or `@channel_username`.\n* The workflow will auto-post reports every cycle.\n\n---\n\n## 🖥️ Workflow Overview\n\n```\n[Schedule Trigger]\n → [HTTP Orderbook Nodes: Binance, Coinbase, Bybit, MEXC, Gate, Bitget, OKX, Kraken, HTX, Crypto.com]\n   → [Normalize Node per Exchange]\n     → [Merge Exchange Data]\n       → [Join Into One Report] → [Split if >4000 chars] → [Telegram Liquidity Report]\n       → [Join Into One Input for Analysis] → [Bitcoin Liquidity Analysis AI Agent] → [Split if >4000 chars] → [Telegram Trading Brief]\n```\n\n---\n\n## 📬 Telegram Output Format\n\n### **Liquidity Report (Raw Snapshots)**\n\n```\nBTC Liquidity Snapshot — 2025-10-06T12:00:00Z\n\nBinance — Best Bid: 62,345 | Best Ask: 62,355 | Spread: 10 (1.6 bps)\nCoinbase — Best Bid: 62,340 | Best Ask: 62,358 | Spread: 18 (2.9 bps)\nBybit — ...\n—\nTotal Bid Liquidity: $183M\nTotal Ask Liquidity: $177M\n```\n\n### **AI Trading Brief (Signals)**\n\n```\nBitcoin Multi-Exchange Liquidity Analysis\nDate: 2025-10-06\n\n1. Market Overview\n• Mid: 62,350 | Spread: 2.1 bps\n• Total Liquidity: $360M (Balanced)\n\n2. Liquidity Conditions\n• Bid imbalance on Binance (+12% vs ask side)\n• Thin resistance above 63,000\n\n3. Support/Resistance Zones\n• Support: 61,800–62,000 ($58M across Binance, OKX)\n• Resistance: 63,200–63,400 ($42M across Coinbase, Kraken)\n\n4. Cross-Exchange Comparison\n• Binance/OKX leading bids\n• Coinbase showing higher resistance\n\n5. Key Risks & Opportunities\n• Liquidity gap between 62,800–63,000 may invite breakout\n• Thin liquidity on Bybit books\n\n6. Trade Signals\n**Intraday**\n• Long breakout above 63,000 → Target 63,400 | Stop 62,750\n• Scalp short fade at 63,400 → Target 63,100 | Stop 63,500\n\n**Weekly**\n• Accumulation at 61,800–62,000\n• Breakout continuation if 63,400 resistance breaks\n```\n\n---\n\n## 🚀 Notes\n\n* Each exchange may format pairs differently (`BTCUSDT`, `BTC-USD`, `btcusdt`) — normalization fixes this.\n* Order book depth defaults to **5000 levels** where supported.\n* If any exchange API fails, the workflow continues with available data.\n* Telegram posts are always plain text (`parse_mode=None`) to avoid formatting issues.\n\n---\n\n## 🚀 Support & Licensing\n\n🔗 **Don Jayamaha – LinkedIn**\n[linkedin.com/in/donjayamahajr](https://www.linkedin.com/in/donjayamahajr)\n\n© 2025 **Treasurium Capital Limited Company**. All rights reserved.\nThis workflow structure, system architecture, and AI prompts are proprietary and protected by **U.S. copyright law**.\nReuse, resale, or redistribution is strictly prohibited without a valid license.\n\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "950b62a6-5c32-4023-b779-8ff2a127a0a2",
  "connections": {
    "89fd198b-9d25-4690-b1b4-40c8642068b4": {
      "main": [
        [
          {
            "node": "6300c4b4-0d78-4031-a3e9-3d3e62c08596",
            "type": "main",
            "index": 0
          },
          {
            "node": "ffe47e26-0088-4863-8b90-f00fda0fe505",
            "type": "main",
            "index": 0
          },
          {
            "node": "7695b78a-4943-4acc-8e5f-ec5ca4e14752",
            "type": "main",
            "index": 0
          },
          {
            "node": "7d6d1f1d-20cd-4401-a8af-c031528fd75a",
            "type": "main",
            "index": 0
          },
          {
            "node": "675007eb-4b8b-425e-b434-9c4cd1ced692",
            "type": "main",
            "index": 0
          },
          {
            "node": "49909b61-c69d-4ee1-a511-575bd6a5e5f4",
            "type": "main",
            "index": 0
          },
          {
            "node": "6b30a489-2691-41c9-9a09-7cb42845d211",
            "type": "main",
            "index": 0
          },
          {
            "node": "12622d19-c17b-42d7-b4ca-805b5a4503f1",
            "type": "main",
            "index": 0
          },
          {
            "node": "a0f11725-7b0a-4ebf-821f-8b2a290b0ec5",
            "type": "main",
            "index": 0
          },
          {
            "node": "d32ad6fb-d65c-4c31-a281-0869a7416f11",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "337565e5-1993-4327-b278-2df5e902108a": {
      "ai_languageModel": [
        [
          {
            "node": "ac40d0d8-2085-42fc-9e3e-4d642c5a1eb9",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "22f6a76b-076e-422f-897f-bc46c5a24c11": {
      "main": [
        [
          {
            "node": "4b187480-ec1d-41b2-8186-48287673ab69",
            "type": "main",
            "index": 0
          },
          {
            "node": "735b52c4-6bcf-4d6a-8791-c7c37b46e0f8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "735b52c4-6bcf-4d6a-8791-c7c37b46e0f8": {
      "main": [
        [
          {
            "node": "fdc7a08f-869a-41fe-b17f-8f33635d0a39",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6b30a489-2691-41c9-9a09-7cb42845d211": {
      "main": [
        [
          {
            "node": "e66dbe8b-96de-4423-bbf0-99048bbcd0a6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "675007eb-4b8b-425e-b434-9c4cd1ced692": {
      "main": [
        [
          {
            "node": "4049e954-1c4b-43fb-a6ec-f71fe6dc4f05",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "a0f11725-7b0a-4ebf-821f-8b2a290b0ec5": {
      "main": [
        [
          {
            "node": "1987a40f-b9a5-48c0-a2f4-3484e264e490",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7d6d1f1d-20cd-4401-a8af-c031528fd75a": {
      "main": [
        [
          {
            "node": "a055c8f2-e70c-40fa-ba2b-ffee91982954",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "49909b61-c69d-4ee1-a511-575bd6a5e5f4": {
      "main": [
        [
          {
            "node": "9c6be517-72e3-4fb7-8aad-a6b85d4e3ed9",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7695b78a-4943-4acc-8e5f-ec5ca4e14752": {
      "main": [
        [
          {
            "node": "4138ea50-b071-4d28-8568-4478a4e15b4a",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "12622d19-c17b-42d7-b4ca-805b5a4503f1": {
      "main": [
        [
          {
            "node": "dd44cb84-3aec-4f9c-a35e-f3dea4785cfc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4b187480-ec1d-41b2-8186-48287673ab69": {
      "main": [
        [
          {
            "node": "ac40d0d8-2085-42fc-9e3e-4d642c5a1eb9",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6300c4b4-0d78-4031-a3e9-3d3e62c08596": {
      "main": [
        [
          {
            "node": "ead99762-ca50-47c5-af17-57fceab89879",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ffe47e26-0088-4863-8b90-f00fda0fe505": {
      "main": [
        [
          {
            "node": "45580616-e460-4b37-a038-6e89ea087c6e",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ac40d0d8-2085-42fc-9e3e-4d642c5a1eb9": {
      "main": [
        [
          {
            "node": "fdc7a08f-869a-41fe-b17f-8f33635d0a39",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "d32ad6fb-d65c-4c31-a281-0869a7416f11": {
      "main": [
        [
          {
            "node": "3d9d4b6e-207b-44bd-a077-ce13357a9010",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "fdc7a08f-869a-41fe-b17f-8f33635d0a39": {
      "main": [
        [
          {
            "node": "ff337127-416d-4ba4-9f0b-712fcde0ebe0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "0f0cbc28-4b34-4be3-a999-412a1ca2685a": {
      "main": [
        [
          {
            "node": "22f6a76b-076e-422f-897f-bc46c5a24c11",
            "type": "main",
            "index": 8
          }
        ]
      ]
    },
    "259479b4-e4fc-4a60-91f0-e6ee4b7a11c7": {
      "main": [
        [
          {
            "node": "22f6a76b-076e-422f-897f-bc46c5a24c11",
            "type": "main",
            "index": 6
          }
        ]
      ]
    },
    "ff5ce551-1dd9-4afe-a312-8c69d3509c9d": {
      "main": [
        [
          {
            "node": "22f6a76b-076e-422f-897f-bc46c5a24c11",
            "type": "main",
            "index": 9
          }
        ]
      ]
    },
    "1c838693-f752-429c-b61d-3c36280a38da": {
      "main": [
        [
          {
            "node": "22f6a76b-076e-422f-897f-bc46c5a24c11",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "1987a40f-b9a5-48c0-a2f4-3484e264e490": {
      "main": [
        [
          {
            "node": "0f0cbc28-4b34-4be3-a999-412a1ca2685a",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "e66dbe8b-96de-4423-bbf0-99048bbcd0a6": {
      "main": [
        [
          {
            "node": "259479b4-e4fc-4a60-91f0-e6ee4b7a11c7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "f766dece-26bd-4cb9-bb37-bb69b6c5b131": {
      "main": [
        [
          {
            "node": "22f6a76b-076e-422f-897f-bc46c5a24c11",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "a055c8f2-e70c-40fa-ba2b-ffee91982954": {
      "main": [
        [
          {
            "node": "1c838693-f752-429c-b61d-3c36280a38da",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8e2e662c-9481-4613-9a59-ef01806a60f7": {
      "main": [
        [
          {
            "node": "22f6a76b-076e-422f-897f-bc46c5a24c11",
            "type": "main",
            "index": 5
          }
        ]
      ]
    },
    "8d0bed3f-89dd-4e27-a7b3-03f0512473a2": {
      "main": [
        [
          {
            "node": "22f6a76b-076e-422f-897f-bc46c5a24c11",
            "type": "main",
            "index": 7
          }
        ]
      ]
    },
    "4138ea50-b071-4d28-8568-4478a4e15b4a": {
      "main": [
        [
          {
            "node": "f766dece-26bd-4cb9-bb37-bb69b6c5b131",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "cde17236-64e9-4088-b760-7eeabd052170": {
      "main": [
        [
          {
            "node": "22f6a76b-076e-422f-897f-bc46c5a24c11",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "3201f266-baf9-4da2-a7cc-3e9d358df6d0": {
      "main": [
        [
          {
            "node": "22f6a76b-076e-422f-897f-bc46c5a24c11",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "9c6be517-72e3-4fb7-8aad-a6b85d4e3ed9": {
      "main": [
        [
          {
            "node": "8e2e662c-9481-4613-9a59-ef01806a60f7",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "dd44cb84-3aec-4f9c-a35e-f3dea4785cfc": {
      "main": [
        [
          {
            "node": "8d0bed3f-89dd-4e27-a7b3-03f0512473a2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7a60368c-225c-4ad5-87a1-e781de0faf39": {
      "main": [
        [
          {
            "node": "22f6a76b-076e-422f-897f-bc46c5a24c11",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ead99762-ca50-47c5-af17-57fceab89879": {
      "main": [
        [
          {
            "node": "cde17236-64e9-4088-b760-7eeabd052170",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4049e954-1c4b-43fb-a6ec-f71fe6dc4f05": {
      "main": [
        [
          {
            "node": "3201f266-baf9-4da2-a7cc-3e9d358df6d0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "45580616-e460-4b37-a038-6e89ea087c6e": {
      "main": [
        [
          {
            "node": "7a60368c-225c-4ad5-87a1-e781de0faf39",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3d9d4b6e-207b-44bd-a077-ce13357a9010": {
      "main": [
        [
          {
            "node": "ff5ce551-1dd9-4afe-a312-8c69d3509c9d",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ff337127-416d-4ba4-9f0b-712fcde0ebe0": {
      "main": [
        []
      ]
    }
  }
}
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é ?

Avancé - Création de contenu, 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.

Workflows recommandés

Informations sur le workflow
Niveau de difficulté
Avancé
Nombre de nœuds50
Catégorie2
Types de nœuds8
Description de la difficulté

Adapté aux utilisateurs avancés, avec des workflows complexes contenant 16+ nœuds

Auteur
Don Jayamaha Jr

Don Jayamaha Jr

@don-the-gem-dealer

With 12 years of experience as a Blockchain Strategist and Web3 Architect, I specialize in bridging the gap between traditional industries and decentralized technologies. My expertise spans tokenized assets, crypto payment integrations, and blockchain-driven market solutions.

Liens externes
Voir sur n8n.io

Partager ce workflow

Catégories

Catégories: 34