8
n8n 한국어amn8n.com

비트코인 기술 지표 Webhook 도구

고급

이것은Finance, AI분야의자동화 워크플로우로, 69개의 노드를 포함합니다.주로 Code, Merge, Webhook, HttpRequest, RespondToWebhook 등의 노드를 사용하며인공지능 기술을 결합하여 스마트 자동화를 구현합니다. 币安技术指标 Webhook 工具

사전 요구사항
  • HTTP Webhook 엔드포인트(n8n이 자동으로 생성)
  • 대상 API의 인증 정보가 필요할 수 있음

카테고리

워크플로우 미리보기
노드 연결 관계를 시각적으로 표시하며, 확대/축소 및 이동을 지원합니다
워크플로우 내보내기
다음 JSON 구성을 복사하여 n8n에 가져오면 이 워크플로우를 사용할 수 있습니다
{
  "id": "HrTD222kWpvKsy2j",
  "meta": {
    "instanceId": "a5283507e1917a33cc3ae615b2e7d5ad2c1e50955e6f831272ddd5ab816f3fb6"
  },
  "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 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": "ADX 계산",
      "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 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 Request 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 Request 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": "## Technical Indicator API Merge \n\n**Binance API** for 6 indicators:\n\n• **RSI, MACD, BBANDS, SMA, EMA, ADX**"
      },
      "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": "## Technical Indicator API Merge \n\n**Binance API** for 6 indicators:\n\n• **RSI, MACD, BBANDS, SMA, EMA, ADX**"
      },
      "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": "## Technical Indicator API Merge \n\n**Binance API** for 6 indicators:\n\n• **RSI, MACD, BBANDS, SMA, EMA, ADX**"
      },
      "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": "## Technical Indicator API Merge \n\n**Binance API** for 6 indicators:\n\n• **RSI, MACD, BBANDS, SMA, EMA, ADX**"
      },
      "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 (15minData)\n\n• **Entry point** for external workflows calling indicator batches\n\n• Orchestrates trigger for **indicator API pull + formatter nodes**\n\n• Returns **cleaned batch** back via Respond to Webhook node\n\n📎 Notes:\n\n• Must pass through correct **webhook** path\n\n• Responds after **merge with all 6 formatted outputs** per timeframe"
      },
      "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 (1hourData)\n\n• **Entry point** for external workflows calling indicator batches\n\n• Orchestrates trigger for **indicator API pull + formatter nodes**\n\n• Returns **cleaned batch** back via Respond to Webhook node\n\n📎 Notes:\n\n• Must pass through correct **webhook** path\n\n• Responds after **merge with all 6 formatted outputs** per timeframe"
      },
      "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 (4hourData)\n\n• **Entry point** for external workflows calling indicator batches\n\n• Orchestrates trigger for **indicator API pull + formatter nodes**\n\n• Returns **cleaned batch** back via Respond to Webhook node\n\n📎 Notes:\n\n• Must pass through correct **webhook** path\n\n• Responds after **merge with all 6 formatted outputs** per timeframe"
      },
      "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 (1dayData)\n\n• **Entry point** for external workflows calling indicator batches\n\n• Orchestrates trigger for **indicator API pull + formatter nodes**\n\n• Returns **cleaned batch** back via Respond to Webhook node\n\n📎 Notes:\n\n• Must pass through correct **webhook** path\n\n• Responds after **merge with all 6 formatted outputs** per timeframe"
      },
      "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 Respond (15minData)\n\n• **Exit point** for external workflows calling indicator batches\n\n• Orchestrates trigger for **indicator API pull + formatter nodes**\n\n• Returns **cleaned batch** back via Respond to Webhook node\n\n📎 Notes:\n\n• Must pass through correct **webhook** path\n\n• Responds after **merge with all 6 formatted outputs** per timeframe"
      },
      "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 Respond (4hourData)\n\n• **Exit point** for external workflows calling indicator batches\n\n• Orchestrates trigger for **indicator API pull + formatter nodes**\n\n• Returns **cleaned batch** back via Respond to Webhook node\n\n📎 Notes:\n\n• Must pass through correct **webhook** path\n\n• Responds after **merge with all 6 formatted outputs** per timeframe"
      },
      "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 Respond (1hourData)\n\n• **Exit point** for external workflows calling indicator batches\n\n• Orchestrates trigger for **indicator API pull + formatter nodes**\n\n• Returns **cleaned batch** back via Respond to Webhook node\n\n📎 Notes:\n\n• Must pass through correct **webhook** path\n\n• Responds after **merge with all 6 formatted outputs** per timeframe"
      },
      "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 Respond (1dayData)\n\n• **Exit point** for external workflows calling indicator batches\n\n• Orchestrates trigger for **indicator API pull + formatter nodes**\n\n• Returns **cleaned batch** back via Respond to Webhook node\n\n📎 Notes:\n\n• Must pass through correct **webhook** path\n\n• Responds after **merge with all 6 formatted outputs** per timeframe"
      },
      "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": "## Binance API Calls 15m \n\n• Triggers **Binance API** for k-line data:\n\n\n• Each node pulls and returns **raw API data** for formatting"
      },
      "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": "## Binance API Calls 1h \n\n• Triggers **Binance API** for k-line data:\n\n\n• Each node pulls and returns **raw API data** for formatting"
      },
      "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": "## Binance API Calls 4h \n\n• Triggers **Binance API** for k-line data:\n\n\n• Each node pulls and returns **raw API data** for formatting"
      },
      "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": "## Binance API Calls 1d \n\n• Triggers **Binance API** for k-line data:\n\n\n• Each node pulls and returns **raw API data** for formatting"
      },
      "typeVersion": 1
    },
    {
      "id": "8d8bd17f-c437-4e57-a517-3b6eb44eeff7",
      "name": "스티키 노트16",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2120,
        720
      ],
      "parameters": {
        "color": 2,
        "height": 1860,
        "content": "## Calculate Technical Indicators \n  \n\n• Ensures consistent **downstream format for merge and reasoning**\n\n📎 Notes:\n\n• **Format logic** is customized per indicator\n\n• Essential for **standardizing multi-timeframe data ingestion**"
      },
      "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": "## Calculate Technical Indicators \n  \n\n• Ensures consistent **downstream format for merge and reasoning**\n\n📎 Notes:\n\n• **Format logic** is customized per indicator\n\n• Essential for **standardizing multi-timeframe data ingestion**"
      },
      "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": "## Calculate Technical Indicators \n  \n\n• Ensures consistent **downstream format for merge and reasoning**\n\n📎 Notes:\n\n• **Format logic** is customized per indicator\n\n• Essential for **standardizing multi-timeframe data ingestion**"
      },
      "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": "## Calculate Technical Indicators \n  \n\n• Ensures consistent **downstream format for merge and reasoning**\n\n📎 Notes:\n\n• **Format logic** is customized per indicator\n\n• Essential for **standardizing multi-timeframe data ingestion**"
      },
      "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 Merge \n\n**Binance API** for k-line Data to calculate:\n\n• **RSI, MACD, BBANDS, SMA, EMA, ADX**"
      },
      "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 Merge \n\n**Binance API** for k-line Data to calculate:\n\n• **RSI, MACD, BBANDS, SMA, EMA, ADX**"
      },
      "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 Merge \n\n**Binance API** for k-line Data to calculate:\n\n• **RSI, MACD, BBANDS, SMA, EMA, ADX**"
      },
      "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 Merge \n\n**Binance API** for k-line Data to calculate:\n\n• **RSI, MACD, BBANDS, SMA, EMA, ADX**"
      },
      "typeVersion": 1
    },
    {
      "id": "773a2b4f-f716-41ab-ba18-1412e2dd50e5",
      "name": "스티키 노트24",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2800,
        -60
      ],
      "parameters": {
        "width": 1460,
        "height": 3020,
        "content": "# 🧪 Binance SM Indicators Webhook Tool – Documentation\n\nA backend webhook processor that performs **real-time indicator calculations** for 15m, 1h, 4h, and 1d Binance candlestick data. This workflow powers all the indicator sub-agents and is essential for maintaining modular and scalable logic across timeframes.\n\n---\n\n## 🎯 Purpose\n\n* Accepts HTTP POST requests from the 15m, 1h, 4h, or 1d sub-agents\n* Pulls raw OHLCV kline data for the symbol and interval\n* Calculates technical indicators:\n\n  * RSI, MACD, Bollinger Bands, SMA, EMA, ADX\n* Returns structured JSON with labeled values (e.g., `\"MACD_Cross\": \"Bullish\"`)\n\nThis tool ensures **centralized logic reuse** across all timeframe agents while improving speed and maintainability.\n\n---\n\n## ⚙️ Key Components\n\n| Node Name              | Description                                               |\n| ---------------------- | --------------------------------------------------------- |\n| `Webhook Trigger`      | Handles POST at `/15m-indicators`, `/1h-indicators`, etc. |\n| `Extract Symbol`       | Validates input payload (must include `symbol`)           |\n| `Fetch Binance Klines` | Gets latest 100 candles using `/api/v3/klines`            |\n| `RSI Calculator`       | Computes 14-period RSI from closing prices                |\n| `MACD Calculator`      | Computes 12, 26, 9 MACD with signal and histogram         |\n| `BBANDS Calculator`    | Calculates 20-period bands using std. deviation           |\n| `SMA/EMA Node`         | Computes 20-period simple and exponential MAs             |\n| `ADX Calculator`       | Computes 14-period trend strength (DI+/DI− included)      |\n| `Prepare Output`       | Returns JSON with all labeled indicators                  |\n\n---\n\n## 📥 Expected Input (from sub-agent)\n\n```json\n{\n  \"symbol\": \"BTCUSDT\"\n}\n```\n\nMust be a valid Binance Spot trading pair. Interval is auto-routed based on webhook endpoint:\n\n| Endpoint Path     | Interval |\n| ----------------- | -------- |\n| `/15m-indicators` | 15m      |\n| `/1h-indicators`  | 1h       |\n| `/4h-indicators`  | 4h       |\n| `/1d-indicators`  | 1d       |\n\n---\n\n## 📊 Sample Output JSON\n\n```json\n{\n  \"symbol\": \"BTCUSDT\",\n  \"interval\": \"4h\",\n  \"rsi\": 65.1,\n  \"macd\": {\n    \"value\": 24.3,\n    \"signal\": 21.7,\n    \"histogram\": 2.6,\n    \"cross\": \"Bullish\"\n  },\n  \"bb\": {\n    \"basis\": 62600,\n    \"upper\": 63500,\n    \"lower\": 61700,\n    \"status\": \"Expanding\"\n  },\n  \"sma\": 62000,\n  \"ema\": 62400,\n  \"adx\": {\n    \"value\": 32.8,\n    \"strength\": \"Strong\"\n  }\n}\n```\n\n---\n\n## 🧩 Use Case Scenarios\n\n| Scenario                                   | Result                                                   |\n| ------------------------------------------ | -------------------------------------------------------- |\n| 15m tool POSTs to `/15m-indicators`        | Webhook responds with 15m indicator set                  |\n| Financial Analyst Tool requests 1h signals | This workflow handles the actual math for each indicator |\n| 1d tool needs RSI + MACD + ADX             | Centralized logic avoids duplication across agents       |\n\n---\n\n## 🛠️ Installation Instructions\n\n### 1. Import & Activate\n\n* Import JSON into n8n\n* Ensure webhook is live and accessible\n* Test each endpoint (e.g., `/1h-indicators`) via POST\n\n### 2. Connect to Binance API\n\n* Public API: no credentials required\n* Ensure the HTTP request node for klines uses:\n\n  ```\n  https://api.binance.com/api/v3/klines\n  ```\n\n### 3. Link to Sub-Agent Workflows\n\n* 15m, 1h, 4h, 1d tools all depend on this for logic\n* Must POST `symbol` to the correct interval endpoint\n\n---\n\n## 📦 Example Workflow Integration\n\nAll the following tools rely on this:\n\n* `Binance SM 15min Indicators Tool`\n* `Binance SM 1hour Indicators Tool`\n* `Binance SM 4hour Indicators Tool`\n* `Binance SM 1day Indicators Tool`\n\nThis acts as the math engine behind each.\n\n---\n\n## 🔐 Licensing & Support\n\n🔗 **Don Jayamaha – LinkedIn**\n[http://linkedin.com/in/donjayamahajr](http://linkedin.com/in/donjayamahajr)\n\n© 2025 Treasurium Capital Limited Company. All rights reserved.\nThe design, architecture, and indicator calculation code within this system are proprietary. Reuse, resale, or modification without license is strictly prohibited.\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "f7032997-bb7f-4e2a-9982-7ee18c95cfbe",
  "connections": {
    "61cb3bd7-60ba-4dcf-a080-146b7612a99b": {
      "main": [
        [
          {
            "node": "46b937d5-7338-457f-b2f0-eb85ac44a8bb",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7301ba32-ab0c-4c80-b041-cd398f7daa2c": {
      "main": [
        [
          {
            "node": "af471a36-1107-4ccd-be14-efa06ba27330",
            "type": "main",
            "index": 5
          }
        ]
      ]
    },
    "25ef2964-c9b1-4b9e-b6ce-3766b215d163": {
      "main": [
        [
          {
            "node": "5ea4756f-a20a-444c-8471-57d209457823",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "b0391e28-03a3-40f0-8182-72df0928ca2c": {
      "main": [
        [
          {
            "node": "5ea4756f-a20a-444c-8471-57d209457823",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "c50f7fa5-2707-47a4-9b32-1d582bf5d600": {
      "main": [
        [
          {
            "node": "5ea4756f-a20a-444c-8471-57d209457823",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "2c598960-630c-4687-91c0-bfae10da1943": {
      "main": [
        [
          {
            "node": "5ea4756f-a20a-444c-8471-57d209457823",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "29121e2e-f6e2-4d42-8e5b-e8eea1e61ec8": {
      "main": [
        [
          {
            "node": "1be26316-3d08-4983-9d12-6bc36465e76e",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9ec777b1-eb23-43b0-8bb3-726adcc3418f": {
      "main": [
        [
          {
            "node": "d7d9bb31-6986-42e6-9f0c-7d7f00d66ee1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "bbb81a2a-b1c4-4c1e-80e9-a4990c45fae8": {
      "main": [
        [
          {
            "node": "20e58706-035f-46ef-b235-78fe468875bc",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "0b3278a6-5793-4d70-8954-70562c43a3eb": {
      "main": [
        [
          {
            "node": "660dfcb4-ed63-4c53-806b-cba96c105fee",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "83fa487a-1a67-4059-9007-d5298d8bb105": {
      "main": [
        [
          {
            "node": "af471a36-1107-4ccd-be14-efa06ba27330",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "17bf4e9f-7053-4a04-b439-3e23fe5cb7f9": {
      "main": [
        [
          {
            "node": "7940bc3f-c5f4-4b4b-b542-8737f17afbbf",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "ac84877a-75cf-4ef2-bad1-8d0f444ecead": {
      "main": [
        [
          {
            "node": "660dfcb4-ed63-4c53-806b-cba96c105fee",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "702e8bc8-d8f0-4fb4-b44f-fd19479c8b03": {
      "main": [
        [
          {
            "node": "af471a36-1107-4ccd-be14-efa06ba27330",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "d1c2fcc7-65ac-446c-8f59-c4cd8ac7d795": {
      "main": [
        [
          {
            "node": "7940bc3f-c5f4-4b4b-b542-8737f17afbbf",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "5b1f34b3-e675-46b1-a754-679080680058": {
      "main": [
        [
          {
            "node": "660dfcb4-ed63-4c53-806b-cba96c105fee",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "15bf5c3b-de07-412b-988c-f5c59fa7a06f": {
      "main": [
        [
          {
            "node": "af471a36-1107-4ccd-be14-efa06ba27330",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "93a115f7-e876-43f0-8ecb-70a47a06f41c": {
      "main": [
        [
          {
            "node": "7940bc3f-c5f4-4b4b-b542-8737f17afbbf",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "28dd6fb1-a161-49ed-9c14-740e88e2195d": {
      "main": [
        [
          {
            "node": "660dfcb4-ed63-4c53-806b-cba96c105fee",
            "type": "main",
            "index": 5
          }
        ]
      ]
    },
    "d61db420-9def-4ec1-bf8a-94afeabda186": {
      "main": [
        [
          {
            "node": "7940bc3f-c5f4-4b4b-b542-8737f17afbbf",
            "type": "main",
            "index": 5
          }
        ]
      ]
    },
    "a5da65c5-db5a-4f9d-a6cf-228f081ffb3c": {
      "main": [
        [
          {
            "node": "660dfcb4-ed63-4c53-806b-cba96c105fee",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "d16b7829-59ca-45ca-a5ae-4e315168c96d": {
      "main": [
        [
          {
            "node": "af471a36-1107-4ccd-be14-efa06ba27330",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "df32e55c-2dbc-44d6-8161-f70db651cfcd": {
      "main": [
        [
          {
            "node": "7940bc3f-c5f4-4b4b-b542-8737f17afbbf",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "46b937d5-7338-457f-b2f0-eb85ac44a8bb": {
      "main": [
        [
          {
            "node": "5b8c1049-bcf4-41d4-bba3-5f76e940bd3e",
            "type": "main",
            "index": 0
          },
          {
            "node": "b0391e28-03a3-40f0-8182-72df0928ca2c",
            "type": "main",
            "index": 0
          },
          {
            "node": "2c598960-630c-4687-91c0-bfae10da1943",
            "type": "main",
            "index": 0
          },
          {
            "node": "c50f7fa5-2707-47a4-9b32-1d582bf5d600",
            "type": "main",
            "index": 0
          },
          {
            "node": "25ef2964-c9b1-4b9e-b6ce-3766b215d163",
            "type": "main",
            "index": 0
          },
          {
            "node": "7301ba32-ab0c-4c80-b041-cd398f7daa2c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "660dfcb4-ed63-4c53-806b-cba96c105fee": {
      "main": [
        [
          {
            "node": "939665ce-7a50-4a6d-9b37-62ad800350b0",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "af471a36-1107-4ccd-be14-efa06ba27330": {
      "main": [
        [
          {
            "node": "39e6b058-d800-4751-b75b-5e62be379c61",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7940bc3f-c5f4-4b4b-b542-8737f17afbbf": {
      "main": [
        [
          {
            "node": "8585a269-b70d-4af7-b264-db9f638c8a77",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1be26316-3d08-4983-9d12-6bc36465e76e": {
      "main": [
        [
          {
            "node": "6e9c7976-737b-4377-b412-633a1a67097b",
            "type": "main",
            "index": 0
          },
          {
            "node": "ac84877a-75cf-4ef2-bad1-8d0f444ecead",
            "type": "main",
            "index": 0
          },
          {
            "node": "a5da65c5-db5a-4f9d-a6cf-228f081ffb3c",
            "type": "main",
            "index": 0
          },
          {
            "node": "5b1f34b3-e675-46b1-a754-679080680058",
            "type": "main",
            "index": 0
          },
          {
            "node": "0b3278a6-5793-4d70-8954-70562c43a3eb",
            "type": "main",
            "index": 0
          },
          {
            "node": "28dd6fb1-a161-49ed-9c14-740e88e2195d",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "d7d9bb31-6986-42e6-9f0c-7d7f00d66ee1": {
      "main": [
        [
          {
            "node": "a2e9d933-47d7-47f5-8d49-224860d8600a",
            "type": "main",
            "index": 0
          },
          {
            "node": "702e8bc8-d8f0-4fb4-b44f-fd19479c8b03",
            "type": "main",
            "index": 0
          },
          {
            "node": "d16b7829-59ca-45ca-a5ae-4e315168c96d",
            "type": "main",
            "index": 0
          },
          {
            "node": "15bf5c3b-de07-412b-988c-f5c59fa7a06f",
            "type": "main",
            "index": 0
          },
          {
            "node": "83fa487a-1a67-4059-9007-d5298d8bb105",
            "type": "main",
            "index": 0
          },
          {
            "node": "7301ba32-ab0c-4c80-b041-cd398f7daa2c",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "20e58706-035f-46ef-b235-78fe468875bc": {
      "main": [
        [
          {
            "node": "1675168b-97ab-4be4-92d2-734d075e2222",
            "type": "main",
            "index": 0
          },
          {
            "node": "d1c2fcc7-65ac-446c-8f59-c4cd8ac7d795",
            "type": "main",
            "index": 0
          },
          {
            "node": "df32e55c-2dbc-44d6-8161-f70db651cfcd",
            "type": "main",
            "index": 0
          },
          {
            "node": "93a115f7-e876-43f0-8ecb-70a47a06f41c",
            "type": "main",
            "index": 0
          },
          {
            "node": "17bf4e9f-7053-4a04-b439-3e23fe5cb7f9",
            "type": "main",
            "index": 0
          },
          {
            "node": "d61db420-9def-4ec1-bf8a-94afeabda186",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "91a05310-bd36-4ee0-8381-b3d16c3346be": {
      "main": [
        [
          {
            "node": "29121e2e-f6e2-4d42-8e5b-e8eea1e61ec8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "40fcbba9-489b-4452-baf8-9a41ef0f9b07": {
      "main": [
        [
          {
            "node": "9ec777b1-eb23-43b0-8bb3-726adcc3418f",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "c4a718f8-f2d6-464b-8120-1e4067d760ad": {
      "main": [
        [
          {
            "node": "bbb81a2a-b1c4-4c1e-80e9-a4990c45fae8",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "b372f57e-917a-42d2-964c-25a296ea982d": {
      "main": [
        [
          {
            "node": "61cb3bd7-60ba-4dcf-a080-146b7612a99b",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5ea4756f-a20a-444c-8471-57d209457823": {
      "main": [
        [
          {
            "node": "0a46ea7c-a85d-4fc6-b20c-7e65ab962ea6",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5b8c1049-bcf4-41d4-bba3-5f76e940bd3e": {
      "main": [
        [
          {
            "node": "5ea4756f-a20a-444c-8471-57d209457823",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6e9c7976-737b-4377-b412-633a1a67097b": {
      "main": [
        [
          {
            "node": "660dfcb4-ed63-4c53-806b-cba96c105fee",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "a2e9d933-47d7-47f5-8d49-224860d8600a": {
      "main": [
        [
          {
            "node": "af471a36-1107-4ccd-be14-efa06ba27330",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "1675168b-97ab-4be4-92d2-734d075e2222": {
      "main": [
        [
          {
            "node": "7940bc3f-c5f4-4b4b-b542-8737f17afbbf",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
자주 묻는 질문

이 워크플로우를 어떻게 사용하나요?

위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.

이 워크플로우는 어떤 시나리오에 적합한가요?

고급 - 금융, 인공지능

유료인가요?

이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.

워크플로우 정보
난이도
고급
노드 수69
카테고리2
노드 유형6
난이도 설명

고급 사용자를 위한 16+개 노드의 복잡한 워크플로우

저자
Don Jayamaha Jr

Don Jayamaha Jr

@don-the-gem-dealer

With 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에서 보기

이 워크플로우 공유

카테고리

카테고리: 34