币安技术指标 Webhook 工具
高级
这是一个Finance, AI领域的自动化工作流,包含 69 个节点。主要使用 Code, Merge, Webhook, HttpRequest, RespondToWebhook 等节点,结合人工智能技术实现智能自动化。 币安技术指标 Webhook 工具
前置要求
- •HTTP Webhook 端点(n8n 会自动生成)
- •可能需要目标 API 的认证凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "HrTD222kWpvKsy2j",
"meta": {
"instanceId": "a5283507e1917a33cc3ae615b2e7d5ad2c1e50955e6f831272ddd5ab816f3fb6"
},
"name": "币安技术指标 Webhook 工具",
"tags": [],
"nodes": [
{
"id": "61cb3bd7-60ba-4dcf-a080-146b7612a99b",
"name": "HTTP 请求",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1080,
0
],
"parameters": {
"url": "https://api.binance.com/api/v3/klines",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "symbol",
"value": "={{$json.body.symbol}}"
},
{
"name": "interval",
"value": "15m"
},
{
"name": "limit",
"value": "40"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "b372f57e-917a-42d2-964c-25a296ea982d",
"name": "Webhook 15分钟指标",
"type": "n8n-nodes-base.webhook",
"position": [
-1460,
0
],
"webhookId": "39cc366c-af5f-472a-9d48-bbe30a4fe3ea",
"parameters": {
"path": "39cc366c-af5f-472a-9d48-bbe30a4fe3ea",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "46b937d5-7338-457f-b2f0-eb85ac44a8bb",
"name": "合并为1个数组",
"type": "n8n-nodes-base.code",
"position": [
-740,
0
],
"parameters": {
"jsCode": "const klines = $input.all().map(item => item.json);\nreturn [{ json: { klines } }];"
},
"typeVersion": 2
},
{
"id": "5b8c1049-bcf4-41d4-bba3-5f76e940bd3e",
"name": "计算布林带",
"type": "n8n-nodes-base.code",
"position": [
-280,
-460
],
"parameters": {
"jsCode": "// Get raw klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20;\nconst stdMultiplier = 2;\n\n// Validate type\nif (!Array.isArray(klines)) {\n throw new Error(\"klines is not an array\");\n}\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Ensure enough data\nif (closes.length < period) {\n throw new Error(`Not enough data: got ${closes.length}, expected at least ${period}`);\n}\n\n// BB calculation logic\nfunction calculateBB(prices, period) {\n const result = [];\n for (let i = period - 1; i < prices.length; i++) {\n const slice = prices.slice(i - period + 1, i + 1);\n const mean = slice.reduce((sum, val) => sum + val, 0) / period;\n const variance = slice.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / period;\n const stdDev = Math.sqrt(variance);\n\n result.push({\n close: prices[i],\n basis: mean,\n upper: mean + stdMultiplier * stdDev,\n lower: mean - stdMultiplier * stdDev,\n timestamp: klines[i][0]\n });\n }\n return result;\n}\n\n// Calculate bands\nconst bands = calculateBB(closes, period);\n\n// Format output\nreturn bands.map(b => ({\n json: {\n timestamp: b.timestamp,\n close: b.close,\n bb_basis: b.basis,\n bb_upper: b.upper,\n bb_lower: b.lower\n }\n}));"
},
"typeVersion": 2
},
{
"id": "b0391e28-03a3-40f0-8182-72df0928ca2c",
"name": "计算RSI",
"type": "n8n-nodes-base.code",
"position": [
-280,
-240
],
"parameters": {
"jsCode": "// Pull klines array from JSON\nconst klines = $input.first().json.klines;\n\n// === CONFIGURATION ===\nconst period = 14; // RSI period\n\n// === Extract closing prices ===\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// === Ensure enough data ===\nif (closes.length < period + 1) {\n throw new Error(`Not enough data for RSI. Need at least ${period + 1}, got ${closes.length}`);\n}\n\n// === RSI Calculation ===\nfunction calculateRSI(prices, period) {\n const result = [];\n\n for (let i = period; i < prices.length; i++) {\n let gains = 0;\n let losses = 0;\n\n for (let j = i - period + 1; j <= i; j++) {\n const change = prices[j] - prices[j - 1];\n if (change >= 0) gains += change;\n else losses -= change;\n }\n\n const avgGain = gains / period;\n const avgLoss = losses / period;\n\n const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;\n const rsi = avgLoss === 0 ? 100 : 100 - (100 / (1 + rs));\n\n result.push({\n timestamp: klines[i][0],\n close: prices[i],\n rsi: rsi\n });\n }\n\n return result;\n}\n\n// === Run RSI ===\nconst rsiSeries = calculateRSI(closes, period);\n\n// === Return formatted RSI output ===\nreturn rsiSeries.map(r => ({\n json: {\n timestamp: r.timestamp,\n close: r.close,\n rsi: r.rsi\n }\n}));"
},
"typeVersion": 2
},
{
"id": "2c598960-630c-4687-91c0-bfae10da1943",
"name": "计算MACD",
"type": "n8n-nodes-base.code",
"position": [
-280,
-40
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Parameters\nconst fastPeriod = 12;\nconst slowPeriod = 26;\nconst signalPeriod = 9;\n\n// Validate data length\nconst minRequired = slowPeriod + signalPeriod;\nif (closes.length < minRequired) {\n throw new Error(`Not enough data for MACD. Need at least ${minRequired}, got ${closes.length}`);\n}\n\n// === Helper: EMA function ===\nfunction calculateEMA(prices, period) {\n const k = 2 / (period + 1);\n const ema = [prices.slice(0, period).reduce((a, b) => a + b, 0) / period];\n\n for (let i = period; i < prices.length; i++) {\n ema.push(prices[i] * k + ema[ema.length - 1] * (1 - k));\n }\n\n return ema;\n}\n\n// === MACD Core Calculation ===\nconst slowEMA = calculateEMA(closes, slowPeriod);\nconst fastEMA = calculateEMA(closes.slice(slowPeriod - fastPeriod), fastPeriod);\n\n// Align lengths\nconst alignedFastEMA = fastEMA.slice(fastEMA.length - slowEMA.length);\nconst macdLine = alignedFastEMA.map((val, i) => val - slowEMA[i]);\n\n// Signal line\nconst signalLine = calculateEMA(macdLine, signalPeriod);\n\n// Histogram\nconst histogram = macdLine.slice(signalPeriod - 1).map((macd, i) => macd - signalLine[i]);\n\n// Final output start index\nconst startIndex = closes.length - histogram.length;\n\nconst result = [];\n\nfor (let i = 0; i < histogram.length; i++) {\n const idx = startIndex + i;\n result.push({\n timestamp: klines[idx]?.[0] || null,\n close: closes[idx],\n macd: macdLine[i + signalPeriod - 1],\n signal: signalLine[i],\n histogram: histogram[i]\n });\n}\n\nreturn result.map(r => ({\n json: r\n}));"
},
"typeVersion": 2
},
{
"id": "c50f7fa5-2707-47a4-9b32-1d582bf5d600",
"name": "计算SMA",
"type": "n8n-nodes-base.code",
"position": [
-280,
200
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20; // Change to 50, 100, 200, etc. as needed\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Check for sufficient data\nif (closes.length < period) {\n throw new Error(`Not enough data to compute SMA. Need at least ${period}, got ${closes.length}`);\n}\n\n// Calculate SMA values\nconst result = [];\n\nfor (let i = period - 1; i < closes.length; i++) {\n const slice = closes.slice(i - period + 1, i + 1);\n const sum = slice.reduce((a, b) => a + b, 0);\n const average = sum / period;\n\n result.push({\n timestamp: klines[i][0],\n close: closes[i],\n sma: average\n });\n}\n\n// Return formatted output\nreturn result.map(r => ({\n json: r\n}));"
},
"typeVersion": 2
},
{
"id": "25ef2964-c9b1-4b9e-b6ce-3766b215d163",
"name": "计算EMA",
"type": "n8n-nodes-base.code",
"position": [
-280,
420
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20; // Change to 9, 12, 26, 50, etc. as needed\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Check for sufficient data\nif (closes.length < period) {\n throw new Error(`Not enough data to compute EMA. Need at least ${period}, got ${closes.length}`);\n}\n\n// EMA Calculation\nconst k = 2 / (period + 1);\nconst ema = [];\n\n// Start with SMA of first period\nlet sma = closes.slice(0, period).reduce((a, b) => a + b, 0) / period;\nema.push({\n timestamp: klines[period - 1][0],\n close: closes[period - 1],\n ema: sma\n});\n\n// Continue EMA calculation\nfor (let i = period; i < closes.length; i++) {\n const value = closes[i] * k + ema[ema.length - 1].ema * (1 - k);\n ema.push({\n timestamp: klines[i][0],\n close: closes[i],\n ema: value\n });\n}\n\n// Return result\nreturn ema.map(e => ({\n json: e\n}));"
},
"typeVersion": 2
},
{
"id": "df6546be-3778-4fba-801a-870c0fe883c3",
"name": "计算ADX",
"type": "n8n-nodes-base.code",
"position": [
-280,
660
],
"parameters": {
"jsCode": "// Get kline array from merged item\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 14;\n\n// Parse high, low, close arrays\nconst highs = klines.map(k => parseFloat(k[2]));\nconst lows = klines.map(k => parseFloat(k[3]));\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Validation\nif (closes.length < period + 1) {\n throw new Error(`Not enough data for ADX. Need at least ${period + 1}, got ${closes.length}`);\n}\n\n// Initialize arrays\nconst tr = [];\nconst plusDM = [];\nconst minusDM = [];\n\n// Step 1: Calculate TR, +DM, -DM\nfor (let i = 1; i < klines.length; i++) {\n const highDiff = highs[i] - highs[i - 1];\n const lowDiff = lows[i - 1] - lows[i];\n const upMove = highDiff > 0 && highDiff > lowDiff ? highDiff : 0;\n const downMove = lowDiff > 0 && lowDiff > highDiff ? lowDiff : 0;\n\n const trueRange = Math.max(\n highs[i] - lows[i],\n Math.abs(highs[i] - closes[i - 1]),\n Math.abs(lows[i] - closes[i - 1])\n );\n\n tr.push(trueRange);\n plusDM.push(upMove);\n minusDM.push(downMove);\n}\n\n// Step 2: Smooth TR, +DM, -DM and calculate DI\nconst smoothedTR = [tr.slice(0, period).reduce((a, b) => a + b, 0)];\nconst smoothedPlusDM = [plusDM.slice(0, period).reduce((a, b) => a + b, 0)];\nconst smoothedMinusDM = [minusDM.slice(0, period).reduce((a, b) => a + b, 0)];\n\nfor (let i = period; i < tr.length; i++) {\n smoothedTR.push(smoothedTR[smoothedTR.length - 1] - smoothedTR[smoothedTR.length - 1] / period + tr[i]);\n smoothedPlusDM.push(smoothedPlusDM[smoothedPlusDM.length - 1] - smoothedPlusDM[smoothedPlusDM.length - 1] / period + plusDM[i]);\n smoothedMinusDM.push(smoothedMinusDM[smoothedMinusDM.length - 1] - smoothedMinusDM[smoothedMinusDM.length - 1] / period + minusDM[i]);\n}\n\nconst plusDI = smoothedPlusDM.map((val, i) => 100 * val / smoothedTR[i]);\nconst minusDI = smoothedMinusDM.map((val, i) => 100 * val / smoothedTR[i]);\n\n// Step 3: Calculate DX\nconst dx = plusDI.map((val, i) => {\n const diff = Math.abs(plusDI[i] - minusDI[i]);\n const sum = plusDI[i] + minusDI[i];\n return sum === 0 ? 0 : 100 * (diff / sum);\n});\n\n// Step 4: Smooth DX into ADX\nconst adx = [];\nconst firstADX = dx.slice(0, period).reduce((a, b) => a + b, 0) / period;\nadx.push(firstADX);\n\nfor (let i = period; i < dx.length; i++) {\n const newADX = (adx[adx.length - 1] * (period - 1) + dx[i]) / period;\n adx.push(newADX);\n}\n\n// Step 5: Final result formatting\nconst output = [];\n\nfor (let i = 0; i < adx.length; i++) {\n const index = i + (2 * period); // account for offset\n if (klines[index]) {\n output.push({\n timestamp: klines[index][0],\n close: closes[index],\n adx: adx[i],\n plusDI: plusDI[i + period],\n minusDI: minusDI[i + period]\n });\n }\n}\n\nreturn output.map(r => ({ json: r }));"
},
"typeVersion": 2
},
{
"id": "5ea4756f-a20a-444c-8471-57d209457823",
"name": "合并15分钟指标",
"type": "n8n-nodes-base.merge",
"position": [
180,
-80
],
"parameters": {
"numberInputs": 6
},
"typeVersion": 3.1
},
{
"id": "0a46ea7c-a85d-4fc6-b20c-7e65ab962ea6",
"name": "响应15分钟Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
560,
-20
],
"parameters": {
"options": {},
"respondWith": "allIncomingItems"
},
"typeVersion": 1.3
},
{
"id": "40fcbba9-489b-4452-baf8-9a41ef0f9b07",
"name": "Webhook 1小时指标",
"type": "n8n-nodes-base.webhook",
"position": [
-3220,
1760
],
"webhookId": "78948764-5cdb-4808-8ef9-2155f10dd721",
"parameters": {
"path": "78948764-5cdb-4808-8ef9-2155f10dd721",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "39e6b058-d800-4751-b75b-5e62be379c61",
"name": "响应1小时Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-1060,
1740
],
"parameters": {
"options": {},
"respondWith": "allIncomingItems"
},
"typeVersion": 1.3
},
{
"id": "af471a36-1107-4ccd-be14-efa06ba27330",
"name": "合并1小时指标",
"type": "n8n-nodes-base.merge",
"position": [
-1460,
1680
],
"parameters": {
"numberInputs": 6
},
"typeVersion": 3.1
},
{
"id": "d7d9bb31-6986-42e6-9f0c-7d7f00d66ee1",
"name": "合并为1个数组(1小时)",
"type": "n8n-nodes-base.code",
"position": [
-2540,
1760
],
"parameters": {
"jsCode": "const klines = $input.all().map(item => item.json);\nreturn [{ json: { klines } }];"
},
"typeVersion": 2
},
{
"id": "a2e9d933-47d7-47f5-8d49-224860d8600a",
"name": "计算布林带(1小时)",
"type": "n8n-nodes-base.code",
"position": [
-2060,
1220
],
"parameters": {
"jsCode": "// Get raw klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20;\nconst stdMultiplier = 2;\n\n// Validate type\nif (!Array.isArray(klines)) {\n throw new Error(\"klines is not an array\");\n}\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Ensure enough data\nif (closes.length < period) {\n throw new Error(`Not enough data: got ${closes.length}, expected at least ${period}`);\n}\n\n// BB calculation logic\nfunction calculateBB(prices, period) {\n const result = [];\n for (let i = period - 1; i < prices.length; i++) {\n const slice = prices.slice(i - period + 1, i + 1);\n const mean = slice.reduce((sum, val) => sum + val, 0) / period;\n const variance = slice.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / period;\n const stdDev = Math.sqrt(variance);\n\n result.push({\n close: prices[i],\n basis: mean,\n upper: mean + stdMultiplier * stdDev,\n lower: mean - stdMultiplier * stdDev,\n timestamp: klines[i][0]\n });\n }\n return result;\n}\n\n// Calculate bands\nconst bands = calculateBB(closes, period);\n\n// Format output\nreturn bands.map(b => ({\n json: {\n timestamp: b.timestamp,\n close: b.close,\n bb_basis: b.basis,\n bb_upper: b.upper,\n bb_lower: b.lower\n }\n}));"
},
"typeVersion": 2
},
{
"id": "702e8bc8-d8f0-4fb4-b44f-fd19479c8b03",
"name": "计算RSI(1小时)",
"type": "n8n-nodes-base.code",
"position": [
-2060,
1500
],
"parameters": {
"jsCode": "// Pull klines array from JSON\nconst klines = $input.first().json.klines;\n\n// === CONFIGURATION ===\nconst period = 14; // RSI period\n\n// === Extract closing prices ===\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// === Ensure enough data ===\nif (closes.length < period + 1) {\n throw new Error(`Not enough data for RSI. Need at least ${period + 1}, got ${closes.length}`);\n}\n\n// === RSI Calculation ===\nfunction calculateRSI(prices, period) {\n const result = [];\n\n for (let i = period; i < prices.length; i++) {\n let gains = 0;\n let losses = 0;\n\n for (let j = i - period + 1; j <= i; j++) {\n const change = prices[j] - prices[j - 1];\n if (change >= 0) gains += change;\n else losses -= change;\n }\n\n const avgGain = gains / period;\n const avgLoss = losses / period;\n\n const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;\n const rsi = avgLoss === 0 ? 100 : 100 - (100 / (1 + rs));\n\n result.push({\n timestamp: klines[i][0],\n close: prices[i],\n rsi: rsi\n });\n }\n\n return result;\n}\n\n// === Run RSI ===\nconst rsiSeries = calculateRSI(closes, period);\n\n// === Return formatted RSI output ===\nreturn rsiSeries.map(r => ({\n json: {\n timestamp: r.timestamp,\n close: r.close,\n rsi: r.rsi\n }\n}));"
},
"typeVersion": 2
},
{
"id": "d16b7829-59ca-45ca-a5ae-4e315168c96d",
"name": "计算MACD(1小时)",
"type": "n8n-nodes-base.code",
"position": [
-2060,
1720
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Parameters\nconst fastPeriod = 12;\nconst slowPeriod = 26;\nconst signalPeriod = 9;\n\n// Validate data length\nconst minRequired = slowPeriod + signalPeriod;\nif (closes.length < minRequired) {\n throw new Error(`Not enough data for MACD. Need at least ${minRequired}, got ${closes.length}`);\n}\n\n// === Helper: EMA function ===\nfunction calculateEMA(prices, period) {\n const k = 2 / (period + 1);\n const ema = [prices.slice(0, period).reduce((a, b) => a + b, 0) / period];\n\n for (let i = period; i < prices.length; i++) {\n ema.push(prices[i] * k + ema[ema.length - 1] * (1 - k));\n }\n\n return ema;\n}\n\n// === MACD Core Calculation ===\nconst slowEMA = calculateEMA(closes, slowPeriod);\nconst fastEMA = calculateEMA(closes.slice(slowPeriod - fastPeriod), fastPeriod);\n\n// Align lengths\nconst alignedFastEMA = fastEMA.slice(fastEMA.length - slowEMA.length);\nconst macdLine = alignedFastEMA.map((val, i) => val - slowEMA[i]);\n\n// Signal line\nconst signalLine = calculateEMA(macdLine, signalPeriod);\n\n// Histogram\nconst histogram = macdLine.slice(signalPeriod - 1).map((macd, i) => macd - signalLine[i]);\n\n// Final output start index\nconst startIndex = closes.length - histogram.length;\n\nconst result = [];\n\nfor (let i = 0; i < histogram.length; i++) {\n const idx = startIndex + i;\n result.push({\n timestamp: klines[idx]?.[0] || null,\n close: closes[idx],\n macd: macdLine[i + signalPeriod - 1],\n signal: signalLine[i],\n histogram: histogram[i]\n });\n}\n\nreturn result.map(r => ({\n json: r\n}));"
},
"typeVersion": 2
},
{
"id": "15bf5c3b-de07-412b-988c-f5c59fa7a06f",
"name": "计算SMA(1小时)",
"type": "n8n-nodes-base.code",
"position": [
-2060,
1960
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20; // Change to 50, 100, 200, etc. as needed\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Check for sufficient data\nif (closes.length < period) {\n throw new Error(`Not enough data to compute SMA. Need at least ${period}, got ${closes.length}`);\n}\n\n// Calculate SMA values\nconst result = [];\n\nfor (let i = period - 1; i < closes.length; i++) {\n const slice = closes.slice(i - period + 1, i + 1);\n const sum = slice.reduce((a, b) => a + b, 0);\n const average = sum / period;\n\n result.push({\n timestamp: klines[i][0],\n close: closes[i],\n sma: average\n });\n}\n\n// Return formatted output\nreturn result.map(r => ({\n json: r\n}));"
},
"typeVersion": 2
},
{
"id": "83fa487a-1a67-4059-9007-d5298d8bb105",
"name": "计算EMA(1小时)",
"type": "n8n-nodes-base.code",
"position": [
-2060,
2220
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20; // Change to 9, 12, 26, 50, etc. as needed\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Check for sufficient data\nif (closes.length < period) {\n throw new Error(`Not enough data to compute EMA. Need at least ${period}, got ${closes.length}`);\n}\n\n// EMA Calculation\nconst k = 2 / (period + 1);\nconst ema = [];\n\n// Start with SMA of first period\nlet sma = closes.slice(0, period).reduce((a, b) => a + b, 0) / period;\nema.push({\n timestamp: klines[period - 1][0],\n close: closes[period - 1],\n ema: sma\n});\n\n// Continue EMA calculation\nfor (let i = period; i < closes.length; i++) {\n const value = closes[i] * k + ema[ema.length - 1].ema * (1 - k);\n ema.push({\n timestamp: klines[i][0],\n close: closes[i],\n ema: value\n });\n}\n\n// Return result\nreturn ema.map(e => ({\n json: e\n}));"
},
"typeVersion": 2
},
{
"id": "7301ba32-ab0c-4c80-b041-cd398f7daa2c",
"name": "计算ADX1",
"type": "n8n-nodes-base.code",
"position": [
-2060,
2420
],
"parameters": {
"jsCode": "// Get kline array from merged item\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 14;\n\n// Parse high, low, close arrays\nconst highs = klines.map(k => parseFloat(k[2]));\nconst lows = klines.map(k => parseFloat(k[3]));\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Validation\nif (closes.length < period + 1) {\n throw new Error(`Not enough data for ADX. Need at least ${period + 1}, got ${closes.length}`);\n}\n\n// Initialize arrays\nconst tr = [];\nconst plusDM = [];\nconst minusDM = [];\n\n// Step 1: Calculate TR, +DM, -DM\nfor (let i = 1; i < klines.length; i++) {\n const highDiff = highs[i] - highs[i - 1];\n const lowDiff = lows[i - 1] - lows[i];\n const upMove = highDiff > 0 && highDiff > lowDiff ? highDiff : 0;\n const downMove = lowDiff > 0 && lowDiff > highDiff ? lowDiff : 0;\n\n const trueRange = Math.max(\n highs[i] - lows[i],\n Math.abs(highs[i] - closes[i - 1]),\n Math.abs(lows[i] - closes[i - 1])\n );\n\n tr.push(trueRange);\n plusDM.push(upMove);\n minusDM.push(downMove);\n}\n\n// Step 2: Smooth TR, +DM, -DM and calculate DI\nconst smoothedTR = [tr.slice(0, period).reduce((a, b) => a + b, 0)];\nconst smoothedPlusDM = [plusDM.slice(0, period).reduce((a, b) => a + b, 0)];\nconst smoothedMinusDM = [minusDM.slice(0, period).reduce((a, b) => a + b, 0)];\n\nfor (let i = period; i < tr.length; i++) {\n smoothedTR.push(smoothedTR[smoothedTR.length - 1] - smoothedTR[smoothedTR.length - 1] / period + tr[i]);\n smoothedPlusDM.push(smoothedPlusDM[smoothedPlusDM.length - 1] - smoothedPlusDM[smoothedPlusDM.length - 1] / period + plusDM[i]);\n smoothedMinusDM.push(smoothedMinusDM[smoothedMinusDM.length - 1] - smoothedMinusDM[smoothedMinusDM.length - 1] / period + minusDM[i]);\n}\n\nconst plusDI = smoothedPlusDM.map((val, i) => 100 * val / smoothedTR[i]);\nconst minusDI = smoothedMinusDM.map((val, i) => 100 * val / smoothedTR[i]);\n\n// Step 3: Calculate DX\nconst dx = plusDI.map((val, i) => {\n const diff = Math.abs(plusDI[i] - minusDI[i]);\n const sum = plusDI[i] + minusDI[i];\n return sum === 0 ? 0 : 100 * (diff / sum);\n});\n\n// Step 4: Smooth DX into ADX\nconst adx = [];\nconst firstADX = dx.slice(0, period).reduce((a, b) => a + b, 0) / period;\nadx.push(firstADX);\n\nfor (let i = period; i < dx.length; i++) {\n const newADX = (adx[adx.length - 1] * (period - 1) + dx[i]) / period;\n adx.push(newADX);\n}\n\n// Step 5: Final result formatting\nconst output = [];\n\nfor (let i = 0; i < adx.length; i++) {\n const index = i + (2 * period); // account for offset\n if (klines[index]) {\n output.push({\n timestamp: klines[index][0],\n close: closes[index],\n adx: adx[i],\n plusDI: plusDI[i + period],\n minusDI: minusDI[i + period]\n });\n }\n}\n\nreturn output.map(r => ({ json: r }));"
},
"typeVersion": 2
},
{
"id": "9ec777b1-eb23-43b0-8bb3-726adcc3418f",
"name": "HTTP请求 1小时",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2880,
1760
],
"parameters": {
"url": "https://api.binance.com/api/v3/klines",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "symbol",
"value": "={{$json.body.symbol}}"
},
{
"name": "interval",
"value": "1h"
},
{
"name": "limit",
"value": "40"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "c4a718f8-f2d6-464b-8120-1e4067d760ad",
"name": "Webhook 4小时指标",
"type": "n8n-nodes-base.webhook",
"position": [
-40,
1680
],
"webhookId": "55fd9665-ed4a-4e1a-8062-890cad0fb6ac",
"parameters": {
"path": "55fd9665-ed4a-4e1a-8062-890cad0fb6ac",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "bbb81a2a-b1c4-4c1e-80e9-a4990c45fae8",
"name": "HTTP请求 4小时",
"type": "n8n-nodes-base.httpRequest",
"position": [
300,
1680
],
"parameters": {
"url": "https://api.binance.com/api/v3/klines",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "symbol",
"value": "={{$json.body.symbol}}"
},
{
"name": "interval",
"value": "4h"
},
{
"name": "limit",
"value": "40"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "20e58706-035f-46ef-b235-78fe468875bc",
"name": "合并为1个数组(4小时)",
"type": "n8n-nodes-base.code",
"position": [
640,
1680
],
"parameters": {
"jsCode": "const klines = $input.all().map(item => item.json);\nreturn [{ json: { klines } }];"
},
"typeVersion": 2
},
{
"id": "1675168b-97ab-4be4-92d2-734d075e2222",
"name": "计算布林带(4小时)",
"type": "n8n-nodes-base.code",
"position": [
1120,
1180
],
"parameters": {
"jsCode": "// Get raw klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20;\nconst stdMultiplier = 2;\n\n// Validate type\nif (!Array.isArray(klines)) {\n throw new Error(\"klines is not an array\");\n}\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Ensure enough data\nif (closes.length < period) {\n throw new Error(`Not enough data: got ${closes.length}, expected at least ${period}`);\n}\n\n// BB calculation logic\nfunction calculateBB(prices, period) {\n const result = [];\n for (let i = period - 1; i < prices.length; i++) {\n const slice = prices.slice(i - period + 1, i + 1);\n const mean = slice.reduce((sum, val) => sum + val, 0) / period;\n const variance = slice.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / period;\n const stdDev = Math.sqrt(variance);\n\n result.push({\n close: prices[i],\n basis: mean,\n upper: mean + stdMultiplier * stdDev,\n lower: mean - stdMultiplier * stdDev,\n timestamp: klines[i][0]\n });\n }\n return result;\n}\n\n// Calculate bands\nconst bands = calculateBB(closes, period);\n\n// Format output\nreturn bands.map(b => ({\n json: {\n timestamp: b.timestamp,\n close: b.close,\n bb_basis: b.basis,\n bb_upper: b.upper,\n bb_lower: b.lower\n }\n}));"
},
"typeVersion": 2
},
{
"id": "d1c2fcc7-65ac-446c-8f59-c4cd8ac7d795",
"name": "计算RSI(4小时)",
"type": "n8n-nodes-base.code",
"position": [
1120,
1420
],
"parameters": {
"jsCode": "// Pull klines array from JSON\nconst klines = $input.first().json.klines;\n\n// === CONFIGURATION ===\nconst period = 14; // RSI period\n\n// === Extract closing prices ===\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// === Ensure enough data ===\nif (closes.length < period + 1) {\n throw new Error(`Not enough data for RSI. Need at least ${period + 1}, got ${closes.length}`);\n}\n\n// === RSI Calculation ===\nfunction calculateRSI(prices, period) {\n const result = [];\n\n for (let i = period; i < prices.length; i++) {\n let gains = 0;\n let losses = 0;\n\n for (let j = i - period + 1; j <= i; j++) {\n const change = prices[j] - prices[j - 1];\n if (change >= 0) gains += change;\n else losses -= change;\n }\n\n const avgGain = gains / period;\n const avgLoss = losses / period;\n\n const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;\n const rsi = avgLoss === 0 ? 100 : 100 - (100 / (1 + rs));\n\n result.push({\n timestamp: klines[i][0],\n close: prices[i],\n rsi: rsi\n });\n }\n\n return result;\n}\n\n// === Run RSI ===\nconst rsiSeries = calculateRSI(closes, period);\n\n// === Return formatted RSI output ===\nreturn rsiSeries.map(r => ({\n json: {\n timestamp: r.timestamp,\n close: r.close,\n rsi: r.rsi\n }\n}));"
},
"typeVersion": 2
},
{
"id": "df32e55c-2dbc-44d6-8161-f70db651cfcd",
"name": "计算MACD(4小时)",
"type": "n8n-nodes-base.code",
"position": [
1120,
1660
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Parameters\nconst fastPeriod = 12;\nconst slowPeriod = 26;\nconst signalPeriod = 9;\n\n// Validate data length\nconst minRequired = slowPeriod + signalPeriod;\nif (closes.length < minRequired) {\n throw new Error(`Not enough data for MACD. Need at least ${minRequired}, got ${closes.length}`);\n}\n\n// === Helper: EMA function ===\nfunction calculateEMA(prices, period) {\n const k = 2 / (period + 1);\n const ema = [prices.slice(0, period).reduce((a, b) => a + b, 0) / period];\n\n for (let i = period; i < prices.length; i++) {\n ema.push(prices[i] * k + ema[ema.length - 1] * (1 - k));\n }\n\n return ema;\n}\n\n// === MACD Core Calculation ===\nconst slowEMA = calculateEMA(closes, slowPeriod);\nconst fastEMA = calculateEMA(closes.slice(slowPeriod - fastPeriod), fastPeriod);\n\n// Align lengths\nconst alignedFastEMA = fastEMA.slice(fastEMA.length - slowEMA.length);\nconst macdLine = alignedFastEMA.map((val, i) => val - slowEMA[i]);\n\n// Signal line\nconst signalLine = calculateEMA(macdLine, signalPeriod);\n\n// Histogram\nconst histogram = macdLine.slice(signalPeriod - 1).map((macd, i) => macd - signalLine[i]);\n\n// Final output start index\nconst startIndex = closes.length - histogram.length;\n\nconst result = [];\n\nfor (let i = 0; i < histogram.length; i++) {\n const idx = startIndex + i;\n result.push({\n timestamp: klines[idx]?.[0] || null,\n close: closes[idx],\n macd: macdLine[i + signalPeriod - 1],\n signal: signalLine[i],\n histogram: histogram[i]\n });\n}\n\nreturn result.map(r => ({\n json: r\n}));"
},
"typeVersion": 2
},
{
"id": "93a115f7-e876-43f0-8ecb-70a47a06f41c",
"name": "计算SMA(4小时)",
"type": "n8n-nodes-base.code",
"position": [
1120,
1880
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20; // Change to 50, 100, 200, etc. as needed\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Check for sufficient data\nif (closes.length < period) {\n throw new Error(`Not enough data to compute SMA. Need at least ${period}, got ${closes.length}`);\n}\n\n// Calculate SMA values\nconst result = [];\n\nfor (let i = period - 1; i < closes.length; i++) {\n const slice = closes.slice(i - period + 1, i + 1);\n const sum = slice.reduce((a, b) => a + b, 0);\n const average = sum / period;\n\n result.push({\n timestamp: klines[i][0],\n close: closes[i],\n sma: average\n });\n}\n\n// Return formatted output\nreturn result.map(r => ({\n json: r\n}));"
},
"typeVersion": 2
},
{
"id": "17bf4e9f-7053-4a04-b439-3e23fe5cb7f9",
"name": "计算EMA(4小时)",
"type": "n8n-nodes-base.code",
"position": [
1120,
2100
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20; // Change to 9, 12, 26, 50, etc. as needed\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Check for sufficient data\nif (closes.length < period) {\n throw new Error(`Not enough data to compute EMA. Need at least ${period}, got ${closes.length}`);\n}\n\n// EMA Calculation\nconst k = 2 / (period + 1);\nconst ema = [];\n\n// Start with SMA of first period\nlet sma = closes.slice(0, period).reduce((a, b) => a + b, 0) / period;\nema.push({\n timestamp: klines[period - 1][0],\n close: closes[period - 1],\n ema: sma\n});\n\n// Continue EMA calculation\nfor (let i = period; i < closes.length; i++) {\n const value = closes[i] * k + ema[ema.length - 1].ema * (1 - k);\n ema.push({\n timestamp: klines[i][0],\n close: closes[i],\n ema: value\n });\n}\n\n// Return result\nreturn ema.map(e => ({\n json: e\n}));"
},
"typeVersion": 2
},
{
"id": "d61db420-9def-4ec1-bf8a-94afeabda186",
"name": "计算ADX(4小时)",
"type": "n8n-nodes-base.code",
"position": [
1120,
2360
],
"parameters": {
"jsCode": "// Get kline array from merged item\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 14;\n\n// Parse high, low, close arrays\nconst highs = klines.map(k => parseFloat(k[2]));\nconst lows = klines.map(k => parseFloat(k[3]));\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Validation\nif (closes.length < period + 1) {\n throw new Error(`Not enough data for ADX. Need at least ${period + 1}, got ${closes.length}`);\n}\n\n// Initialize arrays\nconst tr = [];\nconst plusDM = [];\nconst minusDM = [];\n\n// Step 1: Calculate TR, +DM, -DM\nfor (let i = 1; i < klines.length; i++) {\n const highDiff = highs[i] - highs[i - 1];\n const lowDiff = lows[i - 1] - lows[i];\n const upMove = highDiff > 0 && highDiff > lowDiff ? highDiff : 0;\n const downMove = lowDiff > 0 && lowDiff > highDiff ? lowDiff : 0;\n\n const trueRange = Math.max(\n highs[i] - lows[i],\n Math.abs(highs[i] - closes[i - 1]),\n Math.abs(lows[i] - closes[i - 1])\n );\n\n tr.push(trueRange);\n plusDM.push(upMove);\n minusDM.push(downMove);\n}\n\n// Step 2: Smooth TR, +DM, -DM and calculate DI\nconst smoothedTR = [tr.slice(0, period).reduce((a, b) => a + b, 0)];\nconst smoothedPlusDM = [plusDM.slice(0, period).reduce((a, b) => a + b, 0)];\nconst smoothedMinusDM = [minusDM.slice(0, period).reduce((a, b) => a + b, 0)];\n\nfor (let i = period; i < tr.length; i++) {\n smoothedTR.push(smoothedTR[smoothedTR.length - 1] - smoothedTR[smoothedTR.length - 1] / period + tr[i]);\n smoothedPlusDM.push(smoothedPlusDM[smoothedPlusDM.length - 1] - smoothedPlusDM[smoothedPlusDM.length - 1] / period + plusDM[i]);\n smoothedMinusDM.push(smoothedMinusDM[smoothedMinusDM.length - 1] - smoothedMinusDM[smoothedMinusDM.length - 1] / period + minusDM[i]);\n}\n\nconst plusDI = smoothedPlusDM.map((val, i) => 100 * val / smoothedTR[i]);\nconst minusDI = smoothedMinusDM.map((val, i) => 100 * val / smoothedTR[i]);\n\n// Step 3: Calculate DX\nconst dx = plusDI.map((val, i) => {\n const diff = Math.abs(plusDI[i] - minusDI[i]);\n const sum = plusDI[i] + minusDI[i];\n return sum === 0 ? 0 : 100 * (diff / sum);\n});\n\n// Step 4: Smooth DX into ADX\nconst adx = [];\nconst firstADX = dx.slice(0, period).reduce((a, b) => a + b, 0) / period;\nadx.push(firstADX);\n\nfor (let i = period; i < dx.length; i++) {\n const newADX = (adx[adx.length - 1] * (period - 1) + dx[i]) / period;\n adx.push(newADX);\n}\n\n// Step 5: Final result formatting\nconst output = [];\n\nfor (let i = 0; i < adx.length; i++) {\n const index = i + (2 * period); // account for offset\n if (klines[index]) {\n output.push({\n timestamp: klines[index][0],\n close: closes[index],\n adx: adx[i],\n plusDI: plusDI[i + period],\n minusDI: minusDI[i + period]\n });\n }\n}\n\nreturn output.map(r => ({ json: r }));"
},
"typeVersion": 2
},
{
"id": "7940bc3f-c5f4-4b4b-b542-8737f17afbbf",
"name": "合并4小时指标",
"type": "n8n-nodes-base.merge",
"position": [
1720,
1600
],
"parameters": {
"numberInputs": 6
},
"typeVersion": 3.1
},
{
"id": "8585a269-b70d-4af7-b264-db9f638c8a77",
"name": "响应4小时Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
2120,
1660
],
"parameters": {
"options": {},
"respondWith": "allIncomingItems"
},
"typeVersion": 1.3
},
{
"id": "91a05310-bd36-4ee0-8381-b3d16c3346be",
"name": "Webhook 1日指标",
"type": "n8n-nodes-base.webhook",
"position": [
-1600,
3140
],
"webhookId": "23c8ec04-5aec-49c6-9c2c-c352ccec11b4",
"parameters": {
"path": "23c8ec04-5aec-49c6-9c2c-c352ccec11b4",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "29121e2e-f6e2-4d42-8e5b-e8eea1e61ec8",
"name": "HTTP请求 1日",
"type": "n8n-nodes-base.httpRequest",
"position": [
-1260,
3140
],
"parameters": {
"url": "https://api.binance.com/api/v3/klines",
"options": {},
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "symbol",
"value": "={{$json.body.symbol}}"
},
{
"name": "interval",
"value": "1d"
},
{
"name": "limit",
"value": "40"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "1be26316-3d08-4983-9d12-6bc36465e76e",
"name": "合并为1个数组(1日)",
"type": "n8n-nodes-base.code",
"position": [
-900,
3140
],
"parameters": {
"jsCode": "const klines = $input.all().map(item => item.json);\nreturn [{ json: { klines } }];"
},
"typeVersion": 2
},
{
"id": "6e9c7976-737b-4377-b412-633a1a67097b",
"name": "计算布林带(1日)",
"type": "n8n-nodes-base.code",
"position": [
-440,
2620
],
"parameters": {
"jsCode": "// Get raw klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20;\nconst stdMultiplier = 2;\n\n// Validate type\nif (!Array.isArray(klines)) {\n throw new Error(\"klines is not an array\");\n}\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Ensure enough data\nif (closes.length < period) {\n throw new Error(`Not enough data: got ${closes.length}, expected at least ${period}`);\n}\n\n// BB calculation logic\nfunction calculateBB(prices, period) {\n const result = [];\n for (let i = period - 1; i < prices.length; i++) {\n const slice = prices.slice(i - period + 1, i + 1);\n const mean = slice.reduce((sum, val) => sum + val, 0) / period;\n const variance = slice.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / period;\n const stdDev = Math.sqrt(variance);\n\n result.push({\n close: prices[i],\n basis: mean,\n upper: mean + stdMultiplier * stdDev,\n lower: mean - stdMultiplier * stdDev,\n timestamp: klines[i][0]\n });\n }\n return result;\n}\n\n// Calculate bands\nconst bands = calculateBB(closes, period);\n\n// Format output\nreturn bands.map(b => ({\n json: {\n timestamp: b.timestamp,\n close: b.close,\n bb_basis: b.basis,\n bb_upper: b.upper,\n bb_lower: b.lower\n }\n}));"
},
"typeVersion": 2
},
{
"id": "ac84877a-75cf-4ef2-bad1-8d0f444ecead",
"name": "计算RSI(1日)",
"type": "n8n-nodes-base.code",
"position": [
-440,
2880
],
"parameters": {
"jsCode": "// Pull klines array from JSON\nconst klines = $input.first().json.klines;\n\n// === CONFIGURATION ===\nconst period = 14; // RSI period\n\n// === Extract closing prices ===\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// === Ensure enough data ===\nif (closes.length < period + 1) {\n throw new Error(`Not enough data for RSI. Need at least ${period + 1}, got ${closes.length}`);\n}\n\n// === RSI Calculation ===\nfunction calculateRSI(prices, period) {\n const result = [];\n\n for (let i = period; i < prices.length; i++) {\n let gains = 0;\n let losses = 0;\n\n for (let j = i - period + 1; j <= i; j++) {\n const change = prices[j] - prices[j - 1];\n if (change >= 0) gains += change;\n else losses -= change;\n }\n\n const avgGain = gains / period;\n const avgLoss = losses / period;\n\n const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;\n const rsi = avgLoss === 0 ? 100 : 100 - (100 / (1 + rs));\n\n result.push({\n timestamp: klines[i][0],\n close: prices[i],\n rsi: rsi\n });\n }\n\n return result;\n}\n\n// === Run RSI ===\nconst rsiSeries = calculateRSI(closes, period);\n\n// === Return formatted RSI output ===\nreturn rsiSeries.map(r => ({\n json: {\n timestamp: r.timestamp,\n close: r.close,\n rsi: r.rsi\n }\n}));"
},
"typeVersion": 2
},
{
"id": "a5da65c5-db5a-4f9d-a6cf-228f081ffb3c",
"name": "计算MACD(1日)",
"type": "n8n-nodes-base.code",
"position": [
-440,
3120
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Parameters\nconst fastPeriod = 12;\nconst slowPeriod = 26;\nconst signalPeriod = 9;\n\n// Validate data length\nconst minRequired = slowPeriod + signalPeriod;\nif (closes.length < minRequired) {\n throw new Error(`Not enough data for MACD. Need at least ${minRequired}, got ${closes.length}`);\n}\n\n// === Helper: EMA function ===\nfunction calculateEMA(prices, period) {\n const k = 2 / (period + 1);\n const ema = [prices.slice(0, period).reduce((a, b) => a + b, 0) / period];\n\n for (let i = period; i < prices.length; i++) {\n ema.push(prices[i] * k + ema[ema.length - 1] * (1 - k));\n }\n\n return ema;\n}\n\n// === MACD Core Calculation ===\nconst slowEMA = calculateEMA(closes, slowPeriod);\nconst fastEMA = calculateEMA(closes.slice(slowPeriod - fastPeriod), fastPeriod);\n\n// Align lengths\nconst alignedFastEMA = fastEMA.slice(fastEMA.length - slowEMA.length);\nconst macdLine = alignedFastEMA.map((val, i) => val - slowEMA[i]);\n\n// Signal line\nconst signalLine = calculateEMA(macdLine, signalPeriod);\n\n// Histogram\nconst histogram = macdLine.slice(signalPeriod - 1).map((macd, i) => macd - signalLine[i]);\n\n// Final output start index\nconst startIndex = closes.length - histogram.length;\n\nconst result = [];\n\nfor (let i = 0; i < histogram.length; i++) {\n const idx = startIndex + i;\n result.push({\n timestamp: klines[idx]?.[0] || null,\n close: closes[idx],\n macd: macdLine[i + signalPeriod - 1],\n signal: signalLine[i],\n histogram: histogram[i]\n });\n}\n\nreturn result.map(r => ({\n json: r\n}));"
},
"typeVersion": 2
},
{
"id": "5b1f34b3-e675-46b1-a754-679080680058",
"name": "计算SMA(1日)",
"type": "n8n-nodes-base.code",
"position": [
-440,
3340
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20; // Change to 50, 100, 200, etc. as needed\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Check for sufficient data\nif (closes.length < period) {\n throw new Error(`Not enough data to compute SMA. Need at least ${period}, got ${closes.length}`);\n}\n\n// Calculate SMA values\nconst result = [];\n\nfor (let i = period - 1; i < closes.length; i++) {\n const slice = closes.slice(i - period + 1, i + 1);\n const sum = slice.reduce((a, b) => a + b, 0);\n const average = sum / period;\n\n result.push({\n timestamp: klines[i][0],\n close: closes[i],\n sma: average\n });\n}\n\n// Return formatted output\nreturn result.map(r => ({\n json: r\n}));"
},
"typeVersion": 2
},
{
"id": "0b3278a6-5793-4d70-8954-70562c43a3eb",
"name": "计算EMA(1日)",
"type": "n8n-nodes-base.code",
"position": [
-440,
3580
],
"parameters": {
"jsCode": "// Get klines array\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 20; // Change to 9, 12, 26, 50, etc. as needed\n\n// Extract closing prices\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Check for sufficient data\nif (closes.length < period) {\n throw new Error(`Not enough data to compute EMA. Need at least ${period}, got ${closes.length}`);\n}\n\n// EMA Calculation\nconst k = 2 / (period + 1);\nconst ema = [];\n\n// Start with SMA of first period\nlet sma = closes.slice(0, period).reduce((a, b) => a + b, 0) / period;\nema.push({\n timestamp: klines[period - 1][0],\n close: closes[period - 1],\n ema: sma\n});\n\n// Continue EMA calculation\nfor (let i = period; i < closes.length; i++) {\n const value = closes[i] * k + ema[ema.length - 1].ema * (1 - k);\n ema.push({\n timestamp: klines[i][0],\n close: closes[i],\n ema: value\n });\n}\n\n// Return result\nreturn ema.map(e => ({\n json: e\n}));"
},
"typeVersion": 2
},
{
"id": "28dd6fb1-a161-49ed-9c14-740e88e2195d",
"name": "计算ADX(1日)",
"type": "n8n-nodes-base.code",
"position": [
-440,
3780
],
"parameters": {
"jsCode": "// Get kline array from merged item\nconst klines = $input.first().json.klines;\n\n// Parameters\nconst period = 14;\n\n// Parse high, low, close arrays\nconst highs = klines.map(k => parseFloat(k[2]));\nconst lows = klines.map(k => parseFloat(k[3]));\nconst closes = klines.map(k => parseFloat(k[4]));\n\n// Validation\nif (closes.length < period + 1) {\n throw new Error(`Not enough data for ADX. Need at least ${period + 1}, got ${closes.length}`);\n}\n\n// Initialize arrays\nconst tr = [];\nconst plusDM = [];\nconst minusDM = [];\n\n// Step 1: Calculate TR, +DM, -DM\nfor (let i = 1; i < klines.length; i++) {\n const highDiff = highs[i] - highs[i - 1];\n const lowDiff = lows[i - 1] - lows[i];\n const upMove = highDiff > 0 && highDiff > lowDiff ? highDiff : 0;\n const downMove = lowDiff > 0 && lowDiff > highDiff ? lowDiff : 0;\n\n const trueRange = Math.max(\n highs[i] - lows[i],\n Math.abs(highs[i] - closes[i - 1]),\n Math.abs(lows[i] - closes[i - 1])\n );\n\n tr.push(trueRange);\n plusDM.push(upMove);\n minusDM.push(downMove);\n}\n\n// Step 2: Smooth TR, +DM, -DM and calculate DI\nconst smoothedTR = [tr.slice(0, period).reduce((a, b) => a + b, 0)];\nconst smoothedPlusDM = [plusDM.slice(0, period).reduce((a, b) => a + b, 0)];\nconst smoothedMinusDM = [minusDM.slice(0, period).reduce((a, b) => a + b, 0)];\n\nfor (let i = period; i < tr.length; i++) {\n smoothedTR.push(smoothedTR[smoothedTR.length - 1] - smoothedTR[smoothedTR.length - 1] / period + tr[i]);\n smoothedPlusDM.push(smoothedPlusDM[smoothedPlusDM.length - 1] - smoothedPlusDM[smoothedPlusDM.length - 1] / period + plusDM[i]);\n smoothedMinusDM.push(smoothedMinusDM[smoothedMinusDM.length - 1] - smoothedMinusDM[smoothedMinusDM.length - 1] / period + minusDM[i]);\n}\n\nconst plusDI = smoothedPlusDM.map((val, i) => 100 * val / smoothedTR[i]);\nconst minusDI = smoothedMinusDM.map((val, i) => 100 * val / smoothedTR[i]);\n\n// Step 3: Calculate DX\nconst dx = plusDI.map((val, i) => {\n const diff = Math.abs(plusDI[i] - minusDI[i]);\n const sum = plusDI[i] + minusDI[i];\n return sum === 0 ? 0 : 100 * (diff / sum);\n});\n\n// Step 4: Smooth DX into ADX\nconst adx = [];\nconst firstADX = dx.slice(0, period).reduce((a, b) => a + b, 0) / period;\nadx.push(firstADX);\n\nfor (let i = period; i < dx.length; i++) {\n const newADX = (adx[adx.length - 1] * (period - 1) + dx[i]) / period;\n adx.push(newADX);\n}\n\n// Step 5: Final result formatting\nconst output = [];\n\nfor (let i = 0; i < adx.length; i++) {\n const index = i + (2 * period); // account for offset\n if (klines[index]) {\n output.push({\n timestamp: klines[index][0],\n close: closes[index],\n adx: adx[i],\n plusDI: plusDI[i + period],\n minusDI: minusDI[i + period]\n });\n }\n}\n\nreturn output.map(r => ({ json: r }));"
},
"typeVersion": 2
},
{
"id": "660dfcb4-ed63-4c53-806b-cba96c105fee",
"name": "合并1日指标",
"type": "n8n-nodes-base.merge",
"position": [
160,
3060
],
"parameters": {
"numberInputs": 6
},
"typeVersion": 3.1
},
{
"id": "939665ce-7a50-4a6d-9b37-62ad800350b0",
"name": "响应1日Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
560,
3120
],
"parameters": {
"options": {},
"respondWith": "allIncomingItems"
},
"typeVersion": 1.3
},
{
"id": "2f44a150-0c5a-4b74-92b5-fe941fccb86d",
"name": "便签 13",
"type": "n8n-nodes-base.stickyNote",
"position": [
100,
-340
],
"parameters": {
"color": 5,
"width": 260,
"height": 620,
"content": "## 技术指标API合并"
},
"typeVersion": 1
},
{
"id": "fbf6424d-8bfc-469b-ae56-c9824e657e75",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
1640,
1340
],
"parameters": {
"color": 5,
"width": 260,
"height": 620,
"content": "## 技术指标API合并"
},
"typeVersion": 1
},
{
"id": "fba15cd9-e7a0-4a03-813e-444dd2cd25f7",
"name": "便签14",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1540,
1380
],
"parameters": {
"color": 5,
"width": 260,
"height": 620,
"content": "## 技术指标API合并"
},
"typeVersion": 1
},
{
"id": "ab87eb1b-3048-4717-af97-b7fa92c0f260",
"name": "便签15",
"type": "n8n-nodes-base.stickyNote",
"position": [
80,
2760
],
"parameters": {
"color": 5,
"width": 260,
"height": 620,
"content": "## 技术指标API合并"
},
"typeVersion": 1
},
{
"id": "b3abceb1-3605-4cca-8b4e-1f85f792fff2",
"name": "便签 1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1540,
-500
],
"parameters": {
"color": 4,
"height": 680,
"content": "## Webhooks(15分钟数据)"
},
"typeVersion": 1
},
{
"id": "2143ce8a-8746-4187-bc43-c9cc294da7b8",
"name": "便签 2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3300,
1240
],
"parameters": {
"color": 4,
"height": 680,
"content": "## Webhooks(1小时数据)"
},
"typeVersion": 1
},
{
"id": "d9150fb0-42b5-48e8-b123-e588672a653f",
"name": "便签 3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-100,
1200
],
"parameters": {
"color": 4,
"height": 680,
"content": "## Webhooks(4小时数据)"
},
"typeVersion": 1
},
{
"id": "50737e51-ef67-42af-bf15-ac228c750f4d",
"name": "便签 4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1680,
2640
],
"parameters": {
"color": 4,
"height": 680,
"content": "## Webhooks(1日数据)"
},
"typeVersion": 1
},
{
"id": "61972dcf-2bd5-454e-880b-0d3e97c82f98",
"name": "便签 5",
"type": "n8n-nodes-base.stickyNote",
"position": [
480,
-480
],
"parameters": {
"color": 4,
"width": 280,
"height": 640,
"content": "## Webhooks响应(15分钟数据)"
},
"typeVersion": 1
},
{
"id": "c545e86b-8f90-4e3b-8e72-25f56de0b214",
"name": "便签6",
"type": "n8n-nodes-base.stickyNote",
"position": [
2020,
1200
],
"parameters": {
"color": 4,
"width": 280,
"height": 640,
"content": "## Webhooks响应(4小时数据)"
},
"typeVersion": 1
},
{
"id": "9e838383-1c81-45c6-98f1-f735dc5789df",
"name": "便签7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1140,
1260
],
"parameters": {
"color": 4,
"width": 280,
"height": 640,
"content": "## Webhooks响应(1小时数据)"
},
"typeVersion": 1
},
{
"id": "f05d2def-9593-46c5-862d-378a5d1f64d4",
"name": "便签8",
"type": "n8n-nodes-base.stickyNote",
"position": [
480,
2660
],
"parameters": {
"color": 4,
"width": 280,
"height": 640,
"content": "## Webhooks响应(1日数据)"
},
"typeVersion": 1
},
{
"id": "ceb5f8e0-e51f-4554-a752-a319033c14c4",
"name": "便签9",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1160,
-300
],
"parameters": {
"color": 6,
"width": 260,
"height": 520,
"content": "## 币安API调用 15分钟"
},
"typeVersion": 1
},
{
"id": "8e1aeb88-3232-4ea7-85c3-49a75097341b",
"name": "便签 10",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2940,
1480
],
"parameters": {
"color": 6,
"width": 260,
"height": 500,
"content": "## 币安API调用 1小时"
},
"typeVersion": 1
},
{
"id": "53996473-bdd7-44d3-9060-96a2f072077c",
"name": "便签11",
"type": "n8n-nodes-base.stickyNote",
"position": [
240,
1420
],
"parameters": {
"color": 6,
"width": 260,
"height": 500,
"content": "## 币安API调用 4小时"
},
"typeVersion": 1
},
{
"id": "6b138f55-5f0e-4844-a570-47919c8888b5",
"name": "便签 12",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1340,
2920
],
"parameters": {
"color": 6,
"width": 260,
"height": 420,
"content": "## 币安API调用 1日"
},
"typeVersion": 1
},
{
"id": "8d8bd17f-c437-4e57-a517-3b6eb44eeff7",
"name": "便签 16",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2120,
720
],
"parameters": {
"color": 2,
"height": 1860,
"content": "## 计算技术指标"
},
"typeVersion": 1
},
{
"id": "ee6e6788-7b9d-4d29-9cdd-0af5256289ef",
"name": "便签 17",
"type": "n8n-nodes-base.stickyNote",
"position": [
-360,
-860
],
"parameters": {
"color": 2,
"width": 260,
"height": 1720,
"content": "## 计算技术指标"
},
"typeVersion": 1
},
{
"id": "e53bcd92-c603-43f3-b25f-0d7dd46afca0",
"name": "便签 18",
"type": "n8n-nodes-base.stickyNote",
"position": [
1040,
720
],
"parameters": {
"color": 2,
"width": 260,
"height": 1820,
"content": "## 计算技术指标"
},
"typeVersion": 1
},
{
"id": "7d27698c-bbc5-4faf-aa41-4c4332abb484",
"name": "便签19",
"type": "n8n-nodes-base.stickyNote",
"position": [
-520,
2180
],
"parameters": {
"color": 2,
"width": 260,
"height": 1800,
"content": "## 计算技术指标"
},
"typeVersion": 1
},
{
"id": "309f22d1-aeb2-4d48-a7c4-deaad51070e3",
"name": "便签20",
"type": "n8n-nodes-base.stickyNote",
"position": [
-820,
-240
],
"parameters": {
"color": 5,
"width": 260,
"height": 460,
"content": "## API合并"
},
"typeVersion": 1
},
{
"id": "5a27535c-5bc9-4718-b895-5a8eccf37974",
"name": "便签21",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2600,
1560
],
"parameters": {
"color": 5,
"width": 260,
"height": 420,
"content": "## API合并"
},
"typeVersion": 1
},
{
"id": "52180c40-a1d3-4c41-aea6-73f62f7c98ee",
"name": "便签22",
"type": "n8n-nodes-base.stickyNote",
"position": [
-980,
2920
],
"parameters": {
"color": 5,
"width": 260,
"height": 460,
"content": "## API合并"
},
"typeVersion": 1
},
{
"id": "08697498-181c-417f-8027-8d6de1471bed",
"name": "便签23",
"type": "n8n-nodes-base.stickyNote",
"position": [
560,
1480
],
"parameters": {
"color": 5,
"width": 260,
"height": 440,
"content": "## API合并"
},
"typeVersion": 1
},
{
"id": "773a2b4f-f716-41ab-ba18-1412e2dd50e5",
"name": "便签24",
"type": "n8n-nodes-base.stickyNote",
"position": [
2800,
-60
],
"parameters": {
"width": 1460,
"height": 3020,
"content": "# 🧪 币安技术指标 Webhook 工具 – 文档"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "f7032997-bb7f-4e2a-9982-7ee18c95cfbe",
"connections": {
"HTTP Request": {
"main": [
[
{
"node": "Merge Into 1 Array",
"type": "main",
"index": 0
}
]
]
},
"Calculate ADX": {
"main": [
[
{
"node": "Merge 15 min Indicators",
"type": "main",
"index": 5
}
]
]
},
"Calculate EMA": {
"main": [
[
{
"node": "Merge 15 min Indicators",
"type": "main",
"index": 4
}
]
]
},
"Calculate RSI": {
"main": [
[
{
"node": "Merge 15 min Indicators",
"type": "main",
"index": 1
}
]
]
},
"Calculate SMA": {
"main": [
[
{
"node": "Merge 15 min Indicators",
"type": "main",
"index": 3
}
]
]
},
"Calculate ADX1": {
"main": [
[
{
"node": "Merge 1h Indicators",
"type": "main",
"index": 5
}
]
]
},
"Calculate MACD": {
"main": [
[
{
"node": "Merge 15 min Indicators",
"type": "main",
"index": 2
}
]
]
},
"HTTP Request 1d": {
"main": [
[
{
"node": "Merge Into 1 Array 1d",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request 1h": {
"main": [
[
{
"node": "Merge Into 1 Array 1h",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request 4h": {
"main": [
[
{
"node": "Merge Into 1 Array 4h",
"type": "main",
"index": 0
}
]
]
},
"Calculate EMA(1d)": {
"main": [
[
{
"node": "Merge 1d Indicators",
"type": "main",
"index": 4
}
]
]
},
"Calculate EMA(1h)": {
"main": [
[
{
"node": "Merge 1h Indicators",
"type": "main",
"index": 4
}
]
]
},
"Calculate EMA(4h)": {
"main": [
[
{
"node": "Merge 4h Indicators",
"type": "main",
"index": 4
}
]
]
},
"Calculate RSI(1d)": {
"main": [
[
{
"node": "Merge 1d Indicators",
"type": "main",
"index": 1
}
]
]
},
"Calculate RSI(1h)": {
"main": [
[
{
"node": "Merge 1h Indicators",
"type": "main",
"index": 1
}
]
]
},
"Calculate RSI(4h)": {
"main": [
[
{
"node": "Merge 4h Indicators",
"type": "main",
"index": 1
}
]
]
},
"Calculate SMA(1d)": {
"main": [
[
{
"node": "Merge 1d Indicators",
"type": "main",
"index": 3
}
]
]
},
"Calculate SMA(1h)": {
"main": [
[
{
"node": "Merge 1h Indicators",
"type": "main",
"index": 3
}
]
]
},
"Calculate SMA(4h)": {
"main": [
[
{
"node": "Merge 4h Indicators",
"type": "main",
"index": 3
}
]
]
},
"Calculate ADX (1d)": {
"main": [
[
{
"node": "Merge 1d Indicators",
"type": "main",
"index": 5
}
]
]
},
"Calculate ADX (4h)": {
"main": [
[
{
"node": "Merge 4h Indicators",
"type": "main",
"index": 5
}
]
]
},
"Calculate MACD(1d)": {
"main": [
[
{
"node": "Merge 1d Indicators",
"type": "main",
"index": 2
}
]
]
},
"Calculate MACD(1h)": {
"main": [
[
{
"node": "Merge 1h Indicators",
"type": "main",
"index": 2
}
]
]
},
"Calculate MACD(4h)": {
"main": [
[
{
"node": "Merge 4h Indicators",
"type": "main",
"index": 2
}
]
]
},
"Merge Into 1 Array": {
"main": [
[
{
"node": "Calculate Bollinger Bands",
"type": "main",
"index": 0
},
{
"node": "Calculate RSI",
"type": "main",
"index": 0
},
{
"node": "Calculate MACD",
"type": "main",
"index": 0
},
{
"node": "Calculate SMA",
"type": "main",
"index": 0
},
{
"node": "Calculate EMA",
"type": "main",
"index": 0
},
{
"node": "Calculate ADX",
"type": "main",
"index": 0
}
]
]
},
"Merge 1d Indicators": {
"main": [
[
{
"node": "Respond to 1d Webhook",
"type": "main",
"index": 0
}
]
]
},
"Merge 1h Indicators": {
"main": [
[
{
"node": "Respond to 1h Webhook",
"type": "main",
"index": 0
}
]
]
},
"Merge 4h Indicators": {
"main": [
[
{
"node": "Respond to 4h Webhook",
"type": "main",
"index": 0
}
]
]
},
"Merge Into 1 Array 1d": {
"main": [
[
{
"node": "Calculate Bollinger Bands(1d)",
"type": "main",
"index": 0
},
{
"node": "Calculate RSI(1d)",
"type": "main",
"index": 0
},
{
"node": "Calculate MACD(1d)",
"type": "main",
"index": 0
},
{
"node": "Calculate SMA(1d)",
"type": "main",
"index": 0
},
{
"node": "Calculate EMA(1d)",
"type": "main",
"index": 0
},
{
"node": "Calculate ADX (1d)",
"type": "main",
"index": 0
}
]
]
},
"Merge Into 1 Array 1h": {
"main": [
[
{
"node": "Calculate Bollinger Bands(1h)",
"type": "main",
"index": 0
},
{
"node": "Calculate RSI(1h)",
"type": "main",
"index": 0
},
{
"node": "Calculate MACD(1h)",
"type": "main",
"index": 0
},
{
"node": "Calculate SMA(1h)",
"type": "main",
"index": 0
},
{
"node": "Calculate EMA(1h)",
"type": "main",
"index": 0
},
{
"node": "Calculate ADX1",
"type": "main",
"index": 0
}
]
]
},
"Merge Into 1 Array 4h": {
"main": [
[
{
"node": "Calculate Bollinger Bands(4h)",
"type": "main",
"index": 0
},
{
"node": "Calculate RSI(4h)",
"type": "main",
"index": 0
},
{
"node": "Calculate MACD(4h)",
"type": "main",
"index": 0
},
{
"node": "Calculate SMA(4h)",
"type": "main",
"index": 0
},
{
"node": "Calculate EMA(4h)",
"type": "main",
"index": 0
},
{
"node": "Calculate ADX (4h)",
"type": "main",
"index": 0
}
]
]
},
"Webhook 1d Indicators": {
"main": [
[
{
"node": "HTTP Request 1d",
"type": "main",
"index": 0
}
]
]
},
"Webhook 1h Indicators": {
"main": [
[
{
"node": "HTTP Request 1h",
"type": "main",
"index": 0
}
]
]
},
"Webhook 4h Indicators": {
"main": [
[
{
"node": "HTTP Request 4h",
"type": "main",
"index": 0
}
]
]
},
"Webhook 15m Indicators": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"Merge 15 min Indicators": {
"main": [
[
{
"node": "Respond to 15m Webhook",
"type": "main",
"index": 0
}
]
]
},
"Calculate Bollinger Bands": {
"main": [
[
{
"node": "Merge 15 min Indicators",
"type": "main",
"index": 0
}
]
]
},
"Calculate Bollinger Bands(1d)": {
"main": [
[
{
"node": "Merge 1d Indicators",
"type": "main",
"index": 0
}
]
]
},
"Calculate Bollinger Bands(1h)": {
"main": [
[
{
"node": "Merge 1h Indicators",
"type": "main",
"index": 0
}
]
]
},
"Calculate Bollinger Bands(4h)": {
"main": [
[
{
"node": "Merge 4h Indicators",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - 财务, 人工智能
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
特斯拉量化技术指标Webhooks工具 n8n
特斯拉量化技术指标Webhooks工具
Code
Merge
Webhook
+3
61 节点Don Jayamaha Jr
财务
Binance SM 新闻和情绪分析师 Webhook 工具
使用GPT-4o和Telegram警报分析任何代币的加密货币新闻情绪
Set
Code
Merge
+7
28 节点Don Jayamaha Jr
财务
加密货币新闻与情绪分析
通过Telegram使用GPT-4o进行实时加密货币新闻与情绪分析
Set
Code
Merge
+7
30 节点Don Jayamaha Jr
财务
ERPNext AI候选人筛选自动化
ERPNext的AI驱动候选人筛选自动化
If
Set
Code
+11
39 节点Amjid Ali
财务
使用Gmail、OpenAI和Google Drive提取和分类发票与收据
使用Gmail、OpenAI和Google Drive提取和分类发票与收据
If
Set
Code
+10
20 节点Tom
财务
Yookassa支付处理
使用YooKassa和Google表格进行支付处理与订单跟踪
If
Set
Code
+7
44 节点Sergey Skorobogatov
财务
工作流信息
难度等级
高级
节点数量69
分类2
节点类型6
作者
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 查看 →
分享此工作流