8
n8n 中文网amn8n.com

交易所流动性AI代理

高级

这是一个Content Creation, Multimodal AI领域的自动化工作流,包含 50 个节点。主要使用 Code, Merge, Telegram, HttpRequest, Agent 等节点。 使用10家交易所流动性数据和GPT-4.1分析自动化比特币交易洞察

前置要求
  • Telegram Bot Token
  • 可能需要目标 API 的认证凭证
  • OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "iiN021rrx2RtSHFJ",
  "meta": {
    "instanceId": "a5283507e1917a33cc3ae615b2e7d5ad2c1e50955e6f831272ddd5ab816f3fb6",
    "templateCredsSetupCompleted": true
  },
  "name": "交易所流动性 AI Agent (官方)",
  "tags": [],
  "nodes": [
    {
      "id": "89fd198b-9d25-4690-b1b4-40c8642068b4",
      "name": "计划触发器",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2720,
        -656
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "6300c4b4-0d78-4031-a3e9-3d3e62c08596",
      "name": "币安 (比特币-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 (比特币-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 (比特币-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": "整理成一个数据集群进行分析 (币安)",
      "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": "整理成一个数据集群进行分析 (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": "整理成一个数据集群进行分析 (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": "计算流动性、阻力和支撑 (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": "计算流动性、阻力和支撑 (币安)",
      "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": "计算流动性、阻力和支撑 (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": "合并交易所数据",
      "type": "n8n-nodes-base.merge",
      "position": [
        -624,
        -816
      ],
      "parameters": {
        "numberInputs": 10
      },
      "executeOnce": false,
      "typeVersion": 3.2
    },
    {
      "id": "735b52c4-6bcf-4d6a-8791-c7c37b46e0f8",
      "name": "合并成一个报告",
      "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 聊天模型",
      "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": "如果消息超过 4000 字符则拆分",
      "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 (比特币-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": "整理成一个数据集群进行分析 (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 (比特币-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": "计算流动性、阻力和支撑 (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": "整理成一个数据集群进行分析 (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": "如果消息超过 4000 字符则拆分",
      "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": "计算流动性、阻力和支撑 (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 (比特币-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": "计算流动性、阻力和支撑 (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": "整理成一个数据集群进行分析 (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 (比特币-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": "计算流动性、阻力和支撑 (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": "整理成一个数据集群进行分析 (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 (比特币-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": "计算流动性、阻力和支撑 (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": "整理成一个数据集群进行分析 (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 (比特币-USDT 订单簿)",
      "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": "计算流动性、阻力和支撑 (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": "整理成一个数据集群进行分析 (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 (比特币-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": "计算流动性、阻力和支撑 (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": "整理成一个数据集群进行分析 (HTX)",
      "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": "发送比特币多交易所流动性报告到频道",
      "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": "发送一份 AI 撰写的包含可操作日内和周度信号的交易简报",
      "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": "合并成一个输入用于分析",
      "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": "比特币流动性分析 AI Agent",
      "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": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1920,
        -2704
      ],
      "parameters": {
        "color": 3,
        "width": 256,
        "height": 3392,
        "content": "## **多交易所订单簿收集器 (BTC/USDT)**"
      },
      "typeVersion": 1
    },
    {
      "id": "30cffe4b-13f3-4adb-9864-608aef805b84",
      "name": "便签 1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2896,
        -1232
      ],
      "parameters": {
        "width": 464,
        "height": 848,
        "content": "## **计划工作流触发器**"
      },
      "typeVersion": 1
    },
    {
      "id": "fa997577-0b73-4122-a9b6-ac07b523dbe4",
      "name": "便签 2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1584,
        -2704
      ],
      "parameters": {
        "color": 4,
        "height": 3392,
        "content": "## **每交易所流动性分析器 (BTC/USDT)**"
      },
      "typeVersion": 1
    },
    {
      "id": "844cf9e7-c0a4-4008-9d68-5663e5126842",
      "name": "便签 3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1296,
        -2704
      ],
      "parameters": {
        "color": 6,
        "height": 3392,
        "content": "## **订单簿有效载荷标准化器(每交易所数据整理器)**"
      },
      "typeVersion": 1
    },
    {
      "id": "889c1f40-3f41-4199-8be7-18172a8de361",
      "name": "便签 4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -704,
        -1248
      ],
      "parameters": {
        "height": 960,
        "content": "## **多源漏斗:合并交易所数据**"
      },
      "typeVersion": 1
    },
    {
      "id": "1c578ca1-ccd2-441c-be33-b4ba8c8ba4e2",
      "name": "便签 5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -336,
        -1584
      ],
      "parameters": {
        "color": 2,
        "width": 208,
        "height": 1296,
        "content": "## **跨场所合并器:最终报告和统一分析输入**"
      },
      "typeVersion": 1
    },
    {
      "id": "fa9b1f14-dddd-43eb-a2ae-de2d900cad31",
      "name": "便签6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        -912
      ],
      "parameters": {
        "color": 3,
        "width": 304,
        "height": 816,
        "content": "## **比特币流动性分析 AI Agent (LLM 编排)**"
      },
      "typeVersion": 1
    },
    {
      "id": "37932d4a-825f-48ed-aed3-27611734f5f8",
      "name": "便签7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        256,
        -1856
      ],
      "parameters": {
        "color": 4,
        "height": 1616,
        "content": "## **长消息分割器(4,000 字符块)**"
      },
      "typeVersion": 1
    },
    {
      "id": "0b9e808c-1760-4f7d-b9c8-c77bda1a8506",
      "name": "便签8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        -2224
      ],
      "parameters": {
        "color": 5,
        "height": 1984,
        "content": "## **Telegram 交付(报告和 AI 交易简报)**"
      },
      "typeVersion": 1
    },
    {
      "id": "016813a2-a135-4a77-955a-7f6071b7d523",
      "name": "便签9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        992,
        -2592
      ],
      "parameters": {
        "width": 1296,
        "height": 3120,
        "content": "# 🧠 比特币多交易所流动性 AI Agent – 系统文档"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "950b62a6-5c32-4023-b779-8ff2a127a0a2",
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Binance (Bitcoin-USDT Orderbook))",
            "type": "main",
            "index": 0
          },
          {
            "node": "Coinbase (Bitcoin-USDT Orderbook))",
            "type": "main",
            "index": 0
          },
          {
            "node": "Bybit (Bitcoin-USDT Orderbook))",
            "type": "main",
            "index": 0
          },
          {
            "node": "MEXC (Bitcoin-USDT Orderbook)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Gate (Bitcoin-USDT Orderbook)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Bitget (Bitcoin-USDT Orderbook)",
            "type": "main",
            "index": 0
          },
          {
            "node": "OKX (Bitcoin-USDT Orderbook)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Kraken (Bitcoin-USDT Orderbook)",
            "type": "main",
            "index": 0
          },
          {
            "node": "HTX (Bitcoin-USDT Orderbook)1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Crypto.com (Bitcoin-USDT Orderbook)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Bitcoin Liquidity Analysis AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Merge Exchange Data": {
      "main": [
        [
          {
            "node": "Join Into One Input for Analysis",
            "type": "main",
            "index": 0
          },
          {
            "node": "Join Into One Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Join Into One Report": {
      "main": [
        [
          {
            "node": "Split message if more than 4000 characters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OKX (Bitcoin-USDT Orderbook)": {
      "main": [
        [
          {
            "node": "Calculate Liquidity, Resistance, and Support (OKX)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Gate (Bitcoin-USDT Orderbook)": {
      "main": [
        [
          {
            "node": "Calculate Liquidity, Resistance, and Support (Gate.io)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTX (Bitcoin-USDT Orderbook)1": {
      "main": [
        [
          {
            "node": "Calculate Liquidity, Resistance, and Support (HTX)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MEXC (Bitcoin-USDT Orderbook)": {
      "main": [
        [
          {
            "node": "Calculate Liquidity, Resistance, and Support (MEXC)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bitget (Bitcoin-USDT Orderbook)": {
      "main": [
        [
          {
            "node": "Calculate Liquidity, Resistance, and Support (Bitget)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bybit (Bitcoin-USDT Orderbook))": {
      "main": [
        [
          {
            "node": "Calculate Liquidity, Resistance, and Support (Bybit)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Kraken (Bitcoin-USDT Orderbook)": {
      "main": [
        [
          {
            "node": "Calculate Liquidity, Resistance, and Support (Kraken)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Join Into One Input for Analysis": {
      "main": [
        [
          {
            "node": "Bitcoin Liquidity Analysis AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Binance (Bitcoin-USDT Orderbook))": {
      "main": [
        [
          {
            "node": "Calculate Liquidity, Resistance, and Support (Binance)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Coinbase (Bitcoin-USDT Orderbook))": {
      "main": [
        [
          {
            "node": "Calculate Liquidity, Resistance, and Support (Coinbase)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Bitcoin Liquidity Analysis AI Agent": {
      "main": [
        [
          {
            "node": "Splits message is more than 4000 characters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Crypto.com (Bitcoin-USDT Orderbook)": {
      "main": [
        [
          {
            "node": "Calculate Liquidity, Resistance, and Support (Crypto.com)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split message if more than 4000 characters": {
      "main": [
        [
          {
            "node": "Send Bitcoin Multi-Exchange Liquidity Report to Channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Splits message is more than 4000 characters": {
      "main": [
        [
          {
            "node": "Send an AI-written trading brief with actionable intraday and weekly signals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wrangle into One Data Cluster for Analysis (HTX)": {
      "main": [
        [
          {
            "node": "Merge Exchange Data",
            "type": "main",
            "index": 8
          }
        ]
      ]
    },
    "Wrangle into One Data Cluster for Analysis (OKX)": {
      "main": [
        [
          {
            "node": "Merge Exchange Data",
            "type": "main",
            "index": 6
          }
        ]
      ]
    },
    "Wrangle into One Data Cluster for Analysis (HTX)1": {
      "main": [
        [
          {
            "node": "Merge Exchange Data",
            "type": "main",
            "index": 9
          }
        ]
      ]
    },
    "Wrangle into One Data Cluster for Analysis (MEXC)": {
      "main": [
        [
          {
            "node": "Merge Exchange Data",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Calculate Liquidity, Resistance, and Support (HTX)": {
      "main": [
        [
          {
            "node": "Wrangle into One Data Cluster for Analysis (HTX)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Liquidity, Resistance, and Support (OKX)": {
      "main": [
        [
          {
            "node": "Wrangle into One Data Cluster for Analysis (OKX)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wrangle into One Data Cluster for Analysis (Bybit)": {
      "main": [
        [
          {
            "node": "Merge Exchange Data",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Calculate Liquidity, Resistance, and Support (MEXC)": {
      "main": [
        [
          {
            "node": "Wrangle into One Data Cluster for Analysis (MEXC)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wrangle into One Data Cluster for Analysis (Bitget)": {
      "main": [
        [
          {
            "node": "Merge Exchange Data",
            "type": "main",
            "index": 5
          }
        ]
      ]
    },
    "Wrangle into One Data Cluster for Analysis (Kraken)": {
      "main": [
        [
          {
            "node": "Merge Exchange Data",
            "type": "main",
            "index": 7
          }
        ]
      ]
    },
    "Calculate Liquidity, Resistance, and Support (Bybit)": {
      "main": [
        [
          {
            "node": "Wrangle into One Data Cluster for Analysis (Bybit)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wrangle into One Data Cluster for Analysis (Binance)": {
      "main": [
        [
          {
            "node": "Merge Exchange Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Wrangle into One Data Cluster for Analysis (Gate.io)": {
      "main": [
        [
          {
            "node": "Merge Exchange Data",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "Calculate Liquidity, Resistance, and Support (Bitget)": {
      "main": [
        [
          {
            "node": "Wrangle into One Data Cluster for Analysis (Bitget)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Liquidity, Resistance, and Support (Kraken)": {
      "main": [
        [
          {
            "node": "Wrangle into One Data Cluster for Analysis (Kraken)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wrangle into One Data Cluster for Analysis (Coinbase)": {
      "main": [
        [
          {
            "node": "Merge Exchange Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Liquidity, Resistance, and Support (Binance)": {
      "main": [
        [
          {
            "node": "Wrangle into One Data Cluster for Analysis (Binance)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Liquidity, Resistance, and Support (Gate.io)": {
      "main": [
        [
          {
            "node": "Wrangle into One Data Cluster for Analysis (Gate.io)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Liquidity, Resistance, and Support (Coinbase)": {
      "main": [
        [
          {
            "node": "Wrangle into One Data Cluster for Analysis (Coinbase)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Liquidity, Resistance, and Support (Crypto.com)": {
      "main": [
        [
          {
            "node": "Wrangle into One Data Cluster for Analysis (HTX)1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send an AI-written trading brief with actionable intraday and weekly signals": {
      "main": [
        []
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 内容创作, 多模态 AI

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量50
分类2
节点类型8
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者
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.

外部链接
在 n8n.io 查看

分享此工作流