{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Detect Binance volume spikes with\u00a0GPT-4o analysis and Telegram alerts",
  "tags": [],
  "nodes": [
    {
      "id": "c4da2e39-13b6-46af-88eb-06e66f33468a",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        192,
        272
      ],
      "parameters": {
        "width": 592,
        "height": 496,
        "content": "## Binance Volume Spike Alert with AI Analysis\n\n### How it works\n\n1. The workflow is triggered on a schedule to retrieve all ticker data from Binance every 24 hours.\n2. It filters the tickers to focus on USDT pairs and calculates volume spikes for these pairs.\n3. Detected spikes trigger further analysis with AI to interpret order book pressure.\n4. The AI-generated analysis is parsed and collected to track all significant volume spikes.\n5. Top significant spikes are selected, formatted, and sent as alerts via Telegram.\n6. All data is flattened and logged to a Google Sheets document for record-keeping.\n\n### Setup steps\n\n- [ ] Set up Binance API credentials for ticker, klines, and order book requests.\n- [ ] Configure OpenAI credentials for AI analysis.\n- [ ] Connect Telegram with proper tokens to enable message sending.\n- [ ] Set up Google Sheets integration for data logging.\n\n### Customization\n\nAdjust MIN_VOL and MIN_CHANGE in the \"Filter USDT Pairs\" node to control which coins enter the pipeline. Modify the system prompt in the Build AI Prompt node to align with your trading strategy."
      },
      "typeVersion": 1
    },
    {
      "id": "c00a4ee1-149f-48d5-96b0-1f740f148846",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        272
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 304,
        "content": "## Initial data retrieval\n\nTrigger workflow and retrieve Binance ticker data."
      },
      "typeVersion": 1
    },
    {
      "id": "fe16cb5e-1068-4d31-bcce-07e120985c83",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1216,
        272
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 304,
        "content": "## Filter and batch processing\n\nFilter USDT pairs, remove stable coins and loop through items for further processing."
      },
      "typeVersion": 1
    },
    {
      "id": "116b808f-1cd4-4302-a9f5-85eeade35f8f",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1616,
        400
      ],
      "parameters": {
        "color": 7,
        "width": 608,
        "height": 288,
        "content": "## Volume spike analysis\n\nCalculate volume spikes and determine if further analysis is needed."
      },
      "typeVersion": 1
    },
    {
      "id": "e89c8a14-421b-4286-b770-da1658e10f93",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2256,
        400
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 288,
        "content": "## Order book analysis\n\nFetch order book data, evaluate book pressure"
      },
      "typeVersion": 1
    },
    {
      "id": "3241ecda-e5b0-4f15-9d39-b27116919e23",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3168,
        528
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 272,
        "content": "## Results collection and iteration\n\nParse AI results and collect all detected spikes for further iteration."
      },
      "typeVersion": 1
    },
    {
      "id": "508f8c48-101b-47ae-9b50-6ccc95fb6eb9",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1616,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 608,
        "height": 432,
        "content": "## Notification and logging\n\nFormat results and send them via Telegram; log data to Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "2c8ea2fb-ca2c-44b2-8706-c17ad2c53c90",
      "name": "Fetch Binance All Tickers",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1040,
        400
      ],
      "parameters": {
        "url": "https://api.binance.com/api/v3/ticker/24hr",
        "options": {}
      },
      "typeVersion": 4.4
    },
    {
      "id": "2b8fbf90-3d55-4924-83e5-4500513ba898",
      "name": "Loop Over Coin Pairs",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1456,
        400
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "cef614c8-085d-46d4-932b-34d7213c11bf",
      "name": "Compute Volume Spike",
      "type": "n8n-nodes-base.code",
      "position": [
        1872,
        512
      ],
      "parameters": {
        "jsCode": "const ticker = $('Loop Over Coin Pairs').first().json;\n\nconst candles = $input.all().map(item => {\n  const c = item.json;\n  return {\n    openTime:      Number(c[0]),\n    open:          parseFloat(c[1]),\n    high:          parseFloat(c[2]),\n    low:           parseFloat(c[3]),\n    close:         parseFloat(c[4]),\n    volume:        parseFloat(c[5]),    // Base asset volume\n    quoteVol:      parseFloat(c[7]),    // Quote volume (USDT)\n    trades:        parseInt(c[8]),\n    takerBuyBase:  parseFloat(c[9]),    // Taker buy base volume\n    takerBuyQuote: parseFloat(c[10]),   // Taker buy quote volume (USDT)\n  };\n});\n\n// \u2500\u2500 Most recent closed candle (index -2) \u2500\u2500\n// Index -1 = currently forming candle (not yet closed) \u2192 skip\nconst lastClosed = candles[candles.length - 2];\n\n// 24 candles before the closed candle (index 0 \u2192 index -3)\nconst previous = candles.slice(0, -2);\n\n// \u2500\u2500 Volume Spike Ratio \u2500\u2500\n// Use trimmed mean (remove top/bottom 10% outliers before averaging)\nconst sortedVols = previous.map(c => c.quoteVol).sort((a, b) => a - b);\nconst trimCount = Math.round(sortedVols.length * 0.1);\nconst trimmedVols = sortedVols.slice(trimCount, sortedVols.length - trimCount);\nconst avgVol1h = trimmedVols.reduce((s, v) => s + v, 0) / trimmedVols.length;\n\nconst sortedTrades = previous.map(c => c.trades).sort((a, b) => a - b);\nconst trimCountT = Math.round(sortedTrades.length * 0.1);\nconst trimmedTrades = sortedTrades.slice(trimCountT, sortedTrades.length - trimCountT);\nconst avgTrades1h = trimmedTrades.reduce((s, v) => s + v, 0) / trimmedTrades.length;\n\nconst volRatio   = avgVol1h > 0\n  ? parseFloat((lastClosed.quoteVol / avgVol1h).toFixed(2)) : 0;\nconst tradeRatio = avgTrades1h > 0\n  ? parseFloat((lastClosed.trades / avgTrades1h).toFixed(2)) : 0;\n\n// \u2500\u2500 Taker Buy/Sell Ratio \u2500\u2500\n// takerBuyQuote = aggressive buy volume (market buys)\n// takerSellQuote = total volume - taker buy = aggressive sell volume\nconst takerBuyPct = lastClosed.quoteVol > 0\n  ? parseFloat((lastClosed.takerBuyQuote / lastClosed.quoteVol * 100).toFixed(1)) : 50;\nconst takerSellPct = parseFloat((100 - takerBuyPct).toFixed(1));\n\n// Classify pressure\nlet pressure = 'NEUTRAL';\nif (takerBuyPct >= 65) pressure = 'STRONG_BUY';\nelse if (takerBuyPct >= 55) pressure = 'BUY';\nelse if (takerBuyPct <= 35) pressure = 'STRONG_SELL';\nelse if (takerBuyPct <= 45) pressure = 'SELL';\n\n// \u2500\u2500 Price Action \u2500\u2500\nconst priceChange = parseFloat(\n  ((lastClosed.close - lastClosed.open) / lastClosed.open * 100).toFixed(2)\n);\nconst candleRange = parseFloat(\n  ((lastClosed.high - lastClosed.low) / lastClosed.low * 100).toFixed(2)\n);\nconst isBullish = lastClosed.close >= lastClosed.open;\n\n// Candle body vs wick ratio (large body = momentum, long wick = rejection)\nconst bodySize = Math.abs(lastClosed.close - lastClosed.open);\nconst totalRange = lastClosed.high - lastClosed.low;\nconst bodyRatio = totalRange > 0\n  ? parseFloat((bodySize / totalRange * 100).toFixed(0)) : 0;\n// bodyRatio > 60% = strong momentum candle\n// bodyRatio < 30% = doji/indecision\n\n// \u2500\u2500 Multi-candle Context \u2500\u2500\n// Check if the 3 candles before the spike have rising volume\nconst last3 = previous.slice(-3).map(c => c.quoteVol);\nconst volTrend =\n  last3.every((v, i) => i === 0 || v >= last3[i-1] * 0.9) ? 'RISING' :\n  last3.every((v, i) => i === 0 || v <= last3[i-1] * 1.1) ? 'FALLING' : 'MIXED';\n\n// Did the previous candle also spike? (sustained volume)\nconst prevCandle = candles[candles.length - 3];\nconst prevVolRatio = avgVol1h > 0\n  ? parseFloat((prevCandle.quoteVol / avgVol1h).toFixed(2)) : 0;\nconst sustainedSpike = prevVolRatio >= 1.5; // Previous candle also abnormally high\n\n// \u2500\u2500 Spike Classification \u2500\u2500\n// \u2500\u2500 with dedup \u2500\u2500\nlet spikeType = 'NONE';\nif (volRatio >= 5)        spikeType = 'EXTREME';\nelse if (volRatio >= 3.5) spikeType = 'VERY_HIGH';\nelse if (volRatio >= 2.5) spikeType = 'HIGH';\n\n// \u2500\u2500 Dedup Check \u2500\u2500\nconst COOLDOWN_HOURS = 4;\nconst staticData = $getWorkflowStaticData('global');\nif (!staticData.lastAlerted) staticData.lastAlerted = {};\n\nconst now = Date.now();\nconst lastTime = staticData.lastAlerted[ticker.symbol] || 0;\nconst hoursSinceLast = (now - lastTime) / (1000 * 60 * 60);\n\nif (spikeType !== 'NONE' && hoursSinceLast < COOLDOWN_HOURS) {\n  spikeType = 'NONE';\n}\n\nif (spikeType !== 'NONE') {\n  staticData.lastAlerted[ticker.symbol] = now;\n}\n\nreturn [{\n  json: {\n    // Ticker data\n    symbol:     ticker.symbol,\n    price:      ticker.price,\n    change24h:  ticker.change24h,\n    volume24h:  ticker.volume24h,\n\n    // Spike metrics\n    volRatio,\n    tradeRatio,\n    spikeType,\n    avgVol1h:      parseFloat(avgVol1h.toFixed(0)),\n    currentVol1h:  parseFloat(lastClosed.quoteVol.toFixed(0)),\n\n    // Taker buy/sell data\n    takerBuyPct,\n    takerSellPct,\n    pressure,\n\n    // Price action\n    priceChange,\n    candleRange,\n    isBullish,\n    bodyRatio,\n\n    // Multi-candle context\n    volTrend,\n    prevVolRatio,\n    sustainedSpike,\n\n    // Timestamp\n    detectedAt: new Date().toISOString(),\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7d361b05-b620-4370-a946-34dd30f1e1ac",
      "name": "Check for Volume Spike",
      "type": "n8n-nodes-base.if",
      "position": [
        2064,
        512
      ],
      "parameters": {
        "options": {
          "ignoreCase": true
        },
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "3a587d6f-2077-46db-a301-7953fc12a9e0",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.spikeType }}",
              "rightValue": "NONE"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "46addde0-e5a6-44a7-b98c-6459a04de3e6",
      "name": "Analyze Book Pressure",
      "type": "n8n-nodes-base.code",
      "position": [
        2528,
        496
      ],
      "parameters": {
        "jsCode": "const spike = $('Compute Volume Spike').first().json;\nconst book = $input.first().json;\n\n// Parse order book\nconst bids = book.bids.map(b => ({\n  price: parseFloat(b[0]),\n  qty:   parseFloat(b[1]),\n}));\nconst asks = book.asks.map(a => ({\n  price: parseFloat(a[0]),\n  qty:   parseFloat(a[1]),\n}));\n\n// Top 5 bid/ask volume (USDT)\nconst bidVol5 = bids.slice(0, 5).reduce((s, b) => s + b.price * b.qty, 0);\nconst askVol5 = asks.slice(0, 5).reduce((s, a) => s + a.price * a.qty, 0);\n\n// Top 20 bid/ask volume (USDT)\nconst bidVol20 = bids.reduce((s, b) => s + b.price * b.qty, 0);\nconst askVol20 = asks.reduce((s, a) => s + a.price * a.qty, 0);\n\n// Bid/Ask ratio\nconst bookRatio5  = askVol5 > 0\n  ? parseFloat((bidVol5 / askVol5).toFixed(2)) : 0;\nconst bookRatio20 = askVol20 > 0\n  ? parseFloat((bidVol20 / askVol20).toFixed(2)) : 0;\n\n// Spread\nconst bestBid = bids[0]?.price || 0;\nconst bestAsk = asks[0]?.price || 0;\nconst spreadPct = bestAsk > 0\n  ? parseFloat(((bestAsk - bestBid) / bestAsk * 100).toFixed(4)) : 0;\n\n// Book pressure classification\nlet bookPressure = 'BALANCED';\nif (bookRatio5 >= 2.0) bookPressure = 'STRONG_BID_WALL';\nelse if (bookRatio5 >= 1.3) bookPressure = 'BID_HEAVY';\nelse if (bookRatio5 <= 0.5) bookPressure = 'STRONG_ASK_WALL';\nelse if (bookRatio5 <= 0.77) bookPressure = 'ASK_HEAVY';\n\nreturn [{\n  json: {\n    // All spike data\n    ...spike,\n\n    // Order book data\n    bidVol5:      parseFloat(bidVol5.toFixed(0)),\n    askVol5:      parseFloat(askVol5.toFixed(0)),\n    bookRatio5,\n    bookRatio20,\n    spreadPct,\n    bookPressure,\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "274c16ea-665c-447d-935e-19544c375e2b",
      "name": "Fetch Order Book Depth",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2320,
        496
      ],
      "parameters": {
        "url": "https://api.binance.com/api/v3/depth",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "symbol",
              "value": "={{ $json.symbol }}"
            },
            {
              "name": "limit",
              "value": "20"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "1be2437d-65f6-47e8-9272-4db1b74b80b2",
      "name": "Prepare AI Analysis Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        2736,
        592
      ],
      "parameters": {
        "jsCode": "const d = $input.first().json;\n\nconst staticData = $getWorkflowStaticData('global');\nconst market = staticData.marketContext || { btcChange: 0, ethChange: 0 };\n\nconst systemPrompt = `You are a crypto market analyst specializing in volume analysis.\n\nTask: Analyze the volume spike data and determine what it likely means for the coin's short-term price action.\n\nAnalysis framework:\n1. Assess spike significance (how unusual is this volume?)\n2. Taker buy ratio: >60% = real buying pressure, <40% = selling pressure, 45-55% = inconclusive\n3. If taker buy is low + volume spike + price drop = likely panic selling or whale dumping\n4. If taker buy is high + volume spike + price up = likely breakout or accumulation\n5. Order book: treat as weak signal only \u2014 walls can be spoofed. Never let book pressure override taker buy/sell ratio or price action. Only mention book data as supporting context, not as primary evidence\n6. Sustained spike (previous candle also high volume) = stronger signal than single candle spike\n7. Body ratio: >60% = strong momentum, <30% = rejection/indecision despite high volume\n8. Give a short-term outlook (next 4-24 hours)\n\nIMPORTANT RULES:\n- High volume + low taker buy (<40%) + price dropping = BEARISH_DUMP, NOT accumulation\n- High volume + small price change + taker buy near 50% = possible wash trading, classify as UNCERTAIN\n- Sustained spike (prevVolRatio >= 1.5) increases confidence by +0.1\n- Ask wall (bookPressure = ASK_HEAVY or STRONG_ASK_WALL) with bullish spike = reduce confidence, add resistance warning\n- Spread > 0.5% = low liquidity warning\n- If BTC drops more than 3%, most altcoin spikes are market-wide panic, not coin-specific events. Lower confidence for BULLISH classifications.\n- If BTC and ETH both move in the same direction more than 2%, treat altcoin spikes as correlated market movement unless the altcoin's move is 3x stronger than BTC's.\n\nClassification:\n- BULLISH_BREAKOUT: High volume + price rising + taker buy >55% + no major ask wall\n- BEARISH_DUMP: High volume + price dropping + taker buy <45%\n- ACCUMULATION: High volume + small price change (<1%) + taker buy >55% + bid wall present\n- DISTRIBUTION: High volume + price near highs + taker buy <45% + ask wall present\n- UNCERTAIN: Conflicting signals or possible wash trading\n\nOutput ONLY valid JSON:\n{\n  \"classification\": \"BULLISH_BREAKOUT | BEARISH_DUMP | ACCUMULATION | DISTRIBUTION | UNCERTAIN\",\n  \"severity\": \"LOW | MEDIUM | HIGH | CRITICAL\",\n  \"summary\": \"<2-3 sentence explanation referencing taker ratio and book pressure>\",\n  \"action\": \"<what a trader should watch for next>\",\n  \"confidence\": <0.0-1.0>,\n  \"entryZone\": \"<price range to watch for entry, e.g. $0.45-$0.47, based on nearest support for longs or resistance for shorts>\",\n  \"invalidation\": \"<price level that invalidates this signal, e.g. below $0.42>\",\n  \"timeframe\": \"<expected timeframe: 1-4h or 4-24h>\"\n}`;\n\nconst userPrompt = `Analyze this volume spike:\n\nSYMBOL: ${d.symbol}\nCurrent Price: $${d.price}\n24h Change: ${d.change24h}%\n\nVOLUME SPIKE:\n\u251c Current 1h Volume: $${d.currentVol1h.toLocaleString()}\n\u251c Avg 1h Volume (24h): $${d.avgVol1h.toLocaleString()}\n\u251c Volume Ratio: ${d.volRatio}x (${d.spikeType})\n\u251c Trade Count Ratio: ${d.tradeRatio}x\n\u251c Volume Trend (last 3h): ${d.volTrend}\n\u251c Previous Candle Vol Ratio: ${d.prevVolRatio}x\n\u2514 Sustained Spike: ${d.sustainedSpike ? 'YES (prev candle also high)' : 'NO (single candle)'}\n\nTAKER BUY/SELL ANALYSIS:\n\u251c Taker Buy: ${d.takerBuyPct}%\n\u251c Taker Sell: ${d.takerSellPct}%\n\u2514 Pressure: ${d.pressure}\n\nPRICE ACTION:\n\u251c 1h Change (open\u2192close): ${d.priceChange}%\n\u251c 1h Range (low\u2192high): ${d.candleRange}%\n\u251c Direction: ${d.isBullish ? 'BULLISH (close > open)' : 'BEARISH (close < open)'}\n\u2514 Body Ratio: ${d.bodyRatio}% ${d.bodyRatio > 60 ? '(strong momentum)' : d.bodyRatio < 30 ? '(rejection/doji)' : '(moderate)'}\n\nORDER BOOK (top 5 levels):\n\u251c Bid Volume: $${d.bidVol5.toLocaleString()}\n\u251c Ask Volume: $${d.askVol5.toLocaleString()}\n\u251c Bid/Ask Ratio: ${d.bookRatio5}x\n\u251c Pressure: ${d.bookPressure}\n\u2514 Spread: ${d.spreadPct}%\n\nMARKET CONTEXT:\n\u251c BTC 24h: ${market.btcChange > 0 ? '+' : ''}${market.btcChange}%\n\u2514 ETH 24h: ${market.ethChange > 0 ? '+' : ''}${market.ethChange}%\n\nReturn ONLY valid JSON.`;\n\nreturn [{\n  json: {\n    ...d,\n    systemPrompt,\n    userPrompt,\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "dddf126d-eab1-47b2-84a2-d6aeeaa0c29a",
      "name": "OpenAI Volume Spike Analysis",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2896,
        592
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "GPT-4O"
        },
        "options": {
          "maxTokens": 300,
          "temperature": 0.2
        },
        "responses": {
          "values": [
            {
              "content": "={{ $json.userPrompt }}"
            },
            {
              "role": "system",
              "content": "={{ $json.systemPrompt }}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "df7c1b81-c3b8-4a77-b86d-5f2e1c8bc471",
      "name": "Extract AI Analysis Results",
      "type": "n8n-nodes-base.code",
      "position": [
        3216,
        640
      ],
      "parameters": {
        "jsCode": "const raw = $input.first().json;\n\n// Handle different output formats from the OpenAI node in n8n\nconst text = raw.output?.[0]?.content?.[0]?.text\n          || raw.message?.content\n          || raw.choices?.[0]?.message?.content\n          || JSON.stringify(raw);\n\nlet analysis;\ntry {\n  const cleaned = text.replace(/```json|```/g, '').trim();\n  analysis = JSON.parse(cleaned);\n} catch (e) {\n  analysis = {\n    classification: 'UNCERTAIN',\n    severity: 'MEDIUM',\n    summary: 'AI parse error: ' + e.message,\n    action: 'Manual review needed',\n    confidence: 0,\n  };\n}\n\nconst origin = $('Prepare AI Analysis Prompt').first().json;\n\nreturn [{\n  json: {\n    // Spike data\n    symbol:        origin.symbol,\n    price:         origin.price,\n    change24h:     origin.change24h,\n    volRatio:      origin.volRatio,\n    tradeRatio:    origin.tradeRatio,\n    spikeType:     origin.spikeType,\n    currentVol1h:  origin.currentVol1h,\n    avgVol1h:      origin.avgVol1h,\n\n    // Taker data\n    takerBuyPct:   origin.takerBuyPct,\n    takerSellPct:  origin.takerSellPct,\n    pressure:      origin.pressure,\n\n    // Price action\n    priceChange:   origin.priceChange,\n    candleRange:   origin.candleRange,\n    isBullish:     origin.isBullish,\n    bodyRatio:     origin.bodyRatio,\n\n    // Context\n    volTrend:      origin.volTrend,\n    sustainedSpike: origin.sustainedSpike,\n\n    // Order book\n    bookRatio5:    origin.bookRatio5,\n    bookPressure:  origin.bookPressure,\n    spreadPct:     origin.spreadPct,\n    bidVol5:       origin.bidVol5,\n    askVol5:       origin.askVol5,\n\n    // AI analysis\n    classification: analysis.classification,\n    severity:       analysis.severity,\n    summary:        analysis.summary,\n    action:         analysis.action,\n    confidence:     analysis.confidence,\n    entryZone:      analysis.entryZone || 'N/A',\n    invalidation:   analysis.invalidation || 'N/A',\n    timeframe:      analysis.timeframe || 'N/A',\n    \n    detectedAt: origin.detectedAt,\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "5901defc-4920-420f-857e-8108385d04a5",
      "name": "Aggregate Spike Findings",
      "type": "n8n-nodes-base.code",
      "position": [
        3392,
        640
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\n// Store results in workflow static data\nconst staticData = $getWorkflowStaticData('global');\nif (!staticData.spikeResults) {\n  staticData.spikeResults = [];\n}\nstaticData.spikeResults.push(item);\n\nreturn [{ json: { collected: staticData.spikeResults.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "e7b9044a-f842-456f-9388-f676e3aeaca9",
      "name": "Send Alert via Telegram",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2064,
        208
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "YOUR_TELEGRAM_CHAT_ID",
        "additionalFields": {
          "parse_mode": "HTML"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "26d09876-3f94-41a9-84f5-41251685f200",
      "name": "Create Alert Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        1872,
        208
      ],
      "parameters": {
        "jsCode": "const TOP_TELEGRAM = 3;\nconst items = $input.all().map(i => i.json).slice(0, TOP_TELEGRAM);\n\nif (!items.length || !items[0].symbol) {\n  return [{ json: { message: null } }];\n}\n\nconst emoji = {\n  'BULLISH_BREAKOUT': '\ud83d\udfe2\ud83d\ude80',\n  'BEARISH_DUMP':     '\ud83d\udd34\ud83d\udcc9',\n  'ACCUMULATION':     '\ud83d\udfe1\ud83d\udc0b',\n  'DISTRIBUTION':     '\ud83d\udfe0\ud83d\udce4',\n  'UNCERTAIN':        '\u26aa\u2753',\n};\n\nconst medal = ['\ud83e\udd47', '\ud83e\udd48', '\ud83e\udd49'];\nconst sign = n => n > 0 ? '+' : '';\n\nlet message = `\ud83d\udce1 <b>VOLUME SPIKE REPORT</b>\\n`;\nconst hasCritical = items.some(d => d.severity === 'CRITICAL' || d.severity === 'HIGH');\nif (hasCritical) {\n  message += `\ud83d\udea8 <b>HIGH PRIORITY ALERT</b>\\n`;\n}\nmessage += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\\n`;\nmessage += `${items.length} spike(s) detected this hour\\n`;\n\nitems.forEach((d, i) => {\n  const cls = emoji[d.classification] || '\u26aa';\n\n  message += `${medal[i]} <b>${d.symbol}</b> ${cls}\\n`;\n  message += `\u251c Volume:  <code>${d.volRatio}x avg</code> (${d.spikeType})`;\n  if (d.sustainedSpike) message += ` \ud83d\udd04`;\n  message += `\\n`;\n  message += `\u251c Taker:   <code>Buy ${d.takerBuyPct}%</code> \u2192 ${d.pressure}\\n`;\n  message += `\u251c Price:   <code>${sign(d.priceChange)}${d.priceChange}%</code> | Body: <code>${d.bodyRatio}%</code>\\n`;\n  message += `\u251c Book:    <code>${d.bookPressure}</code> (${d.bookRatio5}x)\\n`;\n  message += `\u251c AI:      <code>${d.classification.replace(/_/g, ' ')}</code> (${(d.confidence * 100).toFixed(0)}%)\\n`;\n  message += `\u251c Score:   <code>${d.rankScore}/100</code>\\n`;\n  message += `\u251c Entry:   <code>${d.entryZone}</code>\\n`;\n  message += `\u251c Invalid: <code>${d.invalidation}</code>\\n`;\n  message += `\u251c Time:    <code>${d.timeframe}</code>\\n`;\n  message += `\u2514 <i>${d.summary}</i>\\n\\n`;\n});\n\nmessage += `\ud83d\udd50 ${new Date().toISOString()}`;\n\nreturn [{ json: { message } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "a89c6d17-70a0-4739-a083-e64afe5a35e7",
      "name": "Select Top Performers",
      "type": "n8n-nodes-base.code",
      "position": [
        1680,
        208
      ],
      "parameters": {
        "jsCode": "// \u2550\u2550\u2550 CONFIG \u2550\u2550\u2550\nconst TOP_TELEGRAM = 3;   // Number of coins to send via Telegram\nconst TOP_SHEETS   = 10;  // Number of coins to log to Google Sheets\n\n// Get all collected spikes\nconst staticData = $getWorkflowStaticData('global');\nlet spikes = staticData.spikeResults || [];\nstaticData.spikeResults = []; // Reset for next run\n\nif (spikes.length === 0) {\n  return [[]];\n}\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// SCORING\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\nconst scored = spikes.map(s => {\n  let score = 0;\n\n  // 1. Volume Spike Strength (0-30)\n  if (s.volRatio >= 5)        score += 30;\n  else if (s.volRatio >= 3.5) score += 25;\n  else if (s.volRatio >= 2.5) score += 18;\n\n  if (s.tradeRatio >= 3)      score += 10;\n  else if (s.tradeRatio >= 2) score += 7;\n  else if (s.tradeRatio >= 1.5) score += 4;\n\n  if (s.sustainedSpike) score += 8;\n\n  // 2. Taker Pressure Clarity (0-20)\n  const takerDeviation = Math.abs(s.takerBuyPct - 50);\n  if (takerDeviation >= 20) score += 20;\n  else if (takerDeviation >= 15) score += 15;\n  else if (takerDeviation >= 10) score += 10;\n  else if (takerDeviation >= 5)  score += 4;\n\n  // 3. Price Action Quality (0-20)\n  if (s.bodyRatio >= 70) score += 12;\n  else if (s.bodyRatio >= 50) score += 8;\n  else if (s.bodyRatio >= 30) score += 4;\n\n  const absChange = Math.abs(s.priceChange);\n  if (absChange >= 5) score += 8;\n  else if (absChange >= 3) score += 6;\n  else if (absChange >= 1) score += 3;\n\n  // 4. AI Confidence (0-20)\n  score += Math.round((s.confidence || 0) * 20);\n\n  // 5. Penalties\n  if (s.classification === 'UNCERTAIN') score -= 10;\n  if (s.spreadPct > 0.5) score -= 5;\n  if (s.volRatio >= 3 && s.tradeRatio < 1.2) score -= 8;\n\n  return {\n    ...s,\n    rankScore: Math.max(0, score),\n  };\n});\n\n// Sort by score descending\nconst sorted = scored.sort((a, b) => b.rankScore - a.rankScore);\n\n// Tag: which go to Telegram, which are Sheets-only\nconst results = sorted.slice(0, Math.max(TOP_TELEGRAM, TOP_SHEETS)).map((s, i) => ({\n  ...s,\n  rank: i + 1,\n  forTelegram: i < TOP_TELEGRAM,\n  forSheets: i < TOP_SHEETS,\n}));\n\nreturn results.map(s => ({ json: s }));"
      },
      "typeVersion": 2
    },
    {
      "id": "faf15148-0e98-4a1e-9c0e-72a4a1a5d61b",
      "name": "Prepare Data for Logging",
      "type": "n8n-nodes-base.code",
      "position": [
        1872,
        32
      ],
      "parameters": {
        "jsCode": "// Flatten & order fields for Google Sheets logging\n// Remove internal flags (forTelegram, forSheets, rank) and redundant fields\n\nconst items = $input.all();\n\nreturn items.map(i => {\n  const d = i.json;\n  return { json: {\n    // \u2500\u2500 Timestamp & Identity \u2500\u2500\n    detectedAt:     d.detectedAt,\n    symbol:         d.symbol,\n    price:          d.price,\n\n    // \u2500\u2500 AI Verdict \u2500\u2500\n    classification: d.classification,\n    severity:       d.severity,\n    confidence:     d.confidence,\n    rankScore:      d.rankScore,\n\n    // \u2500\u2500 Volume Spike \u2500\u2500\n    spikeType:      d.spikeType,\n    volRatio:       d.volRatio,\n    tradeRatio:     d.tradeRatio,\n    currentVol1h:   d.currentVol1h,\n    avgVol1h:       d.avgVol1h,\n    sustainedSpike: d.sustainedSpike,\n    volTrend:       d.volTrend,\n\n    // \u2500\u2500 Taker Pressure \u2500\u2500\n    takerBuyPct:    d.takerBuyPct,\n    pressure:       d.pressure,\n\n    // \u2500\u2500 Price Action \u2500\u2500\n    change24h:      d.change24h,\n    priceChange:    d.priceChange,\n    candleRange:    d.candleRange,\n    isBullish:      d.isBullish,\n    bodyRatio:      d.bodyRatio,\n\n    // \u2500\u2500 Order Book \u2500\u2500\n    bookPressure:   d.bookPressure,\n    bookRatio5:     d.bookRatio5,\n    spreadPct:      d.spreadPct,\n\n    // \u2500\u2500 AI Details \u2500\u2500\n    entryZone:      d.entryZone,\n    invalidation:   d.invalidation,\n    timeframe:      d.timeframe,\n    summary:        d.summary,\n    action:         d.action,\n  }};\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "bf8940ee-1026-44dd-97f2-0bf9e90b9d5a",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2688,
        480
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 272,
        "content": "## AI analysis\n\nBuilds an AI prompt and analyzes data using OpenAI's models"
      },
      "typeVersion": 1
    },
    {
      "id": "a74994b7-5a44-456c-aa77-42a3cc6b49aa",
      "name": "Every 1h Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        848,
        400
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "triggerAtMinute": 1
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "0b32cfef-60df-4743-94e8-cb18cdba6fb0",
      "name": "Fetch 1h Kline Data",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1680,
        512
      ],
      "parameters": {
        "url": "https://api.binance.com/api/v3/klines",
        "options": {},
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "symbol",
              "value": "={{ $json.symbol }}"
            },
            {
              "name": "interval",
              "value": "1h"
            },
            {
              "name": "limit",
              "value": "25"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "1f70a937-9c02-4f6d-9cc4-c01e54c600d5",
      "name": "Append Log to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2064,
        32
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "2af3c3d1-8595-43e4-b992-f7e06af13874",
      "name": "Filter USDT Pairs",
      "type": "n8n-nodes-base.code",
      "position": [
        1264,
        400
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst MIN_VOL = 10_000_000; // Min $10M volume 24h\nconst MIN_CHANGE = 1;       // Min 1% price change 24h\n\nconst allTickers = items.map(i => i.json);\nconst btc = allTickers.find(t => t.symbol === 'BTCUSDT');\nconst eth = allTickers.find(t => t.symbol === 'ETHUSDT');\n\nconst staticData = $getWorkflowStaticData('global');\nstaticData.marketContext = {\n  btcChange: parseFloat(btc?.priceChangePercent || 0),\n  ethChange: parseFloat(eth?.priceChangePercent || 0),\n};\n\nconst stablecoins = [\n  'USDCUSDT', 'BUSDUSDT', 'TUSDUSDT', 'DAIUSDT',\n  'FDUSDUSDT', 'USDPUSDT', 'EURUSDT', 'GBPUSDT',\n  'TRYUSDT', 'BRLUSDT', 'ARUSDT', 'USD1USDT',\n  'RLUSDUSDT', 'UUSDT', 'XUSDUSDT', 'PYUSDUSDT',\n  'CUSDUSDT', 'GUSDUSDT', 'SUSDUSDT',\n];\n\nconst excludePatterns = [\n/^(USD|EUR|GBP|AUD|TRY|BRL|JPY|KRW|IDR|NGN|ARS|PLN|RON|COP|ZAR).*USDT$/,\n  /(UP|DOWN|BULL|BEAR)d*USDT$/,\n  /^(LBTC|WBTC|WETH|STETH|RETH|CBETH|WBETH)USDT$/,\n];\n\nconst filtered = items\n  .map(i => i.json)\n  .filter(t =>\n    t.symbol.endsWith('USDT') &&\n    !stablecoins.includes(t.symbol) &&\n    !excludePatterns.some(p => p.test(t.symbol)) &&\n    parseFloat(t.quoteVolume) > MIN_VOL &&\n    Math.abs(parseFloat(t.priceChangePercent)) > MIN_CHANGE\n  )\n  .map(t => ({\n    symbol: t.symbol,\n    price: parseFloat(t.lastPrice),\n    change24h: parseFloat(t.priceChangePercent),\n    volume24h: parseFloat(t.quoteVolume),\n    highPrice: parseFloat(t.highPrice),\n    lowPrice: parseFloat(t.lowPrice),\n    trades: parseInt(t.count),\n  }));\n\nreturn filtered.map(f => ({ json: f }));"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "connections": {
    "Every 1h Trigger": {
      "main": [
        [
          {
            "node": "Fetch Binance All Tickers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter USDT Pairs": {
      "main": [
        [
          {
            "node": "Loop Over Coin Pairs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch 1h Kline Data": {
      "main": [
        [
          {
            "node": "Compute Volume Spike",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Volume Spike": {
      "main": [
        [
          {
            "node": "Check for Volume Spike",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Alert Summary": {
      "main": [
        [
          {
            "node": "Send Alert via Telegram",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Coin Pairs": {
      "main": [
        [
          {
            "node": "Select Top Performers",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch 1h Kline Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Book Pressure": {
      "main": [
        [
          {
            "node": "Prepare AI Analysis Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select Top Performers": {
      "main": [
        [
          {
            "node": "Create Alert Summary",
            "type": "main",
            "index": 0
          },
          {
            "node": "Prepare Data for Logging",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Volume Spike": {
      "main": [
        [
          {
            "node": "Fetch Order Book Depth",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Coin Pairs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Order Book Depth": {
      "main": [
        [
          {
            "node": "Analyze Book Pressure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Spike Findings": {
      "main": [
        [
          {
            "node": "Loop Over Coin Pairs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Data for Logging": {
      "main": [
        [
          {
            "node": "Append Log to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Binance All Tickers": {
      "main": [
        [
          {
            "node": "Filter USDT Pairs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare AI Analysis Prompt": {
      "main": [
        [
          {
            "node": "OpenAI Volume Spike Analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract AI Analysis Results": {
      "main": [
        [
          {
            "node": "Aggregate Spike Findings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Volume Spike Analysis": {
      "main": [
        [
          {
            "node": "Extract AI Analysis Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}