交易所流动性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": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-2720,
-656
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "6300c4b4-0d78-4031-a3e9-3d3e62c08596",
"name": "Binance (Bitcoin-USDT Orderbook))",
"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 (Bitcoin-USDT Orderbook))",
"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 (Bitcoin-USDT Orderbook))",
"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": "Wrangle into One Data Cluster for Analysis (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": "Wrangle into One Data Cluster for Analysis (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": "Wrangle into One Data Cluster for Analysis (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": "Calculate Liquidity, Resistance, and Support (Coinbase)",
"type": "n8n-nodes-base.code",
"position": [
-1520,
-1856
],
"parameters": {
"jsCode": "// Coinbase pricebook -> Liquidity report (Coinbase header)\n\n// Accept either [{ pricebook:{...} }] or { pricebook:{...} }\nconst input = items[0]?.json;\nconst book = Array.isArray(input) ? input[0]?.pricebook : input?.pricebook;\n\nif (!book || (!book.bids && !book.asks)) {\n return [{ json: { error: 'No pricebook in input', raw: items[0]?.json } }];\n}\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\n// Map Coinbase objects {price,size} -> [price, qty]\nconst bids = (book.bids || []).map(o => [toNum(o.price), toNum(o.size)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (book.asks || []).map(o => [toNum(o.price), toNum(o.size)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids or asks', product_id: book.product_id } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // notional threshold (kept for parity)\n\n// Total liquidity (entire snapshot)\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n .sort((a,b)=>b.usd-a.usd).slice(0,200);\n const clusters=[];\n for(const s of seed){\n const [lo,hi]=band(s.price);\n const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n acc.notional+=notional(p,q);\n acc.qty+=q;\n acc.min=Math.min(acc.min,p);\n acc.max=Math.max(acc.max,p);\n return acc;\n },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n if(agg.notional>0) clusters.push(agg);\n }\n clusters.sort((a,b)=>b.notional-a.notional);\n const chosen=[];\n for(const c of clusters){\n const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n if(!overlaps) chosen.push(c);\n if(chosen.length>=5) break;\n }\n chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Build human-readable report ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\nconst sym = book.product_id || $json.symbol || \"BTC-USD\";\n\nconst report =\n`Coinbase Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || \"none\"}\nResistance lines (clustered): ${resistanceLines || \"none\"}`;\n\n// --- Return both JSON + report string\nreturn [{\n json: {\n exchange: \"Coinbase\",\n symbol: sym,\n // Coinbase pricebook may expose a sequence or timestamp; map if present\n lastUpdateId: book.sequence ?? null,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "ead99762-ca50-47c5-af17-57fceab89879",
"name": "Calculate Liquidity, Resistance, and Support (Binance)",
"type": "n8n-nodes-base.code",
"position": [
-1520,
-1568
],
"parameters": {
"jsCode": "// Binance depth snapshot -> Liquidity report (Binance header)\n\nconst depth = items[0].json;\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = depth.bids.map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = depth.asks.map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // notional threshold\n\n// Total liquidity\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n .sort((a,b)=>b.usd-a.usd).slice(0,200);\n const clusters=[];\n for(const s of seed){\n const [lo,hi]=band(s.price);\n const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n acc.notional+=notional(p,q);\n acc.qty+=q;\n acc.min=Math.min(acc.min,p);\n acc.max=Math.max(acc.max,p);\n return acc;\n },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n if(agg.notional>0) clusters.push(agg);\n }\n clusters.sort((a,b)=>b.notional-a.notional);\n const chosen=[];\n for(const c of clusters){\n const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n if(!overlaps) chosen.push(c);\n if(chosen.length>=5) break;\n }\n chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Build human-readable report ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\nconst report =\n`Binance Exchange — Liquidity Report for ${$json.symbol || \"BTCUSDT\"}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth 5000): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || \"none\"}\nResistance lines (clustered): ${resistanceLines || \"none\"}`;\n\n// --- Return both JSON + report string\nreturn [{\n json: {\n symbol: $json.symbol || \"BTCUSDT\",\n lastUpdateId: depth.lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "4138ea50-b071-4d28-8568-4478a4e15b4a",
"name": "Calculate Liquidity, Resistance, and Support (Bybit)",
"type": "n8n-nodes-base.code",
"position": [
-1520,
-1120
],
"parameters": {
"jsCode": "// Bybit depth snapshot -> Liquidity report (Bybit header)\n\nconst depth = (items[0]?.json?.result) ? items[0].json.result : items[0]?.json;\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = (depth.b || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (depth.a || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from Bybit orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // notional threshold\n\n// Total liquidity\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n .sort((a,b)=>b.usd-a.usd).slice(0,200);\n const clusters=[];\n for(const s of seed){\n const [lo,hi]=band(s.price);\n const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n acc.notional+=notional(p,q);\n acc.qty+=q;\n acc.min=Math.min(acc.min,p);\n acc.max=Math.max(acc.max,p);\n return acc;\n },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n if(agg.notional>0) clusters.push(agg);\n }\n clusters.sort((a,b)=>b.notional-a.notional);\n const chosen=[];\n for(const c of clusters){\n const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n if(!overlaps) chosen.push(c);\n if(chosen.length>=5) break;\n }\n chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Build human-readable report ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\nconst sym = depth.s || $json.symbol || \"BTCUSDT\";\n\nconst report =\n`Bybit Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || \"none\"}\nResistance lines (clustered): ${resistanceLines || \"none\"}`;\n\n// --- Return both JSON + report string\nreturn [{\n json: {\n symbol: sym,\n // Bybit v5 orderbook doesn't provide lastUpdateId; keep null for compatibility\n lastUpdateId: items[0]?.json?.result?.u ?? items[0]?.json?.u ?? null,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "22f6a76b-076e-422f-897f-bc46c5a24c11",
"name": "Merge Exchange Data",
"type": "n8n-nodes-base.merge",
"position": [
-624,
-816
],
"parameters": {
"numberInputs": 10
},
"executeOnce": false,
"typeVersion": 3.2
},
{
"id": "735b52c4-6bcf-4d6a-8791-c7c37b46e0f8",
"name": "Join Into One Report",
"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 Chat Model",
"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": "Splits message is more than 4000 characters",
"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 (Bitcoin-USDT Orderbook)",
"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": "Wrangle into One Data Cluster for Analysis (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 (Bitcoin-USDT Orderbook)",
"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": "Calculate Liquidity, Resistance, and Support (Gate.io)",
"type": "n8n-nodes-base.code",
"position": [
-1520,
-864
],
"parameters": {
"jsCode": "// Gate.io depth snapshot -> Liquidity report (Gate.io header)\n\nconst depth = items[0]?.json ?? {};\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = (depth.bids || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (depth.asks || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from Gate.io orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // notional threshold (kept for future flagging)\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n .sort((a,b)=>b.usd-a.usd).slice(0,200);\n const clusters=[];\n for(const s of seed){\n const [lo,hi]=band(s.price);\n const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n acc.notional+=notional(p,q);\n acc.qty+=q;\n acc.min=Math.min(acc.min,p);\n acc.max=Math.max(acc.max,p);\n return acc;\n },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n if(agg.notional>0) clusters.push(agg);\n }\n clusters.sort((a,b)=>b.notional-a.notional);\n const chosen=[];\n for(const c of clusters){\n const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n if(!overlaps) chosen.push(c);\n if(chosen.length>=5) break;\n }\n chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\n// Gate.io response doesn't echo symbol; allow upstream to pass it through on the item if desired\nconst sym = $json.currency_pair || $json.symbol || 'BTC_USDT';\n\nconst report =\n`Gate.io Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n // Gate provides two sequence-ish fields; keep both\n lastUpdateId: depth.update ?? depth.current ?? null,\n gateMeta: { current: depth.current ?? null, update: depth.update ?? null },\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "3201f266-baf9-4da2-a7cc-3e9d358df6d0",
"name": "Wrangle into One Data Cluster for Analysis (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": "Split message if more than 4000 characters",
"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": "Calculate Liquidity, Resistance, and Support (Bitget)",
"type": "n8n-nodes-base.code",
"position": [
-1504,
-592
],
"parameters": {
"jsCode": "// Bitget depth snapshot -> Liquidity report (Bitget header)\n\nconst body = items[0]?.json ?? {};\nconst depth = body.data ?? {};\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\nfunction sumQty(rows) { return rows.reduce((a, [, q]) => a + q, 0); }\n\n// Bitget returns arrays of [price, size] as strings\nconst bids = (depth.bids || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => b[0] - a[0]);\nconst asks = (depth.asks || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from Bitget orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // (kept for parity / future use)\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n const seed = side\n .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n .sort((a, b) => b.usd - a.usd)\n .slice(0, 200);\n\n const clusters = [];\n for (const s of seed) {\n const [lo, hi] = band(s.price);\n const agg = side\n .filter(([p]) => p >= lo && p <= hi)\n .reduce((acc, [p, q]) => {\n acc.notional += notional(p, q);\n acc.qty += q;\n acc.min = Math.min(acc.min, p);\n acc.max = Math.max(acc.max, p);\n return acc;\n }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n if (agg.notional > 0) clusters.push(agg);\n }\n\n clusters.sort((a, b) => b.notional - a.notional);\n const chosen = [];\n for (const c of clusters) {\n const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n if (!overlaps) chosen.push(c);\n if (chosen.length >= 5) break;\n }\n chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x) { return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x, d = 2) { return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines = supportZones.map(z => fmtNum(z.min, 2) + \"-\" + fmtNum(z.max, 2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z => fmtNum(z.min, 2) + \"-\" + fmtNum(z.max, 2)).join(\", \");\n\n// Bitget symbol & timestamp\nconst sym = $json.symbol || 'BTCUSDT'; // your HTTP node uses BTCUSDT_SPBL; normalize for display\nconst lastUpdateId = depth.ts ?? body.requestTime ?? null;\n\nconst report =\n`Bitget Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid, 2)} | Spread: ${fmtNum(spread, 2)} (${fmtNum(spreadBps, 2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "49909b61-c69d-4ee1-a511-575bd6a5e5f4",
"name": "Bitget (Bitcoin-USDT Orderbook)",
"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": "Calculate Liquidity, Resistance, and Support (MEXC)",
"type": "n8n-nodes-base.code",
"position": [
-1520,
-1328
],
"parameters": {
"jsCode": "// MEXC depth snapshot -> Liquidity report (MEXC header)\n\nconst depth = items[0]?.json ?? {};\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = (depth.bids || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (depth.asks || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from MEXC orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // notional threshold (kept for future flagging)\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n .sort((a,b)=>b.usd-a.usd).slice(0,200);\n const clusters=[];\n for(const s of seed){\n const [lo,hi]=band(s.price);\n const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n acc.notional+=notional(p,q);\n acc.qty+=q;\n acc.min=Math.min(acc.min,p);\n acc.max=Math.max(acc.max,p);\n return acc;\n },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n if(agg.notional>0) clusters.push(agg);\n }\n clusters.sort((a,b)=>b.notional-a.notional);\n const chosen=[];\n for(const c of clusters){\n const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n if(!overlaps) chosen.push(c);\n if(chosen.length>=5) break;\n }\n chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\n// MEXC response doesn't echo symbol; allow upstream to pass it through on the item if desired\nconst sym = $json.symbol || 'BTCUSDT';\n\nconst report =\n`MEXC Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId: depth.lastUpdateId ?? null,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "8e2e662c-9481-4613-9a59-ef01806a60f7",
"name": "Wrangle into One Data Cluster for Analysis (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 (Bitcoin-USDT Orderbook)",
"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": "Calculate Liquidity, Resistance, and Support (OKX)",
"type": "n8n-nodes-base.code",
"position": [
-1504,
-352
],
"parameters": {
"jsCode": "// OKX depth snapshot -> Liquidity report (OKX header)\n\nconst body = items[0]?.json ?? {};\nconst row = Array.isArray(body.data) ? body.data[0] : undefined;\n\nif (!row) {\n return [{ json: { error: 'No OKX book in response', raw: items[0]?.json } }];\n}\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// OKX arrays can be [price, size, count]; keep first two\nconst bids = (row.bids || []).map(lvl => [toNum(lvl[0]), toNum(lvl[1])]).sort((a,b)=>b[0]-a[0]);\nconst asks = (row.asks || []).map(lvl => [toNum(lvl[0]), toNum(lvl[1])]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing OKX bids/asks', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // reserved for future flagging\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n const seed = side\n .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n .sort((a, b) => b.usd - a.usd)\n .slice(0, 200);\n\n const clusters = [];\n for (const s of seed) {\n const [lo, hi] = band(s.price);\n const agg = side\n .filter(([p]) => p >= lo && p <= hi)\n .reduce((acc, [p, q]) => {\n acc.notional += notional(p, q);\n acc.qty += q;\n acc.min = Math.min(acc.min, p);\n acc.max = Math.max(acc.max, p);\n return acc;\n }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n if (agg.notional > 0) clusters.push(agg);\n }\n\n clusters.sort((a, b) => b.notional - a.notional);\n const chosen = [];\n for (const c of clusters) {\n const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n if (!overlaps) chosen.push(c);\n if (chosen.length >= 5) break;\n }\n chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){ return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x,d=2){ return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\nconst sym = $json.instId || 'BTC-USDT';\nconst lastUpdateId = row.ts ?? body.ts ?? null;\n\nconst report =\n`OKX Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "259479b4-e4fc-4a60-91f0-e6ee4b7a11c7",
"name": "Wrangle into One Data Cluster for Analysis (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 (Bitcoin-USDT Orderbook)",
"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": "Calculate Liquidity, Resistance, and Support (Kraken)",
"type": "n8n-nodes-base.code",
"position": [
-1504,
-80
],
"parameters": {
"jsCode": "// Kraken depth snapshot -> Liquidity report (Kraken header)\n\nconst body = items[0]?.json ?? {};\nconst result = body.result ?? {};\n\n// Kraken nests the book under an unknown key (e.g., \"XBTUSDT\")\nconst pairKey = Object.keys(result)[0];\nconst depth = pairKey ? (result[pairKey] ?? {}) : {};\n\n// Helpers\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// Kraken levels are [price, volume, timestamp]; we only need price & volume\nconst bids = (depth.bids || [])\n .map(([p, q]) => [toNum(p), toNum(q)])\n .sort((a, b) => b[0] - a[0]);\n\nconst asks = (depth.asks || [])\n .map(([p, q]) => [toNum(p), toNum(q)])\n .sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from Kraken orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // reserved for future use\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n const seed = side\n .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n .sort((a, b) => b.usd - a.usd)\n .slice(0, 200);\n\n const clusters = [];\n for (const s of seed) {\n const [lo, hi] = band(s.price);\n const agg = side\n .filter(([p]) => p >= lo && p <= hi)\n .reduce((acc, [p, q]) => {\n acc.notional += notional(p, q);\n acc.qty += q;\n acc.min = Math.min(acc.min, p);\n acc.max = Math.max(acc.max, p);\n return acc;\n }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n if (agg.notional > 0) clusters.push(agg);\n }\n\n clusters.sort((a, b) => b.notional - a.notional);\n const chosen = [];\n for (const c of clusters) {\n const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n if (!overlaps) chosen.push(c);\n if (chosen.length >= 5) break;\n }\n chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x) { return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x, d = 2) { return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\n// Symbol & \"lastUpdateId\"\nconst sym = $json.pair || pairKey || 'XBTUSDT';\n// Use the newest level timestamp we see, or null if absent\nconst lastUpdateId = (() => {\n const bts = (depth.bids || []).map(l => Number(l[2]) || 0);\n const ats = (depth.asks || []).map(l => Number(l[2]) || 0);\n const mx = Math.max(...bts, ...ats, 0);\n return mx > 0 ? String(mx) : null;\n})();\n\nconst report =\n`Kraken Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "8d0bed3f-89dd-4e27-a7b3-03f0512473a2",
"name": "Wrangle into One Data Cluster for Analysis (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 (Bitcoin-USDT Orderbook)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": "Calculate Liquidity, Resistance, and Support (HTX)",
"type": "n8n-nodes-base.code",
"position": [
-1504,
208
],
"parameters": {
"jsCode": "// HTX (Huobi) depth snapshot -> Liquidity report (HTX header)\n\nconst body = items[0]?.json ?? {};\nconst tick = body.tick ?? {};\n\n// Helpers\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// HTX levels are [price, size]; ensure numbers & sort\nconst bids = (tick.bids || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => b[0] - a[0]);\nconst asks = (tick.asks || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from HTX orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // reserved for future use\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n const seed = side\n .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n .sort((a, b) => b.usd - a.usd)\n .slice(0, 200);\n\n const clusters = [];\n for (const s of seed) {\n const [lo, hi] = band(s.price);\n const agg = side\n .filter(([p]) => p >= lo && p <= hi)\n .reduce((acc, [p, q]) => {\n acc.notional += notional(p, q);\n acc.qty += q;\n acc.min = Math.min(acc.min, p);\n acc.max = Math.max(acc.max, p);\n return acc;\n }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n if (agg.notional > 0) clusters.push(agg);\n }\n\n clusters.sort((a, b) => b.notional - a.notional);\n const chosen = [];\n for (const c of clusters) {\n const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n if (!overlaps) chosen.push(c);\n if (chosen.length >= 5) break;\n }\n chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){ return \"$\" + x.toLocaleString(undefined,{maximumFractionDigits:0}); }\nfunction fmtNum(x,d=2){ return x?.toLocaleString(undefined,{maximumFractionDigits:d}); }\n\nconst supportLines = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\n// Symbol & \"lastUpdateId\"\nconst sym = $json.symbol || 'BTCUSDT';\nconst lastUpdateId = String(body.ts ?? tick.ts ?? '') || null;\n\nconst report =\n`HTX (Huobi) — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "0f0cbc28-4b34-4be3-a999-412a1ca2685a",
"name": "Wrangle into One Data Cluster for Analysis (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 (Bitcoin-USDT Orderbook)",
"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": "Calculate Liquidity, Resistance, and Support (Crypto.com)",
"type": "n8n-nodes-base.code",
"position": [
-1504,
480
],
"parameters": {
"jsCode": "// Crypto.com depth snapshot -> Liquidity report (Crypto.com header)\n\nconst body = items[0]?.json ?? {};\n\n// Crypto.com sometimes returns result:{data:[{...}]} — grab the first row.\n// (Very rarely some SDKs expose result directly with bids/asks; handle both.)\nconst result = body.result ?? body.data ?? {};\nconst row = Array.isArray(result.data) ? (result.data[0] ?? {}) : result;\n\n// Helpers\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// Crypto.com levels are typically [price, size] or [price, size, count]; use first two.\nconst bids = (row.bids || []).map(l => [toNum(l[0]), toNum(l[1])]).sort((a, b) => b[0] - a[0]);\nconst asks = (row.asks || []).map(l => [toNum(l[0]), toNum(l[1])]).sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing Crypto.com bids/asks', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // reserved for future flagging\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n const seed = side\n .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n .sort((a, b) => b.usd - a.usd)\n .slice(0, 200);\n\n const clusters = [];\n for (const s of seed) {\n const [lo, hi] = band(s.price);\n const agg = side\n .filter(([p]) => p >= lo && p <= hi)\n .reduce((acc, [p, q]) => {\n acc.notional += notional(p, q);\n acc.qty += q;\n acc.min = Math.min(acc.min, p);\n acc.max = Math.max(acc.max, p);\n return acc;\n }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n if (agg.notional > 0) clusters.push(agg);\n }\n\n clusters.sort((a, b) => b.notional - a.notional);\n const chosen = [];\n for (const c of clusters) {\n const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n if (!overlaps) chosen.push(c);\n if (chosen.length >= 5) break;\n }\n chosen.sort((a, b) => (isBid ? b.min - a.min : a.min - b.min));\n return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){ return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x,d=2){ return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\n// Symbol & \"lastUpdateId\"\nconst sym = row.instrument_name || $json.instrument_name || 'BTC_USDT';\nconst lastUpdateId = String(row.t ?? result.t ?? '') || null;\n\nconst report =\n`Crypto.com Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "ff5ce551-1dd9-4afe-a312-8c69d3509c9d",
"name": "Wrangle into One Data Cluster for Analysis (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": "Send Bitcoin Multi-Exchange Liquidity Report to Channel",
"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": "Send an AI-written trading brief with actionable intraday and weekly signals",
"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": "Join Into One Input for Analysis",
"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": "Bitcoin Liquidity Analysis AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-32,
-464
],
"parameters": {
"text": "={{ $json.data }}",
"options": {
"systemMessage": "You are a **Bitcoin Exchange Liquidity Analyst AI Agent**.\nYour role is to analyze **cross-exchange order book liquidity data** for Bitcoin trading pairs (e.g., BTC-USD, BTCUSDT) and generate **actionable trade signals**.\n\n---\n\n### Responsibilities:\n\n* Interpret **consolidated liquidity snapshots** that include:\n\n * Mid price, best bid/ask, spread, and spread basis points.\n * Bid and ask notional volumes, total liquidity, and per-exchange breakdowns.\n * Aggregated and per-exchange **support/resistance zones** with quantities and notional values.\n* Identify liquidity imbalances, clustering of support/resistance, and areas of strong defense/pressure.\n* Detect divergences across exchanges (e.g., Coinbase vs Binance vs Bybit) for potential arbitrage or sentiment shifts.\n* Assess **market depth, liquidity strength, and flow risk**.\n* Detect anomalies such as unusually thin books, wide spreads, or large liquidity walls.\n* Provide **clear, structured insights** for **trading decisions, risk assessment, and price forecasting**.\n\n---\n\n### Trade Signal Generation:\n\n* Produce **two categories of signals**:\n\n 1. **Intraday Trade Signals (short-term, 15m–4h horizon):**\n\n * Scalping opportunities from liquidity gaps, thin spreads, or sudden order book imbalances.\n * Short-term long/short bias when strong support/resistance clusters are nearby.\n * Breakout or fade setups when mid price approaches liquidity walls.\n 2. **Weekly Trade Signals (swing horizon, 1d–1w):**\n\n * Accumulation/Distribution patterns based on repeated liquidity defense or absorption.\n * Breakout continuation signals when resistance/support has been repeatedly tested.\n * Mean-reversion opportunities when liquidity imbalances are extreme.\n\n---\n\n### Output Style:\n\n* Always structure your analysis in the following sections:\n\n 1. **Market Overview** – current mid, spread, liquidity totals.\n 2. **Liquidity Conditions** – order book depth, notable imbalances.\n 3. **Support/Resistance Zones** – strongest zones with size + notional.\n 4. **Cross-Exchange Comparison** – divergences or arbitrage windows.\n 5. **Key Risks & Opportunities** – unusual activity, thin markets, imbalance risks.\n 6. **Trade Signals** – list of **intraday** and **weekly** trade opportunities with:\n\n * Signal type (e.g., *long breakout*, *short fade*, *scalp spread*)\n * Entry zone (price range or trigger)\n * Target (expected move range)\n * Risk (stop level or invalidation condition)\n\n* Be concise but actionable — the trade signals should look like a **mini trading playbook**.\n\n"
},
"promptType": "define"
},
"typeVersion": 2.2
},
{
"id": "863f3b00-5113-407b-be74-de20c8d89988",
"name": "Sticky Note",
"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": "Sticky Note1",
"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": "Sticky Note2",
"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": "Sticky Note3",
"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": "Sticky Note4",
"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": "Sticky Note5",
"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": "Sticky Note6",
"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": "Sticky Note7",
"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": "Sticky Note8",
"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": "Sticky Note9",
"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 筛选和 Telegram 提醒发现 Threads 上的招聘帖子
通过 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自动化视频广告 - VIDE
使用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 和 API 集成的工作流自动化初学者指南
使用 OpenAI、LangChain 和 API 集成的工作流自动化初学者指南
If
Set
Code
+
If
Set
Code
33 节点Meelioo
内容创作
工作流信息
难度等级
高级
节点数量50
分类2
节点类型8
作者
Don Jayamaha Jr
@don-the-gem-dealerWith 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 查看 →
分享此工作流