Skip to content

Instantly share code, notes, and snippets.

@vitqst
Created October 22, 2025 17:07
Show Gist options
  • Save vitqst/876e04fdde0562324f5fe98e6e28de2c to your computer and use it in GitHub Desktop.
Save vitqst/876e04fdde0562324f5fe98e6e28de2c to your computer and use it in GitHub Desktop.
{
"name": "Layer 2 - RSI MACD Momentum",
"nodes": [
{
"parameters": {},
"id": "execute-workflow-trigger",
"name": "When called by Layer 1",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1,
"position": [250, 300]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "layer1-data",
"name": "layer1_data",
"value": "={{ $json }}",
"type": "object"
},
{
"id": "symbol",
"name": "symbol",
"value": "={{ $json.symbol }}",
"type": "string"
},
{
"id": "signal-from-layer1",
"name": "signal_direction",
"value": "={{ $json.signal }}",
"type": "string"
}
]
}
},
"id": "capture-layer1-data",
"name": "Capture Layer 1 Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [470, 300]
},
{
"parameters": {
"method": "GET",
"url": "https://api.binance.com/api/v3/klines",
"authentication": "none",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "symbol",
"value": "={{ $json.symbol }}"
},
{
"name": "interval",
"value": "15m"
},
{
"name": "limit",
"value": "50"
}
]
},
"options": {}
},
"id": "binance-15m-klines",
"name": "Binance 15m Klines",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [690, 300]
},
{
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Get Layer 1 data\nconst layer1Data = $('Capture Layer 1 Data').first().json;\nconst symbol = layer1Data.symbol;\nconst signalDirection = layer1Data.signal_direction;\n\n// Get 15m klines - collect all items\nconst allItems = $input.all();\nconst klines = allItems.map(item => item.json);\n\nconsole.log('Layer 2: Processing', klines.length, 'candles for', symbol);\nconsole.log('Signal from Layer 1:', signalDirection);\n\n// Parse candles\nconst candles = klines.map(k => ({\n openTime: parseInt(k[0]),\n open: parseFloat(k[1]),\n high: parseFloat(k[2]),\n low: parseFloat(k[3]),\n close: parseFloat(k[4]),\n volume: parseFloat(k[5]),\n closeTime: parseInt(k[6])\n}));\n\n// ===== RSI CALCULATION (14-period) =====\nfunction calculateRSI(prices, period = 14) {\n if (prices.length < period + 1) return [];\n \n const results = [];\n let gains = [];\n let losses = [];\n \n // Calculate initial average gain/loss\n for (let i = 1; i <= period; i++) {\n const change = prices[i] - prices[i - 1];\n gains.push(change > 0 ? change : 0);\n losses.push(change < 0 ? Math.abs(change) : 0);\n }\n \n let avgGain = gains.reduce((a, b) => a + b, 0) / period;\n let avgLoss = losses.reduce((a, b) => a + b, 0) / period;\n \n // Calculate first RSI\n let rs = avgGain / avgLoss;\n let rsi = 100 - (100 / (1 + rs));\n results.push({ index: period, value: rsi, avgGain, avgLoss });\n \n // Calculate subsequent RSI values using smoothed averages\n for (let i = period + 1; i < prices.length; i++) {\n const change = prices[i] - prices[i - 1];\n const gain = change > 0 ? change : 0;\n const loss = change < 0 ? Math.abs(change) : 0;\n \n avgGain = (avgGain * (period - 1) + gain) / period;\n avgLoss = (avgLoss * (period - 1) + loss) / period;\n \n rs = avgGain / avgLoss;\n rsi = 100 - (100 / (1 + rs));\n results.push({ index: i, value: rsi, avgGain, avgLoss });\n }\n \n return results;\n}\n\n// ===== EMA CALCULATION =====\nfunction calculateEMA(prices, period) {\n const k = 2 / (period + 1);\n const ema = [];\n \n // Start with SMA\n let sum = 0;\n for (let i = 0; i < period; i++) {\n sum += prices[i];\n }\n ema.push(sum / period);\n \n // Calculate EMA\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 CALCULATION (12, 26, 9) =====\nfunction calculateMACD(prices) {\n const ema12 = calculateEMA(prices, 12);\n const ema26 = calculateEMA(prices, 26);\n \n // MACD line = EMA12 - EMA26\n const macdLine = [];\n const offset = 26 - 12;\n for (let i = 0; i < ema26.length; i++) {\n macdLine.push(ema12[i + offset] - ema26[i]);\n }\n \n // Signal line = 9-period EMA of MACD line\n const signalLine = calculateEMA(macdLine, 9);\n \n // Histogram = MACD - Signal\n const histogram = [];\n const signalOffset = macdLine.length - signalLine.length;\n for (let i = 0; i < signalLine.length; i++) {\n histogram.push(macdLine[i + signalOffset] - signalLine[i]);\n }\n \n return {\n macdLine,\n signalLine,\n histogram,\n startIndex: 26 + 9 - 1 // First valid MACD with signal\n };\n}\n\n// Extract close prices and volumes\nconst closePrices = candles.map(c => c.close);\nconst volumes = candles.map(c => c.volume);\n\n// Calculate indicators\nconst rsiValues = calculateRSI(closePrices, 14);\nconst macdData = calculateMACD(closePrices);\n\nconsole.log('RSI values calculated:', rsiValues.length);\nconsole.log('MACD values calculated:', macdData.histogram.length);\n\n// Validate we have enough data\nif (rsiValues.length < 2 || macdData.histogram.length < 2) {\n return {\n json: {\n layer: 2,\n symbol: symbol,\n timeframe: '15m',\n error: 'NOT_ENOUGH_DATA',\n message: `Need more data. RSI: ${rsiValues.length}, MACD: ${macdData.histogram.length}`,\n layer1_signal: signalDirection,\n momentum_confirmed: false\n }\n };\n}\n\n// Get current values\nconst currentRSI = rsiValues[rsiValues.length - 1];\nconst previousRSI = rsiValues[rsiValues.length - 2];\nconst currentMACD = {\n macd: macdData.macdLine[macdData.macdLine.length - 1],\n signal: macdData.signalLine[macdData.signalLine.length - 1],\n histogram: macdData.histogram[macdData.histogram.length - 1]\n};\nconst previousMACD = {\n macd: macdData.macdLine[macdData.macdLine.length - 2],\n signal: macdData.signalLine[macdData.signalLine.length - 2],\n histogram: macdData.histogram[macdData.histogram.length - 2]\n};\n\nconsole.log('Current RSI:', currentRSI.value);\nconsole.log('Current MACD histogram:', currentMACD.histogram);\n\n// Calculate volume confirmation (15m volume > avg of last 10 periods)\nconst recentVolumes = volumes.slice(-10);\nconst avgVolume = recentVolumes.reduce((a, b) => a + b, 0) / 10;\nconst volumeConfirmed = candles[candles.length - 1].volume > avgVolume;\n\nconsole.log('Volume confirmed:', volumeConfirmed);\n\n// ===== MOMENTUM CONFIRMATION LOGIC =====\nlet momentumConfirmed = false;\nlet confirmationReason = [];\n\nif (signalDirection === 'LONG') {\n // For LONG: RSI oversold, MACD turning up\n const rsiOversold = currentRSI.value < 40; // Relaxed from 30\n const rsiRising = currentRSI.value > previousRSI.value;\n const macdCrossUp = previousMACD.histogram < 0 && currentMACD.histogram > 0;\n const macdRising = currentMACD.histogram > previousMACD.histogram;\n const macdBullish = currentMACD.macd > currentMACD.signal || macdRising;\n \n if ((rsiOversold || rsiRising) && (macdBullish || macdCrossUp) && volumeConfirmed) {\n momentumConfirmed = true;\n confirmationReason.push('RSI favorable for LONG');\n if (macdCrossUp) confirmationReason.push('MACD bullish crossover');\n if (macdRising) confirmationReason.push('MACD histogram rising');\n }\n \n console.log('LONG checks - RSI:', currentRSI.value, 'MACD rising:', macdRising, 'Volume:', volumeConfirmed);\n}\nelse if (signalDirection === 'SHORT') {\n // For SHORT: RSI overbought, MACD turning down\n const rsiOverbought = currentRSI.value > 60; // Relaxed from 70\n const rsiFalling = currentRSI.value < previousRSI.value;\n const macdCrossDown = previousMACD.histogram > 0 && currentMACD.histogram < 0;\n const macdFalling = currentMACD.histogram < previousMACD.histogram;\n const macdBearish = currentMACD.macd < currentMACD.signal || macdFalling;\n \n if ((rsiOverbought || rsiFalling) && (macdBearish || macdCrossDown) && volumeConfirmed) {\n momentumConfirmed = true;\n confirmationReason.push('RSI favorable for SHORT');\n if (macdCrossDown) confirmationReason.push('MACD bearish crossover');\n if (macdFalling) confirmationReason.push('MACD histogram falling');\n }\n \n console.log('SHORT checks - RSI:', currentRSI.value, 'MACD falling:', macdFalling, 'Volume:', volumeConfirmed);\n}\n\n// ===== DIVERGENCE DETECTION =====\nfunction detectDivergence(prices, indicator, lookback = 5) {\n if (prices.length < lookback || indicator.length < lookback) return null;\n \n const recentPrices = prices.slice(-lookback);\n const recentIndicator = indicator.slice(-lookback);\n \n const priceHigh = Math.max(...recentPrices);\n const priceLow = Math.min(...recentPrices);\n const priceHighIdx = recentPrices.indexOf(priceHigh);\n const priceLowIdx = recentPrices.indexOf(priceLow);\n \n const indHigh = Math.max(...recentIndicator);\n const indLow = Math.min(...recentIndicator);\n const indHighIdx = recentIndicator.indexOf(indHigh);\n const indLowIdx = recentIndicator.indexOf(indLow);\n \n // Bullish divergence: price makes lower low, indicator makes higher low\n if (priceLowIdx > indLowIdx && recentPrices[recentPrices.length - 1] > priceLow) {\n return 'bullish';\n }\n \n // Bearish divergence: price makes higher high, indicator makes lower high\n if (priceHighIdx > indHighIdx && recentPrices[recentPrices.length - 1] < priceHigh) {\n return 'bearish';\n }\n \n return null;\n}\n\nconst rsiDivergence = detectDivergence(\n closePrices.slice(-15),\n rsiValues.slice(-15).map(r => r.value),\n 5\n);\n\nif (rsiDivergence) {\n console.log('RSI Divergence detected:', rsiDivergence);\n confirmationReason.push(`RSI ${rsiDivergence} divergence`);\n}\n\nconsole.log('Momentum confirmed:', momentumConfirmed);\nconsole.log('Reasons:', confirmationReason);\n\n// Return result\nreturn {\n json: {\n layer: 2,\n symbol: symbol,\n timeframe: '15m',\n timestamp: candles[candles.length - 1].closeTime,\n layer1_signal: signalDirection,\n layer1_data: layer1Data.layer1_data,\n momentum_confirmed: momentumConfirmed,\n confirmation_reasons: confirmationReason,\n indicators: {\n rsi: {\n current: currentRSI.value.toFixed(2),\n previous: previousRSI.value.toFixed(2),\n oversold: currentRSI.value < 30,\n overbought: currentRSI.value > 70\n },\n macd: {\n macdLine: currentMACD.macd.toFixed(4),\n signalLine: currentMACD.signal.toFixed(4),\n histogram: currentMACD.histogram.toFixed(4),\n previousHistogram: previousMACD.histogram.toFixed(4),\n crossover: (previousMACD.histogram < 0 && currentMACD.histogram > 0) ? 'bullish' :\n (previousMACD.histogram > 0 && currentMACD.histogram < 0) ? 'bearish' : 'none'\n },\n divergence: rsiDivergence\n },\n volume: {\n current: candles[candles.length - 1].volume,\n average: avgVolume,\n confirmed: volumeConfirmed\n },\n price: {\n current: candles[candles.length - 1].close\n },\n ready_for_layer3: momentumConfirmed && volumeConfirmed\n }\n};"
},
"id": "calculate-rsi-macd",
"name": "Calculate RSI MACD Volume",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [910, 300]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "momentum-confirmed",
"leftValue": "={{ $json.momentum_confirmed }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
},
{
"id": "volume-confirmed",
"leftValue": "={{ $json.volume.confirmed }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "check-momentum",
"name": "Check Momentum Confirmed",
"type": "n8n-nodes-base.if",
"typeVersion": 2.1,
"position": [1130, 300]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "status",
"name": "layer2_status",
"value": "MOMENTUM_CONFIRMED",
"type": "string"
},
{
"id": "proceed",
"name": "proceed_to_layer3",
"value": true,
"type": "boolean"
},
{
"id": "message",
"name": "alert_message",
"value": "=Layer 2: Momentum CONFIRMED for {{ $json.layer1_signal }} on {{ $json.symbol }}. RSI: {{ $json.indicators.rsi.current }}, MACD: {{ $json.indicators.macd.crossover }}",
"type": "string"
}
]
}
},
"id": "momentum-confirmed",
"name": "Momentum Confirmed ✅",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [1350, 200]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "status",
"name": "layer2_status",
"value": "MOMENTUM_REJECTED",
"type": "string"
},
{
"id": "no-proceed",
"name": "proceed_to_layer3",
"value": false,
"type": "boolean"
},
{
"id": "message",
"name": "alert_message",
"value": "=Layer 2: Momentum REJECTED for {{ $json.layer1_signal }} on {{ $json.symbol }}. RSI: {{ $json.indicators.rsi.current }}",
"type": "string"
}
]
}
},
"id": "momentum-rejected",
"name": "Momentum Rejected ❌",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [1350, 400]
}
],
"connections": {
"When called by Layer 1": {
"main": [
[
{
"node": "Capture Layer 1 Data",
"type": "main",
"index": 0
}
]
]
},
"Capture Layer 1 Data": {
"main": [
[
{
"node": "Binance 15m Klines",
"type": "main",
"index": 0
}
]
]
},
"Binance 15m Klines": {
"main": [
[
{
"node": "Calculate RSI MACD Volume",
"type": "main",
"index": 0
}
]
]
},
"Calculate RSI MACD Volume": {
"main": [
[
{
"node": "Check Momentum Confirmed",
"type": "main",
"index": 0
}
]
]
},
"Check Momentum Confirmed": {
"main": [
[
{
"node": "Momentum Confirmed ✅",
"type": "main",
"index": 0
}
],
[
{
"node": "Momentum Rejected ❌",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment