交易所流动性AI代理
高级
这是一个Content Creation, Multimodal AI领域的自动化工作流,包含 50 个节点。主要使用 Code, Merge, Telegram, HttpRequest, Agent 等节点。 使用10家交易所流动性数据和GPT-4.1分析自动化比特币交易洞察
前置要求
- •Telegram Bot Token
- •可能需要目标 API 的认证凭证
- •OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "iiN021rrx2RtSHFJ",
"meta": {
"instanceId": "a5283507e1917a33cc3ae615b2e7d5ad2c1e50955e6f831272ddd5ab816f3fb6",
"templateCredsSetupCompleted": true
},
"name": "交易所流动性 AI Agent (官方)",
"tags": [],
"nodes": [
{
"id": "89fd198b-9d25-4690-b1b4-40c8642068b4",
"name": "计划触发器",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-2720,
-656
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "6300c4b4-0d78-4031-a3e9-3d3e62c08596",
"name": "币安 (比特币-USDT 订单簿)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1840,
-1568
],
"parameters": {
"url": "https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=5000",
"options": {}
},
"typeVersion": 4.2
},
{
"id": "ffe47e26-0088-4863-8b90-f00fda0fe505",
"name": "Coinbase (比特币-USDT 订单簿)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1840,
-1856
],
"parameters": {
"url": "https://api.coinbase.com/api/v3/brokerage/market/product_book",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "=product_id",
"value": "BTC-USD"
},
{
"name": "=limit",
"value": "5000"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "7695b78a-4943-4acc-8e5f-ec5ca4e14752",
"name": "Bybit (比特币-USDT 订单簿)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1840,
-1120
],
"parameters": {
"url": "https://api.bybit.com/v5/market/orderbook",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "category",
"value": "spot"
},
{
"name": "symbol",
"value": "BTCUSDT"
},
{
"name": "limit",
"value": "5000"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "cde17236-64e9-4088-b760-7eeabd052170",
"name": "整理成一个数据集群进行分析 (币安)",
"type": "n8n-nodes-base.code",
"position": [
-1216,
-1568
],
"parameters": {
"jsCode": "// Grab whatever this node receives.\n// It can be an array with 1 object (like your example) or a plain object.\nconst input = items?.[0]?.json;\n\n// Get a clean object: if it's an array, take the first element.\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Emit one item with a single field: \"data\"\nreturn [\n {\n json: {\n data: payload,\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "7a60368c-225c-4ad5-87a1-e781de0faf39",
"name": "整理成一个数据集群进行分析 (Coinbase)",
"type": "n8n-nodes-base.code",
"position": [
-1216,
-1856
],
"parameters": {
"jsCode": "// Grab whatever this node receives.\n// It can be an array with 1 object (like your example) or a plain object.\nconst input = items?.[0]?.json;\n\n// Get a clean object: if it's an array, take the first element.\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Emit one item with a single field: \"data\"\nreturn [\n {\n json: {\n data: payload,\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "f766dece-26bd-4cb9-bb37-bb69b6c5b131",
"name": "整理成一个数据集群进行分析 (Bybit)",
"type": "n8n-nodes-base.code",
"position": [
-1216,
-1120
],
"parameters": {
"jsCode": "// Grab whatever this node receives.\n// It can be an array with 1 object (like your example) or a plain object.\nconst input = items?.[0]?.json;\n\n// Get a clean object: if it's an array, take the first element.\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Emit one item with a single field: \"data\"\nreturn [\n {\n json: {\n data: payload,\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "45580616-e460-4b37-a038-6e89ea087c6e",
"name": "计算流动性、阻力和支撑 (Coinbase)",
"type": "n8n-nodes-base.code",
"position": [
-1520,
-1856
],
"parameters": {
"jsCode": "// Coinbase pricebook -> Liquidity report (Coinbase header)\n\n// Accept either [{ pricebook:{...} }] or { pricebook:{...} }\nconst input = items[0]?.json;\nconst book = Array.isArray(input) ? input[0]?.pricebook : input?.pricebook;\n\nif (!book || (!book.bids && !book.asks)) {\n return [{ json: { error: 'No pricebook in input', raw: items[0]?.json } }];\n}\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\n// Map Coinbase objects {price,size} -> [price, qty]\nconst bids = (book.bids || []).map(o => [toNum(o.price), toNum(o.size)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (book.asks || []).map(o => [toNum(o.price), toNum(o.size)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids or asks', product_id: book.product_id } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // notional threshold (kept for parity)\n\n// Total liquidity (entire snapshot)\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n .sort((a,b)=>b.usd-a.usd).slice(0,200);\n const clusters=[];\n for(const s of seed){\n const [lo,hi]=band(s.price);\n const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n acc.notional+=notional(p,q);\n acc.qty+=q;\n acc.min=Math.min(acc.min,p);\n acc.max=Math.max(acc.max,p);\n return acc;\n },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n if(agg.notional>0) clusters.push(agg);\n }\n clusters.sort((a,b)=>b.notional-a.notional);\n const chosen=[];\n for(const c of clusters){\n const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n if(!overlaps) chosen.push(c);\n if(chosen.length>=5) break;\n }\n chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Build human-readable report ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\nconst sym = book.product_id || $json.symbol || \"BTC-USD\";\n\nconst report =\n`Coinbase Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || \"none\"}\nResistance lines (clustered): ${resistanceLines || \"none\"}`;\n\n// --- Return both JSON + report string\nreturn [{\n json: {\n exchange: \"Coinbase\",\n symbol: sym,\n // Coinbase pricebook may expose a sequence or timestamp; map if present\n lastUpdateId: book.sequence ?? null,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "ead99762-ca50-47c5-af17-57fceab89879",
"name": "计算流动性、阻力和支撑 (币安)",
"type": "n8n-nodes-base.code",
"position": [
-1520,
-1568
],
"parameters": {
"jsCode": "// Binance depth snapshot -> Liquidity report (Binance header)\n\nconst depth = items[0].json;\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = depth.bids.map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = depth.asks.map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // notional threshold\n\n// Total liquidity\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n .sort((a,b)=>b.usd-a.usd).slice(0,200);\n const clusters=[];\n for(const s of seed){\n const [lo,hi]=band(s.price);\n const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n acc.notional+=notional(p,q);\n acc.qty+=q;\n acc.min=Math.min(acc.min,p);\n acc.max=Math.max(acc.max,p);\n return acc;\n },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n if(agg.notional>0) clusters.push(agg);\n }\n clusters.sort((a,b)=>b.notional-a.notional);\n const chosen=[];\n for(const c of clusters){\n const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n if(!overlaps) chosen.push(c);\n if(chosen.length>=5) break;\n }\n chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Build human-readable report ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\nconst report =\n`Binance Exchange — Liquidity Report for ${$json.symbol || \"BTCUSDT\"}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth 5000): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || \"none\"}\nResistance lines (clustered): ${resistanceLines || \"none\"}`;\n\n// --- Return both JSON + report string\nreturn [{\n json: {\n symbol: $json.symbol || \"BTCUSDT\",\n lastUpdateId: depth.lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "4138ea50-b071-4d28-8568-4478a4e15b4a",
"name": "计算流动性、阻力和支撑 (Bybit)",
"type": "n8n-nodes-base.code",
"position": [
-1520,
-1120
],
"parameters": {
"jsCode": "// Bybit depth snapshot -> Liquidity report (Bybit header)\n\nconst depth = (items[0]?.json?.result) ? items[0].json.result : items[0]?.json;\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = (depth.b || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (depth.a || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from Bybit orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // notional threshold\n\n// Total liquidity\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n .sort((a,b)=>b.usd-a.usd).slice(0,200);\n const clusters=[];\n for(const s of seed){\n const [lo,hi]=band(s.price);\n const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n acc.notional+=notional(p,q);\n acc.qty+=q;\n acc.min=Math.min(acc.min,p);\n acc.max=Math.max(acc.max,p);\n return acc;\n },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n if(agg.notional>0) clusters.push(agg);\n }\n clusters.sort((a,b)=>b.notional-a.notional);\n const chosen=[];\n for(const c of clusters){\n const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n if(!overlaps) chosen.push(c);\n if(chosen.length>=5) break;\n }\n chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Build human-readable report ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\nconst sym = depth.s || $json.symbol || \"BTCUSDT\";\n\nconst report =\n`Bybit Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || \"none\"}\nResistance lines (clustered): ${resistanceLines || \"none\"}`;\n\n// --- Return both JSON + report string\nreturn [{\n json: {\n symbol: sym,\n // Bybit v5 orderbook doesn't provide lastUpdateId; keep null for compatibility\n lastUpdateId: items[0]?.json?.result?.u ?? items[0]?.json?.u ?? null,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "22f6a76b-076e-422f-897f-bc46c5a24c11",
"name": "合并交易所数据",
"type": "n8n-nodes-base.merge",
"position": [
-624,
-816
],
"parameters": {
"numberInputs": 10
},
"executeOnce": false,
"typeVersion": 3.2
},
{
"id": "735b52c4-6bcf-4d6a-8791-c7c37b46e0f8",
"name": "合并成一个报告",
"type": "n8n-nodes-base.code",
"position": [
-272,
-1040
],
"parameters": {
"jsCode": "// Collect the \"data\" object from each incoming item\nconst payloads = items.map(i => i.json?.data ?? i.json ?? {});\n\n// Pull out the 'report' strings, skip empties\nconst reports = payloads\n .map(p => p?.report)\n .filter(r => typeof r === 'string' && r.trim().length);\n\n// Optional: add a header timestamp\nconst header = `BTC Liquidity Snapshot — ${new Date().toISOString()}`;\n\n// Join reports with separators\nconst body = reports.join('\\n\\n— — — — — — — — —\\n\\n');\n\n// Final message text for Telegram\nconst text = `${header}\\n\\n${body}`.trim();\n\n// Emit ONE item with a `text` field\nreturn [\n {\n json: { text }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "337565e5-1993-4327-b278-2df5e902108a",
"name": "OpenAI 聊天模型",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
-32,
-288
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1-mini"
},
"options": {}
},
"credentials": {
"openAiApi": {
"id": "yUizd8t0sD5wMYVG",
"name": "OpenAi account"
}
},
"typeVersion": 1.2
},
{
"id": "0834fa76-ae68-4204-aa3d-b6f8bb5279d9",
"name": "如果消息超过 4000 字符则拆分",
"type": "n8n-nodes-base.code",
"position": [
336,
-464
],
"parameters": {
"jsCode": "// Input: assumes incoming message in `item.json.message`\nconst input = $json.output;\nconst chunkSize = 4000;\n\n// Function to split text\nfunction splitMessage(text, size) {\n const result = [];\n for (let i = 0; i < text.length; i += size) {\n result.push(text.substring(i, i + size));\n }\n return result;\n}\n\n// Logic\nif (input.length <= chunkSize) {\n return [{ json: { message: input } }];\n} else {\n const chunks = splitMessage(input, chunkSize);\n return chunks.map(chunk => ({ json: { message: chunk } }));\n}"
},
"typeVersion": 2
},
{
"id": "7d6d1f1d-20cd-4401-a8af-c031528fd75a",
"name": "MEXC (比特币-USDT 订单簿)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1840,
-1328
],
"parameters": {
"url": "https://api.mexc.com/api/v3/depth",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "symbol",
"value": "BTCUSDT"
},
{
"name": "limit",
"value": "5000"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "1c838693-f752-429c-b61d-3c36280a38da",
"name": "整理成一个数据集群进行分析 (MEXC)",
"type": "n8n-nodes-base.code",
"position": [
-1216,
-1328
],
"parameters": {
"jsCode": "// MEXC -> Wrap whatever this node receives into json.data\n// Accepts either a plain object or an array with one object (as MEXC /api/v3/depth returns)\n\nconst input = items?.[0]?.json;\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\nreturn [\n {\n json: {\n data: payload,\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "675007eb-4b8b-425e-b434-9c4cd1ced692",
"name": "Gate (比特币-USDT 订单簿)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1840,
-864
],
"parameters": {
"url": "https://api.gateio.ws/api/v4/spot/order_book",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "currency_pair",
"value": "BTC_USDT"
},
{
"name": "limit",
"value": "5000"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "4049e954-1c4b-43fb-a6ec-f71fe6dc4f05",
"name": "计算流动性、阻力和支撑 (Gate.io)",
"type": "n8n-nodes-base.code",
"position": [
-1520,
-864
],
"parameters": {
"jsCode": "// Gate.io depth snapshot -> Liquidity report (Gate.io header)\n\nconst depth = items[0]?.json ?? {};\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = (depth.bids || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (depth.asks || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from Gate.io orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // notional threshold (kept for future flagging)\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n .sort((a,b)=>b.usd-a.usd).slice(0,200);\n const clusters=[];\n for(const s of seed){\n const [lo,hi]=band(s.price);\n const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n acc.notional+=notional(p,q);\n acc.qty+=q;\n acc.min=Math.min(acc.min,p);\n acc.max=Math.max(acc.max,p);\n return acc;\n },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n if(agg.notional>0) clusters.push(agg);\n }\n clusters.sort((a,b)=>b.notional-a.notional);\n const chosen=[];\n for(const c of clusters){\n const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n if(!overlaps) chosen.push(c);\n if(chosen.length>=5) break;\n }\n chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\n// Gate.io response doesn't echo symbol; allow upstream to pass it through on the item if desired\nconst sym = $json.currency_pair || $json.symbol || 'BTC_USDT';\n\nconst report =\n`Gate.io Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n // Gate provides two sequence-ish fields; keep both\n lastUpdateId: depth.update ?? depth.current ?? null,\n gateMeta: { current: depth.current ?? null, update: depth.update ?? null },\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "3201f266-baf9-4da2-a7cc-3e9d358df6d0",
"name": "整理成一个数据集群进行分析 (Gate.io)",
"type": "n8n-nodes-base.code",
"position": [
-1216,
-864
],
"parameters": {
"jsCode": "// Gate.io -> Wrap whatever this node receives into json.data\n// Accepts either a plain object (Gate /api/v4/spot/order_book) or an array with one object.\n\nconst input = items?.[0]?.json;\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\nreturn [\n {\n json: {\n data: payload,\n // Optional convenience: expose symbol if upstream passed currency_pair\n symbol: $json.currency_pair ?? $json.symbol ?? undefined\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "fdc7a08f-869a-41fe-b17f-8f33635d0a39",
"name": "如果消息超过 4000 字符则拆分",
"type": "n8n-nodes-base.code",
"position": [
336,
-1040
],
"parameters": {
"jsCode": "// Input: assumes incoming message in `item.json.text`\nconst input = $json.text;\nconst chunkSize = 4000;\n\n// Function to split text into chunks\nfunction splitMessage(text, size) {\n const result = [];\n for (let i = 0; i < text.length; i += size) {\n result.push(text.substring(i, i + size));\n }\n return result;\n}\n\n// Logic: if small enough, emit single item\nif (!input || input.length <= chunkSize) {\n return [{ json: { message: input } }];\n} else {\n const chunks = splitMessage(input, chunkSize);\n return chunks.map(chunk => ({ json: { message: chunk } }));\n}"
},
"typeVersion": 2
},
{
"id": "9c6be517-72e3-4fb7-8aad-a6b85d4e3ed9",
"name": "计算流动性、阻力和支撑 (Bitget)",
"type": "n8n-nodes-base.code",
"position": [
-1504,
-592
],
"parameters": {
"jsCode": "// Bitget depth snapshot -> Liquidity report (Bitget header)\n\nconst body = items[0]?.json ?? {};\nconst depth = body.data ?? {};\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\nfunction sumQty(rows) { return rows.reduce((a, [, q]) => a + q, 0); }\n\n// Bitget returns arrays of [price, size] as strings\nconst bids = (depth.bids || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => b[0] - a[0]);\nconst asks = (depth.asks || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from Bitget orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // (kept for parity / future use)\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n const seed = side\n .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n .sort((a, b) => b.usd - a.usd)\n .slice(0, 200);\n\n const clusters = [];\n for (const s of seed) {\n const [lo, hi] = band(s.price);\n const agg = side\n .filter(([p]) => p >= lo && p <= hi)\n .reduce((acc, [p, q]) => {\n acc.notional += notional(p, q);\n acc.qty += q;\n acc.min = Math.min(acc.min, p);\n acc.max = Math.max(acc.max, p);\n return acc;\n }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n if (agg.notional > 0) clusters.push(agg);\n }\n\n clusters.sort((a, b) => b.notional - a.notional);\n const chosen = [];\n for (const c of clusters) {\n const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n if (!overlaps) chosen.push(c);\n if (chosen.length >= 5) break;\n }\n chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x) { return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x, d = 2) { return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines = supportZones.map(z => fmtNum(z.min, 2) + \"-\" + fmtNum(z.max, 2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z => fmtNum(z.min, 2) + \"-\" + fmtNum(z.max, 2)).join(\", \");\n\n// Bitget symbol & timestamp\nconst sym = $json.symbol || 'BTCUSDT'; // your HTTP node uses BTCUSDT_SPBL; normalize for display\nconst lastUpdateId = depth.ts ?? body.requestTime ?? null;\n\nconst report =\n`Bitget Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid, 2)} | Spread: ${fmtNum(spread, 2)} (${fmtNum(spreadBps, 2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "49909b61-c69d-4ee1-a511-575bd6a5e5f4",
"name": "Bitget (比特币-USDT 订单簿)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1840,
-592
],
"parameters": {
"url": "https://api.bitget.com/api/spot/v1/market/depth",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "symbol",
"value": "BTCUSDT_SPBL"
},
{
"name": "limit",
"value": "5000"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "a055c8f2-e70c-40fa-ba2b-ffee91982954",
"name": "计算流动性、阻力和支撑 (MEXC)",
"type": "n8n-nodes-base.code",
"position": [
-1520,
-1328
],
"parameters": {
"jsCode": "// MEXC depth snapshot -> Liquidity report (MEXC header)\n\nconst depth = items[0]?.json ?? {};\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p,q){ return p*q; }\nfunction sumNotional(rows){ return rows.reduce((a,[p,q])=>a+notional(p,q),0); }\nfunction sumQty(rows){ return rows.reduce((a,[,q])=>a+q,0); }\n\nconst bids = (depth.bids || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>b[0]-a[0]);\nconst asks = (depth.asks || []).map(([p,q])=>[toNum(p),toNum(q)]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from MEXC orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid+bestAsk)/2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // notional threshold (kept for future flagging)\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side,isBid){\n const band = p=>[p*(1-CLUSTER_BPS/10000), p*(1+CLUSTER_BPS/10000)];\n const seed = side.map(([p,q])=>({price:p, usd:notional(p,q)}))\n .sort((a,b)=>b.usd-a.usd).slice(0,200);\n const clusters=[];\n for(const s of seed){\n const [lo,hi]=band(s.price);\n const agg=side.filter(([p])=>p>=lo&&p<=hi).reduce((acc,[p,q])=>{\n acc.notional+=notional(p,q);\n acc.qty+=q;\n acc.min=Math.min(acc.min,p);\n acc.max=Math.max(acc.max,p);\n return acc;\n },{center:s.price,min:+Infinity,max:-Infinity,qty:0,notional:0});\n if(agg.notional>0) clusters.push(agg);\n }\n clusters.sort((a,b)=>b.notional-a.notional);\n const chosen=[];\n for(const c of clusters){\n const overlaps=chosen.some(x=>!(c.max<x.min||c.min>x.max));\n if(!overlaps) chosen.push(c);\n if(chosen.length>=5) break;\n }\n chosen.sort((a,b)=>isBid?b.min-a.min:a.min-b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids,true);\nconst resistanceZones = clusterSide(asks,false);\n\n// --- Spread ---\nconst spread = bestAsk-bestBid;\nconst spreadBps = (spread/mid)*10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){return \"$\"+x.toLocaleString(undefined,{maximumFractionDigits:0});}\nfunction fmtNum(x,d=2){return x?.toLocaleString(undefined,{maximumFractionDigits:d});}\n\nconst supportLines = supportZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\nconst resistanceLines = resistanceZones.map(z=>fmtNum(z.min,2)+\"-\"+fmtNum(z.max,2)).join(\", \");\n\n// MEXC response doesn't echo symbol; allow upstream to pass it through on the item if desired\nconst sym = $json.symbol || 'BTCUSDT';\n\nconst report =\n`MEXC Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId: depth.lastUpdateId ?? null,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "8e2e662c-9481-4613-9a59-ef01806a60f7",
"name": "整理成一个数据集群进行分析 (Bitget)",
"type": "n8n-nodes-base.code",
"position": [
-1216,
-592
],
"parameters": {
"jsCode": "// Bitget -> Wrap whatever this node receives into json.data\n// Accepts either a plain object (Bitget /api/spot/v1/market/depth) or an array with one object.\n\nconst input = items?.[0]?.json;\nconst payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\nconst depth = payload.data ?? payload; // Bitget nests bids/asks under .data\n\nreturn [\n {\n json: {\n data: depth,\n // Optional convenience: expose symbol if upstream passed one\n symbol: $json.symbol ?? payload.symbol ?? undefined,\n // Preserve requestTime as a \"lastUpdateId\"-style field\n lastUpdateId: payload.requestTime ?? null\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "6b30a489-2691-41c9-9a09-7cb42845d211",
"name": "OKX (比特币-USDT 订单簿)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1840,
-352
],
"parameters": {
"url": "https://www.okx.com/api/v5/market/books-full",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "instId",
"value": "BTC-USDT"
},
{
"name": "sz",
"value": "5000"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "e66dbe8b-96de-4423-bbf0-99048bbcd0a6",
"name": "计算流动性、阻力和支撑 (OKX)",
"type": "n8n-nodes-base.code",
"position": [
-1504,
-352
],
"parameters": {
"jsCode": "// OKX depth snapshot -> Liquidity report (OKX header)\n\nconst body = items[0]?.json ?? {};\nconst row = Array.isArray(body.data) ? body.data[0] : undefined;\n\nif (!row) {\n return [{ json: { error: 'No OKX book in response', raw: items[0]?.json } }];\n}\n\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// OKX arrays can be [price, size, count]; keep first two\nconst bids = (row.bids || []).map(lvl => [toNum(lvl[0]), toNum(lvl[1])]).sort((a,b)=>b[0]-a[0]);\nconst asks = (row.asks || []).map(lvl => [toNum(lvl[0]), toNum(lvl[1])]).sort((a,b)=>a[0]-b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing OKX bids/asks', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // reserved for future flagging\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n const seed = side\n .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n .sort((a, b) => b.usd - a.usd)\n .slice(0, 200);\n\n const clusters = [];\n for (const s of seed) {\n const [lo, hi] = band(s.price);\n const agg = side\n .filter(([p]) => p >= lo && p <= hi)\n .reduce((acc, [p, q]) => {\n acc.notional += notional(p, q);\n acc.qty += q;\n acc.min = Math.min(acc.min, p);\n acc.max = Math.max(acc.max, p);\n return acc;\n }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n if (agg.notional > 0) clusters.push(agg);\n }\n\n clusters.sort((a, b) => b.notional - a.notional);\n const chosen = [];\n for (const c of clusters) {\n const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n if (!overlaps) chosen.push(c);\n if (chosen.length >= 5) break;\n }\n chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){ return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x,d=2){ return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\nconst sym = $json.instId || 'BTC-USDT';\nconst lastUpdateId = row.ts ?? body.ts ?? null;\n\nconst report =\n`OKX Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "259479b4-e4fc-4a60-91f0-e6ee4b7a11c7",
"name": "整理成一个数据集群进行分析 (OKX)",
"type": "n8n-nodes-base.code",
"position": [
-1216,
-352
],
"parameters": {
"jsCode": "// OKX -> Wrap whatever this node receives into json.data\n// Works with:\n// 1) Raw OKX response: { data: [ { bids: [...], asks: [...], ts: \"...\" } ] }\n// 2) Your report-shape arr acay: [ { symbol, lastUpdateId, mid, report, ... } ]\n// 3) Or a single report object\n\nconst input = items?.[0]?.json;\n\n// Step 1: normalize to a single object\nlet payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Step 2: detect if it's already a computed report object\nconst looksLikeReport =\n typeof payload.mid === 'number' &&\n typeof payload.report === 'string' &&\n (payload.supportZones || payload.resistanceZones);\n\n// If it's not a report yet, try drilling into OKX raw shape (data[0])\nif (!looksLikeReport) {\n const row = Array.isArray(payload.data) ? payload.data[0] : payload.data;\n if (row && typeof row === 'object') payload = row;\n}\n\n// Step 3: build wrapper with helpful metadata\nconst symbol =\n payload.symbol ??\n $json.instId ?? // from query param if present\n $json.symbol ??\n 'BTC-USDT';\n\nconst lastUpdateId =\n payload.lastUpdateId ?? // report shape\n payload.ts ?? // OKX raw row ts\n (Array.isArray(input?.data) ? input.data[0]?.ts : input?.ts) ??\n null;\n\nreturn [\n {\n json: {\n data: payload, // either the report object OR the raw row (bids/asks)\n symbol,\n lastUpdateId,\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "12622d19-c17b-42d7-b4ca-805b5a4503f1",
"name": "Kraken (比特币-USDT 订单簿)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1840,
-80
],
"parameters": {
"url": "https://api.kraken.com/0/public/Depth",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "pair",
"value": "BTCUSDT"
},
{
"name": "count",
"value": "5000"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "dd44cb84-3aec-4f9c-a35e-f3dea4785cfc",
"name": "计算流动性、阻力和支撑 (Kraken)",
"type": "n8n-nodes-base.code",
"position": [
-1504,
-80
],
"parameters": {
"jsCode": "// Kraken depth snapshot -> Liquidity report (Kraken header)\n\nconst body = items[0]?.json ?? {};\nconst result = body.result ?? {};\n\n// Kraken nests the book under an unknown key (e.g., \"XBTUSDT\")\nconst pairKey = Object.keys(result)[0];\nconst depth = pairKey ? (result[pairKey] ?? {}) : {};\n\n// Helpers\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// Kraken levels are [price, volume, timestamp]; we only need price & volume\nconst bids = (depth.bids || [])\n .map(([p, q]) => [toNum(p), toNum(q)])\n .sort((a, b) => b[0] - a[0]);\n\nconst asks = (depth.asks || [])\n .map(([p, q]) => [toNum(p), toNum(q)])\n .sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from Kraken orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // reserved for future use\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n const seed = side\n .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n .sort((a, b) => b.usd - a.usd)\n .slice(0, 200);\n\n const clusters = [];\n for (const s of seed) {\n const [lo, hi] = band(s.price);\n const agg = side\n .filter(([p]) => p >= lo && p <= hi)\n .reduce((acc, [p, q]) => {\n acc.notional += notional(p, q);\n acc.qty += q;\n acc.min = Math.min(acc.min, p);\n acc.max = Math.max(acc.max, p);\n return acc;\n }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n if (agg.notional > 0) clusters.push(agg);\n }\n\n clusters.sort((a, b) => b.notional - a.notional);\n const chosen = [];\n for (const c of clusters) {\n const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n if (!overlaps) chosen.push(c);\n if (chosen.length >= 5) break;\n }\n chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x) { return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x, d = 2) { return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\n// Symbol & \"lastUpdateId\"\nconst sym = $json.pair || pairKey || 'XBTUSDT';\n// Use the newest level timestamp we see, or null if absent\nconst lastUpdateId = (() => {\n const bts = (depth.bids || []).map(l => Number(l[2]) || 0);\n const ats = (depth.asks || []).map(l => Number(l[2]) || 0);\n const mx = Math.max(...bts, ...ats, 0);\n return mx > 0 ? String(mx) : null;\n})();\n\nconst report =\n`Kraken Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "8d0bed3f-89dd-4e27-a7b3-03f0512473a2",
"name": "整理成一个数据集群进行分析 (Kraken)",
"type": "n8n-nodes-base.code",
"position": [
-1216,
-80
],
"parameters": {
"jsCode": "// Kraken -> Wrap whatever this node receives into json.data\n// Works with:\n// 1) Raw Kraken response: { result: { <PAIR>: { bids:[[p, q, ts]...], asks:[[p, q, ts]...] } } }\n// 2) Report-shape array: [ { symbol, lastUpdateId, mid, report, ... } ]\n// 3) Single report object\n\nconst input = items?.[0]?.json;\n\n// Step 1: normalize to a single object\nlet payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Step 2: detect if it's already a computed report object\nconst looksLikeReport =\n typeof payload.mid === 'number' &&\n typeof payload.report === 'string' &&\n (payload.supportZones || payload.resistanceZones);\n\n// If it's not a report yet, drill into Kraken raw shape: result[PAIR]\nif (!looksLikeReport) {\n const result = payload.result ?? {};\n const pairKey = Object.keys(result)[0];\n const depth = pairKey ? (result[pairKey] ?? {}) : {};\n payload = depth;\n}\n\n// Helper to compute a \"lastUpdateId\" from level timestamps if present\nfunction latestTsFromDepth(d) {\n const bts = Array.isArray(d?.bids) ? d.bids.map(l => Number(l?.[2]) || 0) : [];\n const ats = Array.isArray(d?.asks) ? d.asks.map(l => Number(l?.[2]) || 0) : [];\n const mx = Math.max(0, ...bts, ...ats);\n return mx > 0 ? String(mx) : null;\n}\n\n// Step 3: build wrapper with helpful metadata\nconst symbol =\n payload.symbol ??\n $json.pair ?? // from query param if present\n (input?.result ? Object.keys(input.result)[0] : undefined) ??\n 'XBTUSDT';\n\nconst lastUpdateId =\n payload.lastUpdateId ?? // report shape\n latestTsFromDepth(payload) ?? // from raw depth timestamps\n null;\n\nreturn [\n {\n json: {\n data: payload, // report object OR raw {bids, asks}\n symbol,\n lastUpdateId,\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "a0f11725-7b0a-4ebf-821f-8b2a290b0ec5",
"name": "HTX (比特币-USDT 订单簿)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1840,
208
],
"parameters": {
"url": "https://api.huobi.pro/market/depth",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "symbol",
"value": "btcusdt"
},
{
"name": "type",
"value": "step0"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "1987a40f-b9a5-48c0-a2f4-3484e264e490",
"name": "计算流动性、阻力和支撑 (HTX)",
"type": "n8n-nodes-base.code",
"position": [
-1504,
208
],
"parameters": {
"jsCode": "// HTX (Huobi) depth snapshot -> Liquidity report (HTX header)\n\nconst body = items[0]?.json ?? {};\nconst tick = body.tick ?? {};\n\n// Helpers\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// HTX levels are [price, size]; ensure numbers & sort\nconst bids = (tick.bids || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => b[0] - a[0]);\nconst asks = (tick.asks || []).map(([p, q]) => [toNum(p), toNum(q)]).sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing bids/asks from HTX orderbook', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // reserved for future use\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n const seed = side\n .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n .sort((a, b) => b.usd - a.usd)\n .slice(0, 200);\n\n const clusters = [];\n for (const s of seed) {\n const [lo, hi] = band(s.price);\n const agg = side\n .filter(([p]) => p >= lo && p <= hi)\n .reduce((acc, [p, q]) => {\n acc.notional += notional(p, q);\n acc.qty += q;\n acc.min = Math.min(acc.min, p);\n acc.max = Math.max(acc.max, p);\n return acc;\n }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n if (agg.notional > 0) clusters.push(agg);\n }\n\n clusters.sort((a, b) => b.notional - a.notional);\n const chosen = [];\n for (const c of clusters) {\n const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n if (!overlaps) chosen.push(c);\n if (chosen.length >= 5) break;\n }\n chosen.sort((a, b) => isBid ? b.min - a.min : a.min - b.min);\n return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){ return \"$\" + x.toLocaleString(undefined,{maximumFractionDigits:0}); }\nfunction fmtNum(x,d=2){ return x?.toLocaleString(undefined,{maximumFractionDigits:d}); }\n\nconst supportLines = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\n// Symbol & \"lastUpdateId\"\nconst sym = $json.symbol || 'BTCUSDT';\nconst lastUpdateId = String(body.ts ?? tick.ts ?? '') || null;\n\nconst report =\n`HTX (Huobi) — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "0f0cbc28-4b34-4be3-a999-412a1ca2685a",
"name": "整理成一个数据集群进行分析 (HTX)",
"type": "n8n-nodes-base.code",
"position": [
-1216,
208
],
"parameters": {
"jsCode": "// HTX (Huobi) -> Wrap whatever this node receives into json.data\n// Works with:\n// 1) Raw HTX response: { status, ts, tick: { bids:[[p,q]...], asks:[[p,q]...] } }\n// 2) Report-shape array: [ { symbol, lastUpdateId, mid, report, ... } ]\n// 3) Single report object\n\nconst input = items?.[0]?.json;\n\n// Step 1: normalize to a single object\nlet payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Step 2: detect if it's already a computed report object\nconst looksLikeReport =\n typeof payload.mid === 'number' &&\n typeof payload.report === 'string' &&\n (payload.supportZones || payload.resistanceZones);\n\n// If it's not a report yet, drill into HTX raw shape: payload.tick\nif (!looksLikeReport) {\n const depth = payload.tick ?? {};\n payload = depth;\n}\n\n// HTX levels don't carry per-level timestamps; use top-level ts\nconst topTs =\n (Array.isArray(items?.[0]?.json?.data) ? items[0].json.data?.[0]?.ts : null) ??\n items?.[0]?.json?.ts ?? null;\n\n// Step 3: build wrapper with helpful metadata\nconst symbol =\n payload.symbol ??\n $json.symbol ?? // from query param if present (e.g., btcusdt)\n 'btcusdt';\n\nconst lastUpdateId =\n payload.lastUpdateId ?? // report shape\n topTs ?? // raw HTX response timestamp\n null;\n\nreturn [\n {\n json: {\n data: payload, // report object OR raw {bids, asks}\n symbol,\n lastUpdateId,\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "d32ad6fb-d65c-4c31-a281-0869a7416f11",
"name": "Crypto.com (比特币-USDT 订单簿)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1840,
480
],
"parameters": {
"url": "https://api.crypto.com/exchange/v1/public/get-book",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "instrument_name",
"value": "BTC_USDT"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "3d9d4b6e-207b-44bd-a077-ce13357a9010",
"name": "计算流动性、阻力和支撑 (Crypto.com)",
"type": "n8n-nodes-base.code",
"position": [
-1504,
480
],
"parameters": {
"jsCode": "// Crypto.com depth snapshot -> Liquidity report (Crypto.com header)\n\nconst body = items[0]?.json ?? {};\n\n// Crypto.com sometimes returns result:{data:[{...}]} — grab the first row.\n// (Very rarely some SDKs expose result directly with bids/asks; handle both.)\nconst result = body.result ?? body.data ?? {};\nconst row = Array.isArray(result.data) ? (result.data[0] ?? {}) : result;\n\n// Helpers\nfunction toNum(x) { return Number(x); }\nfunction notional(p, q) { return p * q; }\nfunction sumNotional(rows) { return rows.reduce((a, [p, q]) => a + notional(p, q), 0); }\n\n// Crypto.com levels are typically [price, size] or [price, size, count]; use first two.\nconst bids = (row.bids || []).map(l => [toNum(l[0]), toNum(l[1])]).sort((a, b) => b[0] - a[0]);\nconst asks = (row.asks || []).map(l => [toNum(l[0]), toNum(l[1])]).sort((a, b) => a[0] - b[0]);\n\nif (!bids.length || !asks.length) {\n return [{ json: { error: 'Missing Crypto.com bids/asks', raw: items[0]?.json } }];\n}\n\nconst bestBid = bids[0][0];\nconst bestAsk = asks[0][0];\nconst mid = (bestBid + bestAsk) / 2;\n\n// --- Parameters ---\nconst CLUSTER_BPS = 20; // cluster width (±0.20%)\nconst WALL_MIN_USD = 250000; // reserved for future flagging\n\n// Totals\nconst totalBidNotional = sumNotional(bids);\nconst totalAskNotional = sumNotional(asks);\nconst totalLiquidity = totalBidNotional + totalAskNotional;\n\n// --- Clustering for support/resistance ---\nfunction clusterSide(side, isBid) {\n const band = p => [p * (1 - CLUSTER_BPS / 10000), p * (1 + CLUSTER_BPS / 10000)];\n const seed = side\n .map(([p, q]) => ({ price: p, usd: notional(p, q) }))\n .sort((a, b) => b.usd - a.usd)\n .slice(0, 200);\n\n const clusters = [];\n for (const s of seed) {\n const [lo, hi] = band(s.price);\n const agg = side\n .filter(([p]) => p >= lo && p <= hi)\n .reduce((acc, [p, q]) => {\n acc.notional += notional(p, q);\n acc.qty += q;\n acc.min = Math.min(acc.min, p);\n acc.max = Math.max(acc.max, p);\n return acc;\n }, { center: s.price, min: +Infinity, max: -Infinity, qty: 0, notional: 0 });\n if (agg.notional > 0) clusters.push(agg);\n }\n\n clusters.sort((a, b) => b.notional - a.notional);\n const chosen = [];\n for (const c of clusters) {\n const overlaps = chosen.some(x => !(c.max < x.min || c.min > x.max));\n if (!overlaps) chosen.push(c);\n if (chosen.length >= 5) break;\n }\n chosen.sort((a, b) => (isBid ? b.min - a.min : a.min - b.min));\n return chosen;\n}\n\nconst supportZones = clusterSide(bids, true);\nconst resistanceZones = clusterSide(asks, false);\n\n// --- Spread ---\nconst spread = bestAsk - bestBid;\nconst spreadBps = (spread / mid) * 10000;\n\n// --- Formatting ---\nfunction fmtUsd(x){ return \"$\" + x.toLocaleString(undefined, { maximumFractionDigits: 0 }); }\nfunction fmtNum(x,d=2){ return x?.toLocaleString(undefined, { maximumFractionDigits: d }); }\n\nconst supportLines = supportZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\nconst resistanceLines = resistanceZones.map(z => `${fmtNum(z.min,2)}-${fmtNum(z.max,2)}`).join(\", \");\n\n// Symbol & \"lastUpdateId\"\nconst sym = row.instrument_name || $json.instrument_name || 'BTC_USDT';\nconst lastUpdateId = String(row.t ?? result.t ?? '') || null;\n\nconst report =\n`Crypto.com Exchange — Liquidity Report for ${sym}\nMid Price: ${fmtNum(mid,2)} | Spread: ${fmtNum(spread,2)} (${fmtNum(spreadBps,2)} bps)\n\nTotal Liquidity (depth snapshot): ${fmtUsd(totalLiquidity)}\n - Bid Liquidity: ${fmtUsd(totalBidNotional)}\n - Ask Liquidity: ${fmtUsd(totalAskNotional)}\n\nSupport lines (clustered): ${supportLines || 'none'}\nResistance lines (clustered): ${resistanceLines || 'none'}`;\n\nreturn [{\n json: {\n symbol: sym,\n lastUpdateId,\n mid, bestBid, bestAsk, spread, spreadBps,\n totalBidNotional, totalAskNotional, totalLiquidity,\n supportZones, resistanceZones,\n generatedAt: new Date().toISOString(),\n report\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "ff5ce551-1dd9-4afe-a312-8c69d3509c9d",
"name": "整理成一个数据集群进行分析 (HTX)",
"type": "n8n-nodes-base.code",
"position": [
-1216,
480
],
"parameters": {
"jsCode": "// Crypto.com -> Wrap whatever this node receives into json.data\n// Works with:\n// 1) Raw response: { code, result:{ depth, data:[ { bids, asks, t, instrument_name? } ] } }\n// (Some SDKs expose { result:{ bids, asks, t, instrument_name } } without data[].)\n// 2) Report-shape array: [ { symbol, lastUpdateId, mid, report, ... } ]\n// 3) Single report object\n\nconst input = items?.[0]?.json;\n\n// Step 1: normalize to a single object\nlet payload = Array.isArray(input) ? (input[0] ?? {}) : (input ?? {});\n\n// Step 2: detect if it's already a computed report object\nconst looksLikeReport =\n typeof payload.mid === 'number' &&\n typeof payload.report === 'string' &&\n (payload.supportZones || payload.resistanceZones);\n\n// If it's not a report yet, drill into Crypto.com raw shapes\nlet row = payload;\nif (!looksLikeReport) {\n const result = payload.result ?? payload.data ?? {};\n // Prefer result.data[0], else result directly if it already has bids/asks\n row = Array.isArray(result.data) ? (result.data[0] ?? {}) : result;\n}\n\n// Step 3: build wrapper with helpful metadata\nconst symbol =\n row.instrument_name ??\n payload.symbol ??\n $json.instrument_name ?? // from HTTP query param, e.g. BTC_USDT\n 'BTC_USDT';\n\nconst lastUpdateId =\n payload.lastUpdateId ?? // report shape\n (row.t != null ? String(row.t) : null); // snapshot timestamp\n\nreturn [\n {\n json: {\n data: row, // report object OR raw {bids, asks, t, ...}\n symbol,\n lastUpdateId,\n },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "ab6896ce-115d-4a78-9c76-e1ac5d3b9032",
"name": "发送比特币多交易所流动性报告到频道",
"type": "n8n-nodes-base.telegram",
"position": [
624,
-1040
],
"webhookId": "55bbb98b-81b5-4629-9b7c-360bb0fa3fcd",
"parameters": {
"text": "={{ $json.message }}",
"chatId": "-1003052362843",
"additionalFields": {
"parse_mode": "=None",
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"id": "uRmQmYAMvgnSQWWS",
"name": "Treasurium_Signals_bot"
}
},
"typeVersion": 1.2
},
{
"id": "ff337127-416d-4ba4-9f0b-712fcde0ebe0",
"name": "发送一份 AI 撰写的包含可操作日内和周度信号的交易简报",
"type": "n8n-nodes-base.telegram",
"position": [
624,
-464
],
"webhookId": "7c945345-c98d-4a4e-a4ea-7e9085dba612",
"parameters": {
"text": "={{ $json.message }}",
"chatId": "-1003052362843",
"additionalFields": {
"parse_mode": "=None",
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"id": "uRmQmYAMvgnSQWWS",
"name": "Treasurium_Signals_bot"
}
},
"typeVersion": 1.2
},
{
"id": "4b187480-ec1d-41b2-8186-48287673ab69",
"name": "合并成一个输入用于分析",
"type": "n8n-nodes-base.code",
"position": [
-272,
-464
],
"parameters": {
"jsCode": "// n8n Code node (JavaScript)\n// Input: items = array of per-exchange snapshots (any of {json:{data}}, {data}, or raw {...})\n// Output: ONE item shaped as { json: { data: { ...nested consolidated payload... } } }\n\nfunction normalizeInput(items) {\n return items\n .map((it) => it?.json ?? it)\n .map((it) => it?.data ?? it)\n .filter(Boolean);\n}\n\nfunction parseSymbol(sym) {\n if (!sym || typeof sym !== 'string') return { base: null, quote: null, raw: sym };\n const s = sym.toUpperCase().replace(/[^A-Z0-9]/g, '');\n const QUOTES = ['USDT', 'USD', 'USDC', 'EUR', 'JPY', 'GBP', 'KRW', 'AUD'];\n for (const q of QUOTES) if (s.endsWith(q)) return { base: s.slice(0, -q.length), quote: q, raw: sym };\n const dash = sym.split('-');\n if (dash.length === 2) return { base: dash[0].toUpperCase(), quote: dash[1].toUpperCase(), raw: sym };\n return { base: null, quote: null, raw: sym };\n}\n\nfunction safeNumber(x, fallback = 0) {\n const n = Number(x);\n return Number.isFinite(n) ? n : fallback;\n}\n\nfunction latestISO(dates) {\n const valid = dates.map(d => ({ d, t: Date.parse(d) })).filter(x => Number.isFinite(x.t)).sort((a,b)=>b.t-a.t);\n return valid[0]?.d ?? null;\n}\n\nfunction weightedAverage(values, weights) {\n let num = 0, den = 0;\n for (let i = 0; i < values.length; i++) {\n const w = safeNumber(weights[i], 0);\n num += safeNumber(values[i], 0) * w;\n den += w;\n }\n return den > 0 ? num / den : null;\n}\n\nfunction computeGlobalTopOfBook(payloads) {\n const bids = payloads.map(p => safeNumber(p.bestBid, -Infinity));\n const asks = payloads.map(p => safeNumber(p.bestAsk, +Infinity));\n const bestBid = Math.max(...bids);\n const bestAsk = Math.min(...asks);\n const spread = (Number.isFinite(bestBid) && Number.isFinite(bestAsk)) ? (bestAsk - bestBid) : null;\n const spreadBps = (bestBid > 0 && spread !== null) ? (spread / bestBid) * 10000 : null;\n return { bestBid, bestAsk, spread, spreadBps };\n}\n\nfunction flattenZones(payloads, key) {\n const out = [];\n for (const p of payloads) {\n const exch = p.exchange ?? p.symbol ?? 'Unknown';\n const zones = Array.isArray(p[key]) ? p[key] : [];\n zones.forEach(z => {\n out.push({\n exchange: exch,\n center: safeNumber(z.center, null),\n min: safeNumber(z.min, null),\n max: safeNumber(z.max, null),\n qty: safeNumber(z.qty, 0),\n notional: safeNumber(z.notional, 0),\n });\n });\n }\n return out.filter(z => Number.isFinite(z.min) && Number.isFinite(z.max) && z.min <= z.max);\n}\n\nfunction mergeOverlappingZones(zones) {\n if (!zones.length) return [];\n zones.sort((a, b) => a.min - b.min);\n\n const merged = [];\n let cur = { ...zones[0], exchanges: zones[0].exchange ? [zones[0].exchange] : [] };\n\n const accum = (dst, src) => {\n const notional = dst.notional + src.notional;\n const qty = dst.qty + src.qty;\n const center =\n (dst.center * dst.notional + src.center * src.notional) / (notional || 1);\n dst.min = Math.min(dst.min, src.min);\n dst.max = Math.max(dst.max, src.max);\n dst.center = Number.isFinite(center) ? center : (dst.center ?? src.center ?? null);\n dst.qty = qty;\n dst.notional = notional;\n dst.exchanges = Array.from(new Set([...(dst.exchanges ?? []), src.exchange].filter(Boolean)));\n return dst;\n };\n\n for (let i = 1; i < zones.length; i++) {\n const z = { ...zones[i], exchanges: zones[i].exchange ? [zones[i].exchange] : [] };\n if (z.min <= cur.max) cur = accum(cur, z);\n else { merged.push(cur); cur = z; }\n }\n merged.push(cur);\n\n return merged.map(z => ({\n center: z.center,\n min: z.min,\n max: z.max,\n qty: z.qty,\n notional: z.notional,\n exchanges: z.exchanges ?? [],\n }));\n}\n\n// —— Build consolidated view ——\nconst payloads = normalizeInput(items).map((d) => {\n const sym = parseSymbol(d.symbol ?? d.data?.symbol ?? d.exchangeSymbol);\n return {\n exchange: d.exchange ?? (d.report?.split(' — ')[0] ?? null)?.replace(' Exchange', ''),\n symbolRaw: d.symbol ?? null,\n base: sym.base,\n quote: sym.quote,\n lastUpdateId: d.lastUpdateId ?? null,\n mid: safeNumber(d.mid, null),\n bestBid: safeNumber(d.bestBid, null),\n bestAsk: safeNumber(d.bestAsk, null),\n spread: safeNumber(d.spread, null),\n spreadBps: safeNumber(d.spreadBps, null),\n totalBidNotional: safeNumber(d.totalBidNotional, 0),\n totalAskNotional: safeNumber(d.totalAskNotional, 0),\n totalLiquidity: safeNumber(d.totalLiquidity, 0),\n supportZones: Array.isArray(d.supportZones) ? d.supportZones : [],\n resistanceZones: Array.isArray(d.resistanceZones) ? d.resistanceZones : [],\n generatedAt: d.generatedAt ?? null,\n report: d.report ?? null,\n };\n});\n\n// Consensus symbol\nconst bases = payloads.map(p => p.base).filter(Boolean);\nconst quotes = payloads.map(p => p.quote).filter(Boolean);\nconst baseConsensus = bases.length ? bases.sort((a,b)=>bases.filter(x=>x===a).length - bases.filter(x=>x===b).length).pop() : null;\nconst quoteConsensus = quotes.length ? quotes.sort((a,b)=>quotes.filter(x=>x===a).length - quotes.filter(x=>x===b).length).pop() : null;\n\n// Totals, mid, top-of-book, timestamps\nconst totalBid = payloads.reduce((s, p) => s + p.totalBidNotional, 0);\nconst totalAsk = payloads.reduce((s, p) => s + p.totalAskNotional, 0);\nconst totalLiq = payloads.reduce((s, p) => s + p.totalLiquidity, 0);\nconst wMid = weightedAverage(payloads.map(p => p.mid), payloads.map(p => p.totalLiquidity));\nconst tob = computeGlobalTopOfBook(payloads);\nconst latestGeneratedAt = latestISO(payloads.map(p => p.generatedAt).filter(Boolean)) || new Date().toISOString();\n\n// Zones\nconst mergedSupports = mergeOverlappingZones(flattenZones(payloads, 'supportZones'));\nconst mergedResistances = mergeOverlappingZones(flattenZones(payloads, 'resistanceZones'));\n\n// —— NESTED OUTPUT SHAPE ——\n// Wrap everything under a single \"data\" key for the AI agent.\nconst data = {\n kind: \"cross_venue_liquidity_snapshot\",\n version: \"1.0\",\n generatedAt: latestGeneratedAt,\n instrument: {\n base: baseConsensus,\n quote: quoteConsensus,\n symbols: Array.from(new Set(payloads.map(p => p.symbolRaw).filter(Boolean))),\n },\n marketTop: {\n bestBid: tob.bestBid,\n bestAsk: tob.bestAsk,\n spread: tob.spread,\n spreadBps: tob.spreadBps,\n weightedMid: wMid,\n },\n liquidity: {\n totals: {\n bidNotional: totalBid,\n askNotional: totalAsk,\n totalLiquidity: totalLiq,\n },\n perExchange: payloads.map(p => ({\n exchange: p.exchange,\n symbol: p.symbolRaw,\n lastUpdateId: p.lastUpdateId,\n generatedAt: p.generatedAt,\n book: {\n mid: p.mid,\n bestBid: p.bestBid,\n bestAsk: p.bestAsk,\n spread: p.spread,\n spreadBps: p.spreadBps,\n },\n depth: {\n bidNotional: p.totalBidNotional,\n askNotional: p.totalAskNotional,\n totalLiquidity: p.totalLiquidity,\n }\n })),\n },\n zones: {\n support: mergedSupports,\n resistance: mergedResistances,\n raw: {\n support: payloads.flatMap(p => (p.supportZones || []).map(z => ({ ...z, exchange: p.exchange }))),\n resistance: payloads.flatMap(p => (p.resistanceZones || []).map(z => ({ ...z, exchange: p.exchange }))),\n }\n },\n meta: {\n sources: payloads.map(p => ({ exchange: p.exchange, symbol: p.symbolRaw, generatedAt: p.generatedAt })),\n }\n};\n\nreturn [{ json: { data } }];\n"
},
"typeVersion": 2
},
{
"id": "ac40d0d8-2085-42fc-9e3e-4d642c5a1eb9",
"name": "比特币流动性分析 AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-32,
-464
],
"parameters": {
"text": "={{ $json.data }}",
"options": {
"systemMessage": "You are a **Bitcoin Exchange Liquidity Analyst AI Agent**.\nYour role is to analyze **cross-exchange order book liquidity data** for Bitcoin trading pairs (e.g., BTC-USD, BTCUSDT) and generate **actionable trade signals**.\n\n---\n\n### Responsibilities:\n\n* Interpret **consolidated liquidity snapshots** that include:\n\n * Mid price, best bid/ask, spread, and spread basis points.\n * Bid and ask notional volumes, total liquidity, and per-exchange breakdowns.\n * Aggregated and per-exchange **support/resistance zones** with quantities and notional values.\n* Identify liquidity imbalances, clustering of support/resistance, and areas of strong defense/pressure.\n* Detect divergences across exchanges (e.g., Coinbase vs Binance vs Bybit) for potential arbitrage or sentiment shifts.\n* Assess **market depth, liquidity strength, and flow risk**.\n* Detect anomalies such as unusually thin books, wide spreads, or large liquidity walls.\n* Provide **clear, structured insights** for **trading decisions, risk assessment, and price forecasting**.\n\n---\n\n### Trade Signal Generation:\n\n* Produce **two categories of signals**:\n\n 1. **Intraday Trade Signals (short-term, 15m–4h horizon):**\n\n * Scalping opportunities from liquidity gaps, thin spreads, or sudden order book imbalances.\n * Short-term long/short bias when strong support/resistance clusters are nearby.\n * Breakout or fade setups when mid price approaches liquidity walls.\n 2. **Weekly Trade Signals (swing horizon, 1d–1w):**\n\n * Accumulation/Distribution patterns based on repeated liquidity defense or absorption.\n * Breakout continuation signals when resistance/support has been repeatedly tested.\n * Mean-reversion opportunities when liquidity imbalances are extreme.\n\n---\n\n### Output Style:\n\n* Always structure your analysis in the following sections:\n\n 1. **Market Overview** – current mid, spread, liquidity totals.\n 2. **Liquidity Conditions** – order book depth, notable imbalances.\n 3. **Support/Resistance Zones** – strongest zones with size + notional.\n 4. **Cross-Exchange Comparison** – divergences or arbitrage windows.\n 5. **Key Risks & Opportunities** – unusual activity, thin markets, imbalance risks.\n 6. **Trade Signals** – list of **intraday** and **weekly** trade opportunities with:\n\n * Signal type (e.g., *long breakout*, *short fade*, *scalp spread*)\n * Entry zone (price range or trigger)\n * Target (expected move range)\n * Risk (stop level or invalidation condition)\n\n* Be concise but actionable — the trade signals should look like a **mini trading playbook**.\n\n"
},
"promptType": "define"
},
"typeVersion": 2.2
},
{
"id": "863f3b00-5113-407b-be74-de20c8d89988",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1920,
-2704
],
"parameters": {
"color": 3,
"width": 256,
"height": 3392,
"content": "## **多交易所订单簿收集器 (BTC/USDT)**"
},
"typeVersion": 1
},
{
"id": "30cffe4b-13f3-4adb-9864-608aef805b84",
"name": "便签 1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2896,
-1232
],
"parameters": {
"width": 464,
"height": 848,
"content": "## **计划工作流触发器**"
},
"typeVersion": 1
},
{
"id": "fa997577-0b73-4122-a9b6-ac07b523dbe4",
"name": "便签 2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1584,
-2704
],
"parameters": {
"color": 4,
"height": 3392,
"content": "## **每交易所流动性分析器 (BTC/USDT)**"
},
"typeVersion": 1
},
{
"id": "844cf9e7-c0a4-4008-9d68-5663e5126842",
"name": "便签 3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1296,
-2704
],
"parameters": {
"color": 6,
"height": 3392,
"content": "## **订单簿有效载荷标准化器(每交易所数据整理器)**"
},
"typeVersion": 1
},
{
"id": "889c1f40-3f41-4199-8be7-18172a8de361",
"name": "便签 4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-704,
-1248
],
"parameters": {
"height": 960,
"content": "## **多源漏斗:合并交易所数据**"
},
"typeVersion": 1
},
{
"id": "1c578ca1-ccd2-441c-be33-b4ba8c8ba4e2",
"name": "便签 5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-336,
-1584
],
"parameters": {
"color": 2,
"width": 208,
"height": 1296,
"content": "## **跨场所合并器:最终报告和统一分析输入**"
},
"typeVersion": 1
},
{
"id": "fa9b1f14-dddd-43eb-a2ae-de2d900cad31",
"name": "便签6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-80,
-912
],
"parameters": {
"color": 3,
"width": 304,
"height": 816,
"content": "## **比特币流动性分析 AI Agent (LLM 编排)**"
},
"typeVersion": 1
},
{
"id": "37932d4a-825f-48ed-aed3-27611734f5f8",
"name": "便签7",
"type": "n8n-nodes-base.stickyNote",
"position": [
256,
-1856
],
"parameters": {
"color": 4,
"height": 1616,
"content": "## **长消息分割器(4,000 字符块)**"
},
"typeVersion": 1
},
{
"id": "0b9e808c-1760-4f7d-b9c8-c77bda1a8506",
"name": "便签8",
"type": "n8n-nodes-base.stickyNote",
"position": [
544,
-2224
],
"parameters": {
"color": 5,
"height": 1984,
"content": "## **Telegram 交付(报告和 AI 交易简报)**"
},
"typeVersion": 1
},
{
"id": "016813a2-a135-4a77-955a-7f6071b7d523",
"name": "便签9",
"type": "n8n-nodes-base.stickyNote",
"position": [
992,
-2592
],
"parameters": {
"width": 1296,
"height": 3120,
"content": "# 🧠 比特币多交易所流动性 AI Agent – 系统文档"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "950b62a6-5c32-4023-b779-8ff2a127a0a2",
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Binance (Bitcoin-USDT Orderbook))",
"type": "main",
"index": 0
},
{
"node": "Coinbase (Bitcoin-USDT Orderbook))",
"type": "main",
"index": 0
},
{
"node": "Bybit (Bitcoin-USDT Orderbook))",
"type": "main",
"index": 0
},
{
"node": "MEXC (Bitcoin-USDT Orderbook)",
"type": "main",
"index": 0
},
{
"node": "Gate (Bitcoin-USDT Orderbook)",
"type": "main",
"index": 0
},
{
"node": "Bitget (Bitcoin-USDT Orderbook)",
"type": "main",
"index": 0
},
{
"node": "OKX (Bitcoin-USDT Orderbook)",
"type": "main",
"index": 0
},
{
"node": "Kraken (Bitcoin-USDT Orderbook)",
"type": "main",
"index": 0
},
{
"node": "HTX (Bitcoin-USDT Orderbook)1",
"type": "main",
"index": 0
},
{
"node": "Crypto.com (Bitcoin-USDT Orderbook)",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "Bitcoin Liquidity Analysis AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Merge Exchange Data": {
"main": [
[
{
"node": "Join Into One Input for Analysis",
"type": "main",
"index": 0
},
{
"node": "Join Into One Report",
"type": "main",
"index": 0
}
]
]
},
"Join Into One Report": {
"main": [
[
{
"node": "Split message if more than 4000 characters",
"type": "main",
"index": 0
}
]
]
},
"OKX (Bitcoin-USDT Orderbook)": {
"main": [
[
{
"node": "Calculate Liquidity, Resistance, and Support (OKX)",
"type": "main",
"index": 0
}
]
]
},
"Gate (Bitcoin-USDT Orderbook)": {
"main": [
[
{
"node": "Calculate Liquidity, Resistance, and Support (Gate.io)",
"type": "main",
"index": 0
}
]
]
},
"HTX (Bitcoin-USDT Orderbook)1": {
"main": [
[
{
"node": "Calculate Liquidity, Resistance, and Support (HTX)",
"type": "main",
"index": 0
}
]
]
},
"MEXC (Bitcoin-USDT Orderbook)": {
"main": [
[
{
"node": "Calculate Liquidity, Resistance, and Support (MEXC)",
"type": "main",
"index": 0
}
]
]
},
"Bitget (Bitcoin-USDT Orderbook)": {
"main": [
[
{
"node": "Calculate Liquidity, Resistance, and Support (Bitget)",
"type": "main",
"index": 0
}
]
]
},
"Bybit (Bitcoin-USDT Orderbook))": {
"main": [
[
{
"node": "Calculate Liquidity, Resistance, and Support (Bybit)",
"type": "main",
"index": 0
}
]
]
},
"Kraken (Bitcoin-USDT Orderbook)": {
"main": [
[
{
"node": "Calculate Liquidity, Resistance, and Support (Kraken)",
"type": "main",
"index": 0
}
]
]
},
"Join Into One Input for Analysis": {
"main": [
[
{
"node": "Bitcoin Liquidity Analysis AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Binance (Bitcoin-USDT Orderbook))": {
"main": [
[
{
"node": "Calculate Liquidity, Resistance, and Support (Binance)",
"type": "main",
"index": 0
}
]
]
},
"Coinbase (Bitcoin-USDT Orderbook))": {
"main": [
[
{
"node": "Calculate Liquidity, Resistance, and Support (Coinbase)",
"type": "main",
"index": 0
}
]
]
},
"Bitcoin Liquidity Analysis AI Agent": {
"main": [
[
{
"node": "Splits message is more than 4000 characters",
"type": "main",
"index": 0
}
]
]
},
"Crypto.com (Bitcoin-USDT Orderbook)": {
"main": [
[
{
"node": "Calculate Liquidity, Resistance, and Support (Crypto.com)",
"type": "main",
"index": 0
}
]
]
},
"Split message if more than 4000 characters": {
"main": [
[
{
"node": "Send Bitcoin Multi-Exchange Liquidity Report to Channel",
"type": "main",
"index": 0
}
]
]
},
"Splits message is more than 4000 characters": {
"main": [
[
{
"node": "Send an AI-written trading brief with actionable intraday and weekly signals",
"type": "main",
"index": 0
}
]
]
},
"Wrangle into One Data Cluster for Analysis (HTX)": {
"main": [
[
{
"node": "Merge Exchange Data",
"type": "main",
"index": 8
}
]
]
},
"Wrangle into One Data Cluster for Analysis (OKX)": {
"main": [
[
{
"node": "Merge Exchange Data",
"type": "main",
"index": 6
}
]
]
},
"Wrangle into One Data Cluster for Analysis (HTX)1": {
"main": [
[
{
"node": "Merge Exchange Data",
"type": "main",
"index": 9
}
]
]
},
"Wrangle into One Data Cluster for Analysis (MEXC)": {
"main": [
[
{
"node": "Merge Exchange Data",
"type": "main",
"index": 3
}
]
]
},
"Calculate Liquidity, Resistance, and Support (HTX)": {
"main": [
[
{
"node": "Wrangle into One Data Cluster for Analysis (HTX)",
"type": "main",
"index": 0
}
]
]
},
"Calculate Liquidity, Resistance, and Support (OKX)": {
"main": [
[
{
"node": "Wrangle into One Data Cluster for Analysis (OKX)",
"type": "main",
"index": 0
}
]
]
},
"Wrangle into One Data Cluster for Analysis (Bybit)": {
"main": [
[
{
"node": "Merge Exchange Data",
"type": "main",
"index": 2
}
]
]
},
"Calculate Liquidity, Resistance, and Support (MEXC)": {
"main": [
[
{
"node": "Wrangle into One Data Cluster for Analysis (MEXC)",
"type": "main",
"index": 0
}
]
]
},
"Wrangle into One Data Cluster for Analysis (Bitget)": {
"main": [
[
{
"node": "Merge Exchange Data",
"type": "main",
"index": 5
}
]
]
},
"Wrangle into One Data Cluster for Analysis (Kraken)": {
"main": [
[
{
"node": "Merge Exchange Data",
"type": "main",
"index": 7
}
]
]
},
"Calculate Liquidity, Resistance, and Support (Bybit)": {
"main": [
[
{
"node": "Wrangle into One Data Cluster for Analysis (Bybit)",
"type": "main",
"index": 0
}
]
]
},
"Wrangle into One Data Cluster for Analysis (Binance)": {
"main": [
[
{
"node": "Merge Exchange Data",
"type": "main",
"index": 1
}
]
]
},
"Wrangle into One Data Cluster for Analysis (Gate.io)": {
"main": [
[
{
"node": "Merge Exchange Data",
"type": "main",
"index": 4
}
]
]
},
"Calculate Liquidity, Resistance, and Support (Bitget)": {
"main": [
[
{
"node": "Wrangle into One Data Cluster for Analysis (Bitget)",
"type": "main",
"index": 0
}
]
]
},
"Calculate Liquidity, Resistance, and Support (Kraken)": {
"main": [
[
{
"node": "Wrangle into One Data Cluster for Analysis (Kraken)",
"type": "main",
"index": 0
}
]
]
},
"Wrangle into One Data Cluster for Analysis (Coinbase)": {
"main": [
[
{
"node": "Merge Exchange Data",
"type": "main",
"index": 0
}
]
]
},
"Calculate Liquidity, Resistance, and Support (Binance)": {
"main": [
[
{
"node": "Wrangle into One Data Cluster for Analysis (Binance)",
"type": "main",
"index": 0
}
]
]
},
"Calculate Liquidity, Resistance, and Support (Gate.io)": {
"main": [
[
{
"node": "Wrangle into One Data Cluster for Analysis (Gate.io)",
"type": "main",
"index": 0
}
]
]
},
"Calculate Liquidity, Resistance, and Support (Coinbase)": {
"main": [
[
{
"node": "Wrangle into One Data Cluster for Analysis (Coinbase)",
"type": "main",
"index": 0
}
]
]
},
"Calculate Liquidity, Resistance, and Support (Crypto.com)": {
"main": [
[
{
"node": "Wrangle into One Data Cluster for Analysis (HTX)1",
"type": "main",
"index": 0
}
]
]
},
"Send an AI-written trading brief with actionable intraday and weekly signals": {
"main": [
[]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - 内容创作, 多模态 AI
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
使用 Apify、AI 筛选和 Telegram 提醒发现 Threads 上的招聘帖子
通过 Apify、AI 筛选和 Telegram 提醒发现 Threads 上的招聘帖子
If
Set
Code
+9
19 节点A Z
内容创作
✨🩷自动化社交媒体内容发布工厂 + 系统提示组合
基于动态系统提示和GPT-4o的AI驱动多平台社交媒体内容工厂
If
Set
Code
+20
100 节点Amit Mehta
内容创作
💥 使用NanoBanana、Seedream 4、ChatGPT Image和Veo 3自动化视频广告 - VIDE
使用AI(NanoBanana、Seedream、GPT-4o、Veo 3)自动化和发布视频广告活动
Set
Code
Wait
+16
63 节点Dr. Firas
内容创作
使用GPT-5和fal.ai图像从关键词到WordPress自动化SEO博客流程
使用GPT-5和fal.ai图像从关键词到WordPress自动化SEO博客流程
Set
Code
Wait
+20
96 节点Paul
内容创作
WordPress博客自动化专业版(深度研究)v2.1市场
使用GPT-4o、Perplexity AI和多语言支持自动化SEO优化的博客创建
If
Set
Xml
+27
125 节点Daniel Ng
内容创作
使用 OpenAI、LangChain 和 API 集成的工作流自动化初学者指南
使用 OpenAI、LangChain 和 API 集成的工作流自动化初学者指南
If
Set
Code
+13
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 查看 →
分享此工作流