Agente de liquidez de IA para intercambios

Avanzado

Este es unContent Creation, Multimodal AIflujo de automatización del dominio deautomatización que contiene 50 nodos.Utiliza principalmente nodos como Code, Merge, Telegram, HttpRequest, Agent. Usar datos de liquidez de 10 exchanges y análisis de GPT-4.1 para automatizar la generación de ideas sobre trading de Bitcoin

Requisitos previos
  • Bot Token de Telegram
  • Pueden requerirse credenciales de autenticación para la API de destino
  • Clave de API de OpenAI
Vista previa del flujo de trabajo
Visualización de las conexiones entre nodos, con soporte para zoom y panorámica
Exportar flujo de trabajo
Copie la siguiente configuración JSON en n8n para importar y usar este flujo de trabajo
{
  "id": "iiN021rrx2RtSHFJ",
  "meta": {
    "instanceId": "a5283507e1917a33cc3ae615b2e7d5ad2c1e50955e6f831272ddd5ab816f3fb6",
    "templateCredsSetupCompleted": true
  },
  "name": "Exchange Liquidity AI Agent (Official)",
  "tags": [],
  "nodes": [
    {
      "id": "89fd198b-9d25-4690-b1b4-40c8642068b4",
      "name": "Disparador Programado",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2720,
        -656
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6300c4b4-0d78-4031-a3e9-3d3e62c08596",
      "name": "Binance (Libro de Órdenes 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 (Libro de Órdenes 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 (Libro de Órdenes 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": "Organizar en un Clúster de Datos para Análisis (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": "Organizar en un Clúster de Datos para Análisis (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": "Organizar en un Clúster de Datos para Análisis (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": "Calcular Liquidez, Resistencia y Soporte (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": "Calcular Liquidez, Resistencia y Soporte (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": "Calcular Liquidez, Resistencia y Soporte (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": "Combinar Datos de Intercambios",
      "type": "n8n-nodes-base.merge",
      "position": [
        -624,
        -816
      ],
      "parameters": {
        "numberInputs": 10
      },
      "executeOnce": false,
      "typeVersion": 3.2
    },
    {
      "id": "735b52c4-6bcf-4d6a-8791-c7c37b46e0f8",
      "name": "Unir en un Solo Reporte",
      "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 Modelo 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": "Dividir si mensaje supera 4000 caracteres",
      "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 (Libro de Órdenes 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": "Organizar en un Clúster de Datos para Análisis (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 (Libro de Órdenes 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": "Calcular Liquidez, Resistencia y Soporte (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": "Organizar en un Clúster de Datos para Análisis (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": "Dividir mensaje si supera 4000 caracteres",
      "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": "Calcular Liquidez, Resistencia y Soporte (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 (Libro de Órdenes 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": "Calcular Liquidez, Resistencia y Soporte (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": "Organizar en un Clúster de Datos para Análisis (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 (Libro de Órdenes 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": "Calcular Liquidez, Resistencia y Soporte (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": "Organizar en un Clúster de Datos para Análisis (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 (Libro de Órdenes 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": "Calcular Liquidez, Resistencia y Soporte (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": "Organizar en un Clúster de Datos para Análisis (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 (Libro de Órdenes 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": "Calcular Liquidez, Resistencia y Soporte (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": "Organizar en un Clúster de Datos para Análisis (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 (Libro de Órdenes 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": "Calcular Liquidez, Resistencia y Soporte (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": "Organizar en un Clúster de Datos para Análisis (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": "Enviar Reporte de Liquidez Multi-Intercambio de Bitcoin al 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": "Enviar resumen de trading escrito por IA con señales accionables intradía y semanales",
      "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": "Unir en una Entrada Única para Análisis",
      "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": "Agente de IA de Análisis de Liquidez de 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": "Nota Adhesiva",
      "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": "Nota Adhesiva1",
      "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": "Nota Adhesiva2",
      "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": "Nota Adhesiva3",
      "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": "Nota Adhesiva4",
      "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": "Nota Adhesiva5",
      "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": "Nota Adhesiva6",
      "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": "Nota Adhesiva7",
      "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": "Nota Adhesiva8",
      "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": "Nota Adhesiva9",
      "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": "0834fa76-ae68-4204-aa3d-b6f8bb5279d9",
            "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": "ab6896ce-115d-4a78-9c76-e1ac5d3b9032",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "0834fa76-ae68-4204-aa3d-b6f8bb5279d9": {
      "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": [
        []
      ]
    }
  }
}
Preguntas frecuentes

¿Cómo usar este flujo de trabajo?

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

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

Avanzado - Creación de contenido, IA Multimodal

¿Es de pago?

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

Flujos de trabajo relacionados recomendados

Información del flujo de trabajo
Nivel de dificultad
Avanzado
Número de nodos50
Categoría2
Tipos de nodos8
Descripción de la dificultad

Adecuado para usuarios avanzados, flujos de trabajo complejos con 16+ nodos

Autor
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.

Enlaces externos
Ver en n8n.io

Compartir este flujo de trabajo

Categorías

Categorías: 34