This workflow corresponds to n8n.io template #4747 — we link there as the canonical source.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "HrTD222kWpvKsy2j",
"name": "Binance SM Indicators Webhook Tool",
"tags": [],
"nodes": [
{
"id": "61cb3bd7-60ba-4dcf-a080-146b7612a99b",
"name": "HTTP Request",
"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 15m Indicators",
"type": "n8n-nodes-base.webhook",
"position": [
-1460,
0
],
"parameters": {
"path": "39cc366c-af5f-472a-9d48-bbe30a4fe3ea",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "46b937d5-7338-457f-b2f0-eb85ac44a8bb",
"name": "Merge Into 1 Array",
"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": "Calculate Bollinger Bands",
"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": "Calculate 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": "Calculate 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": "Calculate 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": "Calculate 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": "Calculate 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": "Merge 15 min Indicators",
"type": "n8n-nodes-base.merge",
"position": [
180,
-80
],
"parameters": {
"numberInputs": 6
},
"typeVersion": 3.1
},
{
"id": "0a46ea7c-a85d-4fc6-b20c-7e65ab962ea6",
"name": "Respond to 15m Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
560,
-20
],
"parameters": {
"options": {},
"respondWith": "allIncomingItems"
},
"typeVersion": 1.3
},
{
"id": "40fcbba9-489b-4452-baf8-9a41ef0f9b07",
"name": "Webhook 1h Indicators",
"type": "n8n-nodes-base.webhook",
"position": [
-3220,
1760
],
"parameters": {
"path": "78948764-5cdb-4808-8ef9-2155f10dd721",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "39e6b058-d800-4751-b75b-5e62be379c61",
"name": "Respond to 1h Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-1060,
1740
],
"parameters": {
"options": {},
"respondWith": "allIncomingItems"
},
"typeVersion": 1.3
},
{
"id": "af471a36-1107-4ccd-be14-efa06ba27330",
"name": "Merge 1h Indicators",
"type": "n8n-nodes-base.merge",
"position": [
-1460,
1680
],
"parameters": {
"numberInputs": 6
},
"typeVersion": 3.1
},
{
"id": "d7d9bb31-6986-42e6-9f0c-7d7f00d66ee1",
"name": "Merge Into 1 Array 1h",
"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": "Calculate Bollinger Bands(1h)",
"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": "Calculate RSI(1h)",
"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": "Calculate MACD(1h)",
"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": "Calculate SMA(1h)",
"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": "Calculate EMA(1h)",
"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": "Calculate 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 Request 1h",
"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 4h Indicators",
"type": "n8n-nodes-base.webhook",
"position": [
-40,
1680
],
"parameters": {
"path": "55fd9665-ed4a-4e1a-8062-890cad0fb6ac",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "bbb81a2a-b1c4-4c1e-80e9-a4990c45fae8",
"name": "HTTP Request 4h",
"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": "Merge Into 1 Array 4h",
"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": "Calculate Bollinger Bands(4h)",
"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": "Calculate RSI(4h)",
"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": "Calculate MACD(4h)",
"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": "Calculate SMA(4h)",
"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": "Calculate EMA(4h)",
"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": "Calculate ADX (4h)",
"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": "Merge 4h Indicators",
"type": "n8n-nodes-base.merge",
"position": [
1720,
1600
],
"parameters": {
"numberInputs": 6
},
"typeVersion": 3.1
},
{
"id": "8585a269-b70d-4af7-b264-db9f638c8a77",
"name": "Respond to 4h Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
2120,
1660
],
"parameters": {
"options": {},
"respondWith": "allIncomingItems"
},
"typeVersion": 1.3
},
{
"id": "91a05310-bd36-4ee0-8381-b3d16c3346be",
"name": "Webhook 1d Indicators",
"type": "n8n-nodes-base.webhook",
"position": [
-1600,
3140
],
"parameters": {
"path": "23c8ec04-5aec-49c6-9c2c-c352ccec11b4",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "29121e2e-f6e2-4d42-8e5b-e8eea1e61ec8",
"name": "HTTP Request 1d",
"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": "Merge Into 1 Array 1d",
"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": "Calculate Bollinger Bands(1d)",
"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": "Calculate RSI(1d)",
"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": "Calculate MACD(1d)",
"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": "Calculate SMA(1d)",
"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": "Calculate EMA(1d)",
"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": "Calculate ADX (1d)",
"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": "Merge 1d Indicators",
"type": "n8n-nodes-base.merge",
"position": [
160,
3060
],
"parameters": {
"numberInputs": 6
},
"typeVersion": 3.1
},
{
"id": "939665ce-7a50-4a6d-9b37-62ad800350b0",
"name": "Respond to 1d Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
560,
3120
],
"parameters": {
"options": {},
"respondWith": "allIncomingItems"
},
"typeVersion": 1.3
},
{
"id": "2f44a150-0c5a-4b74-92b5-fe941fccb86d",
"name": "Sticky Note13",
"type": "n8n-nodes-base.stickyNote",
"position": [
100,
-340
],
"parameters": {
"color": 5,
"width": 260,
"height": 620,
"content": "## Technical Indicator API Merge \n\n**Binance API** for 6 indicators:\n\n\u2022 **RSI, MACD, BBANDS, SMA, EMA, ADX**"
},
"typeVersion": 1
},
{
"id": "fbf6424d-8bfc-469b-ae56-c9824e657e75",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
1640,
1340
],
"parameters": {
"color": 5,
"width": 260,
"height": 620,
"content": "## Technical Indicator API Merge \n\n**Binance API** for 6 indicators:\n\n\u2022 **RSI, MACD, BBANDS, SMA, EMA, ADX**"
},
"typeVersion": 1
},
{
"id": "fba15cd9-e7a0-4a03-813e-444dd2cd25f7",
"name": "Sticky Note14",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1540,
1380
],
"parameters": {
"color": 5,
"width": 260,
"height": 620,
"content": "## Technical Indicator API Merge \n\n**Binance API** for 6 indicators:\n\n\u2022 **RSI, MACD, BBANDS, SMA, EMA, ADX**"
},
"typeVersion": 1
},
{
"id": "ab87eb1b-3048-4717-af97-b7fa92c0f260",
"name": "Sticky Note15",
"type": "n8n-nodes-base.stickyNote",
"position": [
80,
2760
],
"parameters": {
"color": 5,
"width": 260,
"height": 620,
"content": "## Technical Indicator API Merge \n\n**Binance API** for 6 indicators:\n\n\u2022 **RSI, MACD, BBANDS, SMA, EMA, ADX**"
},
"typeVersion": 1
},
{
"id": "b3abceb1-3605-4cca-8b4e-1f85f792fff2",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1540,
-500
],
"parameters": {
"color": 4,
"height": 680,
"content": "## Webhooks (15minData)\n\n\u2022 **Entry point** for external workflows calling indicator batches\n\n\u2022 Orchestrates trigger for **indicator API pull + formatter nodes**\n\n\u2022 Returns **cleaned batch** back via Respond to Webhook node\n\n\ud83d\udcce Notes:\n\n\u2022 Must pass through correct **webhook** path\n\n\u2022 Responds after **merge with all 6 formatted outputs** per timeframe"
},
"typeVersion": 1
},
{
"id": "2143ce8a-8746-4187-bc43-c9cc294da7b8",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3300,
1240
],
"parameters": {
"color": 4,
"height": 680,
"content": "## Webhooks (1hourData)\n\n\u2022 **Entry point** for external workflows calling indicator batches\n\n\u2022 Orchestrates trigger for **indicator API pull + formatter nodes**\n\n\u2022 Returns **cleaned batch** back via Respond to Webhook node\n\n\ud83d\udcce Notes:\n\n\u2022 Must pass through correct **webhook** path\n\n\u2022 Responds after **merge with all 6 formatted outputs** per timeframe"
},
"typeVersion": 1
},
{
"id": "d9150fb0-42b5-48e8-b123-e588672a653f",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-100,
1200
],
"parameters": {
"color": 4,
"height": 680,
"content": "## Webhooks (4hourData)\n\n\u2022 **Entry point** for external workflows calling indicator batches\n\n\u2022 Orchestrates trigger for **indicator API pull + formatter nodes**\n\n\u2022 Returns **cleaned batch** back via Respond to Webhook node\n\n\ud83d\udcce Notes:\n\n\u2022 Must pass through correct **webhook** path\n\n\u2022 Responds after **merge with all 6 formatted outputs** per timeframe"
},
"typeVersion": 1
},
{
"id": "50737e51-ef67-42af-bf15-ac228c750f4d",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1680,
2640
],
"parameters": {
"color": 4,
"height": 680,
"content": "## Webhooks (1dayData)\n\n\u2022 **Entry point** for external workflows calling indicator batches\n\n\u2022 Orchestrates trigger for **indicator API pull + formatter nodes**\n\n\u2022 Returns **cleaned batch** back via Respond to Webhook node\n\n\ud83d\udcce Notes:\n\n\u2022 Must pass through correct **webhook** path\n\n\u2022 Responds after **merge with all 6 formatted outputs** per timeframe"
},
"typeVersion": 1
},
{
"id": "61972dcf-2bd5-454e-880b-0d3e97c82f98",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
480,
-480
],
"parameters": {
"color": 4,
"width": 280,
"height": 640,
"content": "## Webhooks Respond (15minData)\n\n\u2022 **Exit point** for external workflows calling indicator batches\n\n\u2022 Orchestrates trigger for **indicator API pull + formatter nodes**\n\n\u2022 Returns **cleaned batch** back via Respond to Webhook node\n\n\ud83d\udcce Notes:\n\n\u2022 Must pass through correct **webhook** path\n\n\u2022 Responds after **merge with all 6 formatted outputs** per timeframe"
},
"typeVersion": 1
},
{
"id": "c545e86b-8f90-4e3b-8e72-25f56de0b214",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
2020,
1200
],
"parameters": {
"color": 4,
"width": 280,
"height": 640,
"content": "## Webhooks Respond (4hourData)\n\n\u2022 **Exit point** for external workflows calling indicator batches\n\n\u2022 Orchestrates trigger for **indicator API pull + formatter nodes**\n\n\u2022 Returns **cleaned batch** back via Respond to Webhook node\n\n\ud83d\udcce Notes:\n\n\u2022 Must pass through correct **webhook** path\n\n\u2022 Responds after **merge with all 6 formatted outputs** per timeframe"
},
"typeVersion": 1
},
{
"id": "9e838383-1c81-45c6-98f1-f735dc5789df",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1140,
1260
],
"parameters": {
"color": 4,
"width": 280,
"height": 640,
"content": "## Webhooks Respond (1hourData)\n\n\u2022 **Exit point** for external workflows calling indicator batches\n\n\u2022 Orchestrates trigger for **indicator API pull + formatter nodes**\n\n\u2022 Returns **cleaned batch** back via Respond to Webhook node\n\n\ud83d\udcce Notes:\n\n\u2022 Must pass through correct **webhook** path\n\n\u2022 Responds after **merge with all 6 formatted outputs** per timeframe"
},
"typeVersion": 1
},
{
"id": "f05d2def-9593-46c5-862d-378a5d1f64d4",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
480,
2660
],
"parameters": {
"color": 4,
"width": 280,
"height": 640,
"content": "## Webhooks Respond (1dayData)\n\n\u2022 **Exit point** for external workflows calling indicator batches\n\n\u2022 Orchestrates trigger for **indicator API pull + formatter nodes**\n\n\u2022 Returns **cleaned batch** back via Respond to Webhook node\n\n\ud83d\udcce Notes:\n\n\u2022 Must pass through correct **webhook** path\n\n\u2022 Responds after **merge with all 6 formatted outputs** per timeframe"
},
"typeVersion": 1
},
{
"id": "ceb5f8e0-e51f-4554-a752-a319033c14c4",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1160,
-300
],
"parameters": {
"color": 6,
"width": 260,
"height": 520,
"content": "## Binance API Calls 15m \n\n\u2022 Triggers **Binance API** for k-line data:\n\n\n\u2022 Each node pulls and returns **raw API data** for formatting"
},
"typeVersion": 1
},
{
"id": "8e1aeb88-3232-4ea7-85c3-49a75097341b",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2940,
1480
],
"parameters": {
"color": 6,
"width": 260,
"height": 500,
"content": "## Binance API Calls 1h \n\n\u2022 Triggers **Binance API** for k-line data:\n\n\n\u2022 Each node pulls and returns **raw API data** for formatting"
},
"typeVersion": 1
},
{
"id": "53996473-bdd7-44d3-9060-96a2f072077c",
"name": "Sticky Note11",
"type": "n8n-nodes-base.stickyNote",
"position": [
240,
1420
],
"parameters": {
"color": 6,
"width": 260,
"height": 500,
"content": "## Binance API Calls 4h \n\n\u2022 Triggers **Binance API** for k-line data:\n\n\n\u2022 Each node pulls and returns **raw API data** for formatting"
},
"typeVersion": 1
},
{
"id": "6b138f55-5f0e-4844-a570-47919c8888b5",
"name": "Sticky Note12",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1340,
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow acts as a central API gateway for all technical indicator agents in the Binance Spot Market Quant AI system. It listens for incoming webhook requests and dynamically routes them to the correct timeframe-based indicator tool (15m, 1h, 4h, 1d). Designed to power…
Source: https://n8n.io/workflows/4747/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This n8n template provides enterprise-level version control for your workflows using GitHub integration. Stop losing hours to broken workflows and manual exports – get proper commit history, visual di
This flow creates dummy files for every item added in your *Arrs (Radarr/Sonarr) with the tag .
Sign PDF documents with legally-compliant digital signatures using X.509 certificates. Supports multiple PAdES signature levels (B, T, LT, LTA) with optional visible stamps.
📡 This workflow serves as the central Alpha Vantage API fetcher for Tesla trading indicators, delivering cleaned 20-point JSON outputs for three timeframes: , , and . It is required by the following a
This workflow offers several additional features for time tracking with Awork: Check whether time has been tracked when closing a task. If not, the task is reopened and the user is notified. This can