取引所の流動性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": "Exchange Liquidity AI Agent (Official)",
  "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": "Binance (ビットコイン-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": "分析用単一データクラスターへの整形 (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": "分析用単一データクラスターへの整形 (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": "流動性・抵抗線・支持線の計算 (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": "流動性・抵抗線・支持線の計算 (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 オーダーブック)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": "流動性・抵抗線・支持線の計算 (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)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": "ビットコイン複数取引所流動性レポートのチャネル送信",
      "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エージェント",
      "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": "## **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": "付箋1",
      "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": "付箋2",
      "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": "付箋3",
      "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": "付箋4",
      "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": "付箋5",
      "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": "付箋6",
      "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": "付箋7",
      "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": "付箋8",
      "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": "付箋9",
      "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": [
        []
      ]
    }
  }
}
よくある質問

このワークフローの使い方は?

上記のJSON設定コードをコピーし、n8nインスタンスで新しいワークフローを作成して「JSONからインポート」を選択、設定を貼り付けて認証情報を必要に応じて変更してください。

このワークフローはどんな場面に適していますか?

上級 - コンテンツ作成, マルチモーダルAI

有料ですか?

このワークフローは完全無料です。ただし、ワークフローで使用するサードパーティサービス(OpenAI APIなど)は別途料金が発生する場合があります。

関連ワークフロー

Apify、AIによる募集情報照合でThreadsの募集ポストをTelegramで通知
Apify、AI によるフィルタリングと Telegram 通知で Threads 上の求人募集投稿を発見
If
Set
Code
+
If
Set
Code
19 ノードA Z
コンテンツ作成
✨🩷自動化ソーシャルメディアコンテンツ公開工厂 + 系统提示组合
基于动态系统提示とGPT-4oのAI驱动多平台ソーシャルメディアコンテンツ工厂
If
Set
Code
+
If
Set
Code
100 ノードAmit Mehta
コンテンツ作成
💥 NanoBanana、Seedream 4、ChatGPT Image、Veo 3 を使って動画広告を自動化 - VIDEO
AI(NanoBanana、Seedream、GPT-4o、Veo 3)を使って動画広告キャンペーンを自動化し公開
Set
Code
Wait
+
Set
Code
Wait
63 ノードDr. Firas
コンテンツ作成
キーワードからGPT-5とfal.ai画像を使ってWordPressまで自動SEOブログ生成のプロセス
GPT-5とfal.ai画像を使用したキーワードからWordPressへのSEOブログ自動化プロセス
Set
Code
Wait
+
Set
Code
Wait
96 ノードPaul
コンテンツ作成
WordPressブログの自動化プロフェッショナル版(先端研究)v2.1マーケットプラグイン
GPT-4o、Perplexity AI、そして多言語対応を使ったSEO最適化ブログ作成の自動化
If
Set
Xml
+
If
Set
Xml
125 ノードDaniel Ng
コンテンツ作成
OpenAI・LangChain・アピ業間連携によるワークフレーム自動化入門ガイド
OpenAI、LangChain、API を使用したワークフロー自動化の初心者ガイド
If
Set
Code
+
If
Set
Code
33 ノードMeelioo
コンテンツ作成
ワークフロー情報
難易度
上級
ノード数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で表示

このワークフローを共有

カテゴリー

カテゴリー: 34