{
  "name": "Binance Day Trading Scanner",
  "nodes": [
    {
      "parameters": {
        "content": "## 📈 Binance Day Trading Scanner — LIVE\n\n**Capital**: ~$300 | **Position**: 50% (~$150/trade)\n**Strategy**: Chase momentum — score top USDT gainers\n\n**BTC Correlation Filter** (EMA9/21/50 + RSI + ROC5):\n- Bull BTC: entries allowed\n- Neutral BTC: score ≥ 5 required\n- Bear BTC: no entries, tighter TP/trail\n- ROC5 < -1.5%: emergency exit\n\n**Entry Verification (5 checks, need ≥3/5)**:\n- RSI 35–72 (momentum, not overbought)\n- EMA9 > EMA21 (micro uptrend)\n- LR slope positive (10-candle regression)\n- Volume accelerating ≥1.1×\n- No large bearish wick on last candle\n\n**Switch**: closes current if better coin appears (score gap ≥10, held ≥2min)\n**Status telegram**: every hold cycle (toggle SEND_STATUS)",
        "height": 508,
        "width": 450,
        "color": 5
      },
      "id": "cb35000a-a711-4f53-b484-9d98333bcbd1",
      "name": "Bot Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        9024,
        -1360
      ]
    },
    {
      "parameters": {
        "content": "## ⚙️ Setup\n1. Open **Build Order Request** → paste API_KEY + SECRET_KEY\n2. Keep ORDER_PATH='/api/v3/order/test' to test first\n3. Change to '/api/v3/order' for live trading\n\n**If you already hold a position before first run:**\nOpen Decision Engine → set INITIAL_POSITION:\n```\n{ symbol: 'XYZUSDT', entry: 1.234,\n  qty: 100, tpPct: 0.02, slPct: 0.01,\n  entryTime: '2026-04-06T10:00:00Z' }\n```\n\n**Reset state**: Workflow Settings → Static Data → Clear\n**Toggle hold status**: SEND_STATUS in Format Hold Status node\n\nTelegram wired ✅",
        "height": 494,
        "width": 450,
        "color": 3
      },
      "id": "c74270f9-8a7e-4d78-a0b8-e6410d31f8fb",
      "name": "Setup Instructions",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        9616,
        -1360
      ]
    },
    {
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "mode": "everyX",
              "value": 1,
              "unit": "minutes"
            }
          ]
        }
      },
      "id": "f7141bf4-a6f4-4595-a870-c9a0edd2ee9d",
      "name": "Every 1 Minute",
      "type": "n8n-nodes-base.cron",
      "typeVersion": 1,
      "position": [
        8624,
        -752
      ]
    },
    {
      "parameters": {
        "url": "https://api.binance.com/api/v3/ticker/24hr",
        "options": {
          "timeout": 10000
        }
      },
      "id": "b94007e1-84ce-433f-8eec-c996b9574e37",
      "name": "Fetch All Tickers",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        8848,
        -752
      ]
    },
    {
      "parameters": {
        "jsCode": "// ============================================================\n// PICK BEST CANDIDATE — scores from ticker data only\n// ============================================================\n\nconst MIN_VOLUME_USDT = 200000;\nconst MIN_CHANGE_PCT  = 0.3;\nconst MAX_CHANGE_PCT  = 50.0;\nconst MIN_PRICE       = 0.00001;\n\nconst EXCLUDE = new Set([\n  'USDC','BUSD','TUSD','DAI','FDUSD','USDP','USDS','USDD','GUSD','FRAX',\n  'USTC','USDN','LUSD','SUSD','CUSD','ZUSD','EURS','EURT','GYEN','BIDR',\n  'BVND','IDRT','USDJ','UST','BTC','ETH','BNB'\n]);\n\n// Handle both: single item with full array, and n8n splitting array into multiple items\nconst allItems = $input.all();\nlet tickers;\nif (allItems.length > 1) {\n  // n8n split the HTTP response array into individual items\n  tickers = allItems.map(item => item.json).filter(t => t && t.symbol);\n} else {\n  const raw = allItems[0].json;\n  if (Array.isArray(raw)) {\n    tickers = raw;\n  } else {\n    const vals = Object.values(raw);\n    tickers = (vals.length > 0 && vals[0] && typeof vals[0] === 'object' && vals[0].symbol) ? vals : [];\n  }\n}\n\nconst sd = $getWorkflowStaticData('global');\nconst heldSymbol = sd.position ? sd.position.symbol : null;\nlet heldCurrentPrice = sd.position ? sd.position.entry : 0;\n\nconst candidates = [];\nfor (const t of tickers) {\n  const symbol    = t.symbol || '';\n  const change24h = parseFloat(t.priceChangePercent || 0);\n  const volume    = parseFloat(t.quoteVolume || 0);\n  const price     = parseFloat(t.lastPrice || 0);\n  const high24h   = parseFloat(t.highPrice  || price);\n  const low24h    = parseFloat(t.lowPrice   || price);\n\n  if (symbol === heldSymbol) heldCurrentPrice = price;  // always capture current price of held coin\n\n  if (!symbol.endsWith('USDT')) continue;\n  const base = symbol.replace('USDT', '');\n  let excluded = false;\n  for (const ex of EXCLUDE) {\n    if (base === ex || base.startsWith(ex) || base.endsWith(ex)) { excluded = true; break; }\n  }\n  if (excluded) continue;\n  if (!/^[A-Z0-9]+$/.test(base)) continue;  // reject non-ASCII/unicode symbols (scam tokens)\n  if (price < MIN_PRICE || volume < MIN_VOLUME_USDT) continue;\n  if (change24h < MIN_CHANGE_PCT || change24h > MAX_CHANGE_PCT) continue;\n\n  const ch = Math.abs(change24h);\n  let stageScore = 0;\n  if      (ch >= 3  && ch < 8)  stageScore = 25;\n  else if (ch >= 8  && ch < 15) stageScore = 20;\n  else if (ch >= 15 && ch < 25) stageScore = 12;\n  else if (ch >= 25 && ch < 40) stageScore = 5;\n  else if (ch >= 0.5)           stageScore = 8;\n\n  const range = high24h - low24h;\n  const rangePct = range > 0 ? (price - low24h) / range : 0.5;\n  let rangeScore = 0;\n  if      (rangePct < 0.40) rangeScore = 15;\n  else if (rangePct < 0.60) rangeScore = 12;\n  else if (rangePct < 0.75) rangeScore = 6;\n  else if (rangePct < 0.90) rangeScore = 2;\n\n  let volScore = 0;\n  if      (volume >= 10000000) volScore = 10;\n  else if (volume >=  5000000) volScore = 8;\n  else if (volume >=  2000000) volScore = 6;\n  else if (volume >=   500000) volScore = 3;\n\n  const score    = stageScore + rangeScore + volScore;\n  const preScore = score * Math.log10(Math.max(volume, 1));\n\n  candidates.push({\n    symbol, change24h, volume, price,\n    high24h, low24h, rangePct: +rangePct.toFixed(3),\n    score, preScore,\n    scoringDetail: { stageScore, rangeScore, volScore, change24h, rangePct: +rangePct.toFixed(3) }\n  });\n}\n\ncandidates.sort((a, b) => b.preScore - a.preScore);\n\nconst scanSummary = candidates.slice(0, 8).map(c => ({\n  symbol: c.symbol, score: c.score,\n  change24h: c.change24h, rangePct: c.rangePct, price: c.price\n}));\n\nif (candidates.length === 0) {\n  return [{ json: {\n    bestSymbol: heldSymbol || 'BTCUSDT',\n    noCandidates: true,\n    heldSymbol,\n    heldCurrentPrice,\n    scanSummary: []\n  }}];\n}\n\nconst best = candidates[0];\n\nconst roomToHigh = best.high24h > best.price ? (best.high24h - best.price) / best.price : 0.02;\nconst tpPct = Math.max(Math.min(roomToHigh * 0.6, 0.05), 0.015);\nconst slPct = 0.01;\n\nreturn [{ json: {\n  bestSymbol:       best.symbol,\n  bestCandidate:    best,\n  heldSymbol,\n  heldCurrentPrice,\n  tpPct:            +tpPct.toFixed(4),\n  slPct:            +slPct.toFixed(4),\n  noCandidates:     false,\n  scanSummary\n}}];"
      },
      "id": "7aefddac-d805-4f6b-a2db-381df4ebf297",
      "name": "Pick Best Candidate",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        9056,
        -752
      ]
    },
    {
      "parameters": {
        "url": "https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1m&limit=50",
        "options": {
          "timeout": 8000
        }
      },
      "id": "5890bf80-3ed9-4ab4-b5d4-d3bad83efe74",
      "name": "Fetch BTC Klines",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        9280,
        -752
      ]
    },
    {
      "parameters": {
        "jsCode": "// ============================================================\n// ANALYZE BTC TREND — EMA9/21/50 + RSI14 + ROC5\n// ============================================================\n\nfunction ema(closes, period) {\n  if (closes.length < period) return closes[closes.length - 1] || 0;\n  const k = 2 / (period + 1);\n  let val = closes.slice(0, period).reduce((s, v) => s + v, 0) / period;\n  for (let i = period; i < closes.length; i++) val = closes[i] * k + val * (1 - k);\n  return val;\n}\nfunction rsiCalc(closes, period) {\n  if (closes.length < period + 1) return 50;\n  const slice = closes.slice(-(period + 1));\n  let g = 0, l = 0;\n  for (let i = 1; i < slice.length; i++) {\n    const d = slice[i] - slice[i - 1];\n    d >= 0 ? (g += d) : (l -= d);\n  }\n  const ag = g / period, al = l / period;\n  return al === 0 ? 100 : 100 - 100 / (1 + ag / al);\n}\n\nconst allBtcItems = $input.all();\nlet klines;\nif (allBtcItems.length > 1) {\n  klines = allBtcItems.map(item => {\n    const v = item.json;\n    return Array.isArray(v) ? v : Object.values(v);\n  });\n} else {\n  const rawInput = allBtcItems[0].json;\n  if (Array.isArray(rawInput) && Array.isArray(rawInput[0])) {\n    klines = rawInput;\n  } else if (Array.isArray(rawInput)) {\n    klines = rawInput.length > 0 && !Array.isArray(rawInput[0]) ? [rawInput] : rawInput;\n  } else {\n    klines = [];\n  }\n}\n\nif (klines.length < 22) {\n  return [{ json: { btcSignal: { trend: 'neutral', score: 0, ema9: 0, ema21: 0, ema50: 0, rsi: 50, roc5: 0, dataOk: false } } }];\n}\n\nconst closes = klines.map(k => parseFloat(k[4]));\nconst n = closes.length;\nconst price = closes[n - 1];\n\nconst EMA9  = ema(closes, 9);\nconst EMA21 = ema(closes, 21);\nconst EMA50 = ema(closes, Math.min(50, closes.length));\nconst RSI   = rsiCalc(closes, 14);\nconst roc5  = closes[n - 6] > 0 ? +((price - closes[n - 6]) / closes[n - 6] * 100).toFixed(4) : 0;\n\nlet score = 0;\nif (EMA9 > EMA21)  score += 0.5; else if (EMA9 < EMA21)  score -= 0.5;\nif (price > EMA50) score += 0.5; else if (price < EMA50) score -= 0.5;\nif (RSI >= 55)     score += 0.5; else if (RSI <= 45)      score -= 0.5;\nif (roc5 >= 0.3)   score += 0.5; else if (roc5 <= -0.3)   score -= 0.5;\nscore = +score.toFixed(2);\n\nconst trend = score >= 1.0 ? 'bull' : score <= -1.0 ? 'bear' : 'neutral';\n\nreturn [{ json: { btcSignal: { trend, score, ema9: +EMA9.toFixed(2), ema21: +EMA21.toFixed(2), ema50: +EMA50.toFixed(2), rsi: +RSI.toFixed(1), roc5, price: +price.toFixed(2), dataOk: true } } }];"
      },
      "id": "d8f7b620-ee88-49af-b6b8-45272661c81b",
      "name": "Analyze BTC Trend",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        9504,
        -752
      ]
    },
    {
      "parameters": {
        "url": "=https://api.binance.com/api/v3/klines?symbol={{ $('Pick Best Candidate').first().json.bestSymbol }}&interval=1m&limit=50",
        "options": {
          "timeout": 8000
        }
      },
      "id": "e70dbe96-0ee0-458b-bee7-f57d9437fc39",
      "name": "Fetch Klines for Winner",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        9728,
        -752
      ]
    },
    {
      "parameters": {
        "jsCode": "// ============================================================\n// DECISION ENGINE\n// ============================================================\n\n// ── CONFIG ───────────────────────────────────────────────────\nconst POSITION_PCT   = 0.50;   // 50% of real USDT balance per trade\nconst TAKER_FEE      = 0.001;  // 0.1% Binance taker fee\nconst MAX_TRADES_DAY = 8;\nconst COOLDOWN_MIN   = 1;      // minutes after a loss before re-entry\nconst TRAIL_TRIGGER  = 0.003;  // 0.3% gain before trailing activates\nconst TRAIL_OFFSET   = 0.002;  // 0.2% drop from peak triggers exit\nconst MAX_HOLD_MINS  = 60;     // max time in position\nconst MIN_CAPITAL    = 10;\n\nconst REAL_CAPITAL   = 285;    // used only on first run to seed sd.capital\nconst INITIAL_POSITION = null; // set if already holding: { symbol, entry, qty, tpPct, slPct, entryTime }\n\nconst SWITCH_DELTA    = 15;    // score gap needed to switch\nconst SWITCH_MIN_HOLD = 3 * 60000;\nconst SWITCH_COOLDOWN = 5 * 60000;\n\nconst LIVE_TRADING          = true;\nconst BTC_BEAR_ROC5_EMERGENCY = -2.0;  // BTC drop > 2% in 5 candles = emergency exit only\n\n// ── INDICATOR HELPERS ────────────────────────────────────────\nfunction ema(data, period) {\n  if (data.length < period) return data[data.length - 1] || 0;\n  const k = 2 / (period + 1);\n  let e = data.slice(0, period).reduce((s, v) => s + v, 0) / period;\n  for (let i = period; i < data.length; i++) e = data[i] * k + e * (1 - k);\n  return e;\n}\nfunction rsiCalc(closes, period) {\n  if (closes.length < period + 1) return 50;\n  const slice = closes.slice(-(period + 1));\n  let g = 0, l = 0;\n  for (let i = 1; i < slice.length; i++) {\n    const d = slice[i] - slice[i - 1];\n    d >= 0 ? (g += d) : (l -= d);\n  }\n  const ag = g / period, al = l / period;\n  return al === 0 ? 100 : 100 - 100 / (1 + ag / al);\n}\nfunction calcATR(highs, lows, closes, period) {\n  const trs = [];\n  for (let i = 1; i < highs.length; i++) {\n    trs.push(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  const sl = trs.slice(-period);\n  return sl.length ? sl.reduce((s, v) => s + v, 0) / sl.length : 0;\n}\nfunction lrSlope(values) {\n  const n = values.length;\n  const xm = (n - 1) / 2;\n  const ym = values.reduce((s, v) => s + v, 0) / n;\n  let num = 0, den = 0;\n  for (let i = 0; i < n; i++) { num += (i - xm) * (values[i] - ym); den += (i - xm) ** 2; }\n  return den > 0 ? num / den : 0;\n}\n\n// ── READ INPUTS ──────────────────────────────────────────────\nconst ctx = $('Pick Best Candidate').first().json;\nconst best = ctx.bestCandidate || {};\nconst bestSymbol = ctx.bestSymbol || 'BTCUSDT';\nconst heldCurrentPrice = ctx.heldCurrentPrice || 0;\n\nconst btcNode   = $('Analyze BTC Trend').first().json;\nconst btcSignal = btcNode.btcSignal || { trend: 'neutral', score: 0, roc5: 0, dataOk: false };\nconst btcBear    = btcSignal.trend === 'bear'    && btcSignal.score <= -1;\nconst btcBull    = btcSignal.trend === 'bull'    && btcSignal.score >= 1;\nconst btcNeutral = !btcBear && !btcBull;\n\n// Handle both: single item with full kline array, and n8n splitting into individual candle items\nconst allKlineItems = $input.all();\nlet rawKlines;\nif (allKlineItems.length > 1) {\n  // n8n split — each item.json is one candle [ts, open, high, low, close, vol, ...]\n  rawKlines = allKlineItems.map(item => {\n    const v = item.json;\n    // n8n may wrap array as { 0: ts, 1: open, ... } object\n    return Array.isArray(v) ? v : Object.values(v);\n  });\n} else {\n  const rawInput = allKlineItems[0].json;\n  if (Array.isArray(rawInput) && Array.isArray(rawInput[0])) {\n    rawKlines = rawInput;  // [[ts,o,h,l,c,v], ...] — full array as single item\n  } else if (Array.isArray(rawInput)) {\n    // Might be a single candle returned — wrap it\n    rawKlines = rawInput.length > 0 && !Array.isArray(rawInput[0]) ? [rawInput] : rawInput;\n  } else {\n    rawKlines = [];\n  }\n}\n\nlet klineIndicators = null;\nif (rawKlines.length >= 20) {\n  const highs   = rawKlines.map(k => parseFloat(k[2]));\n  const lows    = rawKlines.map(k => parseFloat(k[3]));\n  const closes  = rawKlines.map(k => parseFloat(k[4]));\n  const volumes = rawKlines.map(k => parseFloat(k[5]));\n  const n       = closes.length;\n  const price   = closes[n - 1];\n\n  const recentVol   = volumes.slice(-5).reduce((s, v) => s + v, 0) / 5;\n  const baselineVol = volumes.slice(-15, -5).reduce((s, v) => s + v, 0) / 10;\n  const volAccel    = baselineVol > 0 ? recentVol / baselineVol : 1;\n\n  const ATR    = calcATR(highs, lows, closes, 14);\n  const RSI    = rsiCalc(closes, 14);\n  const EMA9   = ema(closes, 9);\n  const EMA21  = ema(closes, 21);\n  const tpCalc = Math.max(price > 0 ? (ATR * 2.0) / price : 0.02, 0.015);\n  const slCalc = Math.max(price > 0 ? (ATR * 1.0) / price : 0.01, 0.008);\n\n  klineIndicators = {\n    rsi:      +RSI.toFixed(1),\n    ema9:     +EMA9.toFixed(8),\n    ema21:    +EMA21.toFixed(8),\n    volAccel: +volAccel.toFixed(2),\n    atr:      +ATR.toFixed(8),\n    tpPct:    +tpCalc.toFixed(6),\n    slPct:    +slCalc.toFixed(6)\n  };\n}\n\nconst tpPct = klineIndicators ? klineIndicators.tpPct : (ctx.tpPct || 0.02);\nconst slPct = klineIndicators ? klineIndicators.slPct : (ctx.slPct || 0.01);\n\n// ── ENTRY VERIFICATION — only hard block on extreme RSI ──────\nlet entryConfirmed = true;  // default: allow entry\nlet entryChecks = { pass: 5 };\nif (klineIndicators) {\n  const rsi = klineIndicators.rsi;\n  const rsiHardBlock = rsi > 82 || rsi < 20;  // only block truly extreme RSI\n  const cls    = rawKlines.map(k => parseFloat(k[4]));\n  const opns   = rawKlines.map(k => parseFloat(k[1]));\n  const highs2 = rawKlines.map(k => parseFloat(k[2]));\n  const n2     = cls.length;\n  const slope  = lrSlope(cls.slice(-10));\n  const body   = Math.abs(cls[n2-1] - opns[n2-1]);\n  const upperWick = highs2[n2-1] - Math.max(cls[n2-1], opns[n2-1]);\n  const rsiOk      = rsi >= 30 && rsi <= 78;\n  const emaTrendOk = klineIndicators.ema9 > klineIndicators.ema21;\n  const slopeOk    = slope > 0;\n  const volOk      = klineIndicators.volAccel >= 0.8;\n  const noReversal = body === 0 || upperWick < body * 4.0;\n  const pass = [rsiOk, emaTrendOk, slopeOk, volOk, noReversal].filter(Boolean).length;\n  entryConfirmed = !rsiHardBlock;  // only block if RSI is extreme\n  entryChecks = { rsiOk, emaTrendOk, slopeOk, volOk, noReversal, pass, rsiHardBlock, rsi, slope: +slope.toFixed(8) };\n}\n\n// ── STATE ────────────────────────────────────────────────────\nconst sd = $getWorkflowStaticData('global');\n// Force-correct capital if it looks like stale test data (> 10x REAL_CAPITAL)\nif (sd.capital === undefined || sd.capital > REAL_CAPITAL * 5) sd.capital = REAL_CAPITAL;\nif (sd.trades       === undefined) sd.trades       = 0;\nif (sd.todayTrades  === undefined) sd.todayTrades  = 0;\nif (sd.todayDate    === undefined) sd.todayDate    = '';\nif (sd.wins         === undefined) sd.wins         = 0;\nif (sd.losses       === undefined) sd.losses       = 0;\nif (sd.pnl          === undefined) sd.pnl          = 0;\nif (sd.lastLossTime   === undefined) sd.lastLossTime   = 0;\nif (sd.lastSwitchTime === undefined) sd.lastSwitchTime = 0;\n\nconst today = new Date().toISOString().slice(0, 10);\nif (sd.todayDate !== today) { sd.todayDate = today; sd.todayTrades = 0; }\n\n// ── STATE SELF-HEAL ─────────────────────────────────────────\n// If sd.position is set but pendingFill stuck > 5 min, release it\nif (sd.pendingFill && (!sd.pendingFillTime || (Date.now() - sd.pendingFillTime) > 2 * 60000)) {\n  sd.pendingFill = null;\n  sd.pendingFillTime = null;\n}\n\n// Update capital from real USDT — already done in Build Order Request when balance is fetched.\n// Here just ensure capital is sane.\nif (sd.capital <= 0 || sd.capital > REAL_CAPITAL * 10) sd.capital = REAL_CAPITAL;\n\n// First-run: import existing position if configured manually\nif (!sd.position && INITIAL_POSITION) {\n  sd.position = { ...INITIAL_POSITION, highWater: INITIAL_POSITION.entry, entryScore: 0 };\n}\n\nconst now  = Date.now();\nconst utcH = new Date().getUTCHours();\nconst utcM = new Date().getUTCMinutes();\n\nconst cooldownOk   = (now - sd.lastLossTime) > COOLDOWN_MIN * 60000;\nconst tradeLimitOk = sd.todayTrades < MAX_TRADES_DAY;\nconst capitalOk    = sd.capital > MIN_CAPITAL;\n\nfunction buildPortfolio() {\n  return {\n    capital:     +sd.capital.toFixed(2),\n    position:    sd.position || null,\n    trades:      sd.trades,\n    todayTrades: sd.todayTrades,\n    wins:        sd.wins,\n    losses:      sd.losses,\n    totalPnl:    +sd.pnl.toFixed(2),\n    winRate:     +(sd.trades > 0 ? sd.wins / sd.trades * 100 : 0).toFixed(1)\n  };\n}\n\nfunction closePos(pos, exitPrice, reason) {\n  const gross    = (exitPrice - pos.entry) * pos.qty;\n  const entryFee = pos.entry * pos.qty * TAKER_FEE;\n  const exitFee  = exitPrice * pos.qty * TAKER_FEE;\n  const netPnl   = gross - entryFee - exitFee;\n  const pct      = (exitPrice - pos.entry) / pos.entry;\n  const holdMins = +((now - new Date(pos.entryTime).getTime()) / 60000).toFixed(1);\n  if (!LIVE_TRADING) {\n    sd.capital += netPnl;\n    sd.pnl     += netPnl;\n    sd.trades  += 1;\n    netPnl >= 0 ? sd.wins++ : sd.losses++;\n    if (netPnl < 0) sd.lastLossTime = now;\n    sd.position = null;\n  }\n  return {\n    symbol:     pos.symbol,\n    entryPrice: +pos.entry.toFixed(8),\n    exitPrice:  +exitPrice.toFixed(8),\n    qty:        +pos.qty.toFixed(6),\n    grossPnl:   +gross.toFixed(4),\n    fees:       +(entryFee + exitFee).toFixed(4),\n    netPnl:     +netPnl.toFixed(4),\n    pnlPct:     +(pct * 100).toFixed(3),\n    reason, holdMins,\n    capital:    +sd.capital.toFixed(2),\n    totalPnl:   +sd.pnl.toFixed(2),\n    wins:       sd.wins,\n    losses:     sd.losses,\n    winRate:    +(sd.trades > 0 ? sd.wins / sd.trades * 100 : 0).toFixed(1),\n    entryTime:  pos.entryTime,\n    exitTime:   new Date().toISOString()\n  };\n}\n\nfunction openPos(symbol, price, score, change24h, tp, sl) {\n  const tradeValue = sd.capital * POSITION_PCT;\n  const qty        = tradeValue / price;\n  if (!LIVE_TRADING) {\n    sd.todayTrades += 1;\n    sd.position = {\n      symbol, entry: price, qty, tradeValue,\n      tpPct: tp, slPct: sl,\n      highWater: price,\n      entryTime: new Date().toISOString(),\n      entryScore: score\n    };\n  }\n  return {\n    symbol,\n    entryPrice:  +price.toFixed(8),\n    qty:         +qty.toFixed(6),\n    tradeValue:  +tradeValue.toFixed(2),\n    takeProfit:  +(price * (1 + tp)).toFixed(8),\n    stopLoss:    +(price * (1 - sl)).toFixed(8),\n    tpPct:       +(tp * 100).toFixed(3),\n    slPct:       +(sl * 100).toFixed(3),\n    score, change24h,\n    indicators:  klineIndicators,\n    scoringDetail: best.scoringDetail || {},\n    capital:     +sd.capital.toFixed(2),\n    todayTrades: sd.todayTrades,\n    timestamp:   new Date().toISOString()\n  };\n}\n\n// ── NO CANDIDATES GUARD ──────────────────────────────────────\nif (ctx.noCandidates && !sd.position) {\n  return [{ json: { action: 'hold', reason: 'no_candidates', btcSignal, entryChecks, portfolio: buildPortfolio(), bestCandidate: { symbol: ctx.bestSymbol || '', score: 0, change24h: 0, price: 0 }, scanSummary: ctx.scanSummary } }];\n}\n\n// ── POSITION MANAGEMENT ──────────────────────────────────────\nconst pos = sd.position || null;\nlet action = 'hold';\nlet trade  = null;\n\nif (pos) {\n  // Use ticker price for the held coin — heldCurrentPrice is always updated from tickers in Pick Best Candidate\n  // Fall back to pos.entry only if the coin disappeared from Binance entirely\n  const currentPrice = heldCurrentPrice > 0 ? heldCurrentPrice\n    : (pos.symbol === bestSymbol ? (best.price || pos.entry) : pos.entry);\n\n  const peak = Math.max(pos.highWater || pos.entry, currentPrice);\n  sd.position.highWater = peak;\n\n  const pctFromEntry = (currentPrice - pos.entry) / pos.entry;\n  const pctFromPeak  = (peak - currentPrice) / peak;\n  const heldMins     = (now - new Date(pos.entryTime).getTime()) / 60000;\n\n  let exitReason = null;\n  if (pctFromEntry >=  pos.tpPct)                                                         exitReason = 'Take Profit';\n  if (pctFromEntry <= -pos.slPct)                                                         exitReason = 'Stop Loss';\n  if (!exitReason && pctFromEntry >= TRAIL_TRIGGER && pctFromPeak >= TRAIL_OFFSET)  exitReason = 'Trailing Stop';\n  if (!exitReason && utcH === 23 && utcM >= 45 && pctFromEntry > 0)                       exitReason = 'EOD Close';\n  if (!exitReason && heldMins >= MAX_HOLD_MINS)                                            exitReason = 'Max Hold Time';\n  if (!exitReason && btcSignal.roc5 < BTC_BEAR_ROC5_EMERGENCY)                            exitReason = 'BTC Emergency Exit';\n\n  if (exitReason) {\n    const closed = closePos(pos, currentPrice, exitReason);\n    action = closed.netPnl >= 0 ? 'close_tp' : 'close_sl';\n    trade  = closed;\n  } else {\n    // Switch — close current, open best next cycle\n    const timeSinceSwitch = now - (sd.lastSwitchTime || 0);\n    const canSwitch = (\n      pctFromEntry > -0.005 &&  // not losing more than 0.5%\n      pos.symbol !== bestSymbol &&\n      (best.score || 0) > (pos.entryScore || 0) + SWITCH_DELTA &&\n      (now - new Date(pos.entryTime).getTime()) > SWITCH_MIN_HOLD &&\n      timeSinceSwitch > SWITCH_COOLDOWN &&\n      tradeLimitOk &&\n      entryConfirmed\n    );\n    if (canSwitch) {\n      const closed = closePos(pos, currentPrice, 'Switch to ' + bestSymbol);\n      sd.lastSwitchTime = now;\n      sd.pendingEntry   = { symbol: bestSymbol, price: best.price, score: best.score || 0, change24h: best.change24h };\n      action = 'close_switch';\n      trade  = closed;\n    }\n  }\n\n} else {\n  // ── ENTRY ────────────────────────────────────────────────\n  // Gate: RSI not extreme, capital available, not already waiting for fill\n  // BTC trend does NOT block entry — it only affects TP/SL tightness via ATR\n  const roundTripFees   = 2 * TAKER_FEE;  // 0.2%\n  const profitableEntry = (tpPct - roundTripFees) >= 0.001;  // need >0.1% net after fees\n\n  if (sd.pendingFill) {\n    trade = { holdReasons: ['awaitingFill'] };\n  } else if (sd.pendingEntry) {\n    // Priority re-entry after a switch\n    const pe = sd.pendingEntry;\n    action = 'buy';\n    trade  = openPos(pe.symbol, pe.price, pe.score || 0, pe.change24h || 0, tpPct, slPct);\n    sd.pendingFill = true; sd.pendingFillTime = Date.now();\n  } else if (cooldownOk && tradeLimitOk && capitalOk && !ctx.noCandidates && entryConfirmed && profitableEntry) {\n    action = 'buy';\n    trade  = openPos(bestSymbol, best.price, best.score || 0, best.change24h || 0, tpPct, slPct);\n    sd.pendingFill = true; sd.pendingFillTime = Date.now();\n  } else {\n    const holdReasons = [];\n    if (!cooldownOk)       holdReasons.push('cooldown');\n    if (!tradeLimitOk)     holdReasons.push('dayLimit');\n    if (!capitalOk)        holdReasons.push('lowCapital($' + sd.capital.toFixed(0) + ')');\n    if (ctx.noCandidates)  holdReasons.push('noCandidates');\n    if (!entryConfirmed)   holdReasons.push('rsiExtreme(' + (entryChecks.rsi||'?') + ')');\n    if (!profitableEntry)  holdReasons.push('notProfitable(tp:' + (tpPct*100).toFixed(2) + '%)');\n    trade = { holdReasons };\n  }\n}\n\nreturn [{ json: {\n  action, trade, btcSignal, entryChecks,\n  bestCandidate: { symbol: bestSymbol, score: best.score, change24h: best.change24h, price: best.price },\n  scanSummary:   ctx.scanSummary,\n  portfolio:     buildPortfolio(),\n  klineOk:       rawKlines.length >= 20,\n  timestamp:     new Date().toISOString()\n}}];"
      },
      "id": "735f979b-7d24-4a2a-bfb8-65240d41f9d0",
      "name": "Decision Engine",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        9936,
        -752
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond-action",
              "leftValue": "={{ $json.action }}",
              "rightValue": "hold",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "794d2f88-b4e7-4ecf-bf83-cc158d2512fd",
      "name": "Has Trade Action?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        10160,
        -752
      ]
    },
    {
      "parameters": {
        "jsCode": "// ============================================================\n// BUILD ORDER REQUEST — HMAC-SHA256 signed Binance order\n// Pure-JS HMAC (crypto module blocked by n8n task-runner sandbox)\n// ============================================================\n\nconst API_KEY    = 'kFYcAx3hJjlAtxbtSjl0DuY1s6zIXLm0YNnwhEqueWMkHvOEAkyBfAAGGfJTfoJS';\nconst SECRET_KEY = 'x7Em12CzxC7bTWmCUah7W6Q8DAFiZgXkmzRRLJnxnaJyAdmQtcCkU4Spun7QjYMt';\nconst ORDER_PATH = '/api/v3/order';          // LIVE — real orders\n\nfunction sha256(msgBytes) {\n  const K = [0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2];\n  let h = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];\n  const ml = msgBytes.length * 8;\n  msgBytes.push(0x80);\n  while (msgBytes.length % 64 !== 56) msgBytes.push(0);\n  for (let i = 56; i >= 0; i -= 8) msgBytes.push((ml / Math.pow(2, i)) & 0xff);\n  for (let i = 0; i < msgBytes.length; i += 64) {\n    const w = [];\n    for (let j = 0; j < 16; j++) w[j] = (msgBytes[i+j*4]<<24)|(msgBytes[i+j*4+1]<<16)|(msgBytes[i+j*4+2]<<8)|msgBytes[i+j*4+3];\n    for (let j = 16; j < 64; j++) {\n      const s0 = (w[j-15]>>>7|w[j-15]<<25)^(w[j-15]>>>18|w[j-15]<<14)^(w[j-15]>>>3);\n      const s1 = (w[j-2]>>>17|w[j-2]<<15)^(w[j-2]>>>19|w[j-2]<<13)^(w[j-2]>>>10);\n      w[j] = (w[j-16]+s0+w[j-7]+s1) >>> 0;\n    }\n    let [a,b,c,d,e,f,g,hh] = h;\n    for (let j = 0; j < 64; j++) {\n      const S1 = (e>>>6|e<<26)^(e>>>11|e<<21)^(e>>>25|e<<7);\n      const ch = (e&f)^(~e&g);\n      const t1 = (hh+S1+ch+K[j]+w[j]) >>> 0;\n      const S0 = (a>>>2|a<<30)^(a>>>13|a<<19)^(a>>>22|a<<10);\n      const maj = (a&b)^(a&c)^(b&c);\n      const t2 = (S0+maj) >>> 0;\n      hh=g; g=f; f=e; e=(d+t1)>>>0; d=c; c=b; b=a; a=(t1+t2)>>>0;\n    }\n    h = h.map((v,i)=>[a,b,c,d,e,f,g,hh][i]+v>>>0);\n  }\n  return h.map(v=>v.toString(16).padStart(8,'0')).join('');\n}\nfunction strToBytes(s) { return Array.from(s).map(c => c.charCodeAt(0) & 0xff); }\nfunction hmacSha256(key, msg) {\n  let k = strToBytes(key);\n  const blockSize = 64;\n  if (k.length > blockSize) { const h = sha256([...k]); k = h.match(/../g).map(x => parseInt(x,16)); }\n  while (k.length < blockSize) k.push(0);\n  const ipad = k.map(b => b ^ 0x36);\n  const opad = k.map(b => b ^ 0x5c);\n  const innerBytes = sha256([...ipad, ...strToBytes(msg)]).match(/../g).map(x => parseInt(x,16));\n  return sha256([...opad, ...innerBytes]);\n}\n\nfunction roundStep(qty, decimals) {\n  const factor = Math.pow(10, decimals);\n  return Math.floor(qty * factor) / factor;\n}\n\n// Decision comes from Sign Balance URL node (passed through from Decision Engine)\nconst signNode = $('Sign Balance URL').first().json;\nconst decision = signNode.decision || {};\nconst action   = signNode.action || decision.action;\nconst trade    = { ...(signNode.trade || decision.trade || {}) };  // copy so we can mutate\n\n// Helper: always return a valid output with full tradeContext so Process Order Fill can read symbol etc\nfunction makeOutput(url, side, sym, qParam, qValue) {\n  return [{ json: {\n    url,\n    headers: { 'X-MBX-APIKEY': API_KEY },\n    orderSide: side, symbol: sym, quantityParam: qParam, quantityValue: qValue,\n    action, tradeContext: trade,\n    btcSignal:   decision.btcSignal   || null,\n    portfolio:   decision.portfolio   || {},\n    scanSummary: decision.scanSummary || []\n  } }];\n}\n\nlet side, symbol, quantityParam, quantityValue;\n\nif (action === 'buy') {\n  const sd = $getWorkflowStaticData('global');\n\n  // Use real USDT balance from Fetch Real Balance if available — prevents double-spend\n  let realUSDT = 0;\n  try {\n    const balNode = $('Fetch Real Balance').first().json;\n    const balances = balNode.balances || [];\n    const usdtBal = balances.find(b => b.asset === 'USDT');\n    realUSDT = usdtBal ? parseFloat(usdtBal.free || 0) : 0;\n  } catch(e) { realUSDT = 0; }\n\n  // If real USDT < 10, we have no free balance — skip order (already in position)\n  if (realUSDT < 10) {\n    sd.pendingFill = null;\n    return makeOutput('https://api.binance.com/api/v3/ping', 'BUY', trade.symbol || 'SKIP', 'quoteOrderQty', '0');\n  }\n\n  // Cap at 50% of real free USDT, also sync sd.capital\n  sd.capital = realUSDT;\n  const tradeValue = +(realUSDT * 0.50).toFixed(2);\n\n  side          = 'BUY';\n  symbol        = trade.symbol;\n  quantityParam = 'quoteOrderQty';\n  quantityValue = tradeValue.toFixed(2);\n\n  trade.tradeValue  = tradeValue;\n  trade.realBalance = realUSDT;\n\n} else if (action === 'close_tp' || action === 'close_sl' || action === 'close_switch') {\n  const sd = $getWorkflowStaticData('global');\n  symbol = trade.symbol || (sd.position && sd.position.symbol);\n\n  // Use real coin balance from Fetch Real Balance — avoids -2010 from qty mismatch\n  let realCoinQty = 0;\n  try {\n    const balNode  = $('Fetch Real Balance').first().json;\n    const balances = balNode.balances || [];\n    const base     = symbol.replace('USDT', '');\n    const coinBal  = balances.find(b => b.asset === base);\n    realCoinQty    = coinBal ? parseFloat(coinBal.free || 0) : 0;\n  } catch(e) { realCoinQty = 0; }\n\n  // Fall back to sd.position.qty if balance fetch failed\n  if (realCoinQty <= 0) realCoinQty = sd.position ? sd.position.qty : (trade.qty || 0);\n\n  // Get LOT_SIZE stepDecimals from Fetch Exchange Info node\n  let stepDp = 2; // safe default\n  try {\n    const exInfo = $('Fetch Exchange Info').first().json;\n    const lot = ((exInfo.symbols || [])[0]?.filters || []).find(f => f.filterType === 'LOT_SIZE');\n    if (lot) {\n      const step = lot.stepSize.replace(/0+$/, '');\n      stepDp = step.includes('.') ? step.split('.')[1].length : 0;\n    }\n  } catch(e) { stepDp = 2; }\n  const factor  = Math.pow(10, stepDp);\n  side          = 'SELL';\n  quantityParam = 'quantity';\n  quantityValue = Math.floor(realCoinQty * factor) / factor + '';\n} else {\n  // Unknown action — release guard and pass through\n  const sd = $getWorkflowStaticData('global');\n  sd.pendingFill = null;\n  return makeOutput('https://api.binance.com/api/v3/ping', 'BUY', trade.symbol || 'SKIP', 'quoteOrderQty', '0');\n}\n\nconst timestamp   = Date.now();\nconst queryString = 'symbol=' + symbol + '&side=' + side + '&type=MARKET&' + quantityParam + '=' + quantityValue + '&recvWindow=5000&timestamp=' + timestamp;\nconst signature   = hmacSha256(SECRET_KEY, queryString);\nconst signedUrl   = 'https://api.binance.com' + ORDER_PATH + '?' + queryString + '&signature=' + signature;\n\nreturn makeOutput(signedUrl, side, symbol, quantityParam, quantityValue);"
      },
      "id": "cb976d26-f8e3-4f48-8c42-d7615c5241fd",
      "name": "Build Order Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        11088,
        -880
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $json.url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-MBX-APIKEY",
              "value": "kFYcAx3hJjlAtxbtSjl0DuY1s6zIXLm0YNnwhEqueWMkHvOEAkyBfAAGGfJTfoJS"
            }
          ]
        },
        "options": {
          "timeout": 10000
        }
      },
      "id": "3d99a889-d8a8-42b4-93bf-f14157b53ce4",
      "name": "Place Binance Order",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        11328,
        -880
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "// ============================================================\n// PROCESS ORDER FILL\n// ============================================================\n\nconst sd          = $getWorkflowStaticData('global');\nconst orderCtx    = $('Build Order Request').first().json;\n\n// Always release pendingFill first if this was a skip order\nif (orderCtx.quantityValue === '0' || orderCtx.symbol === 'SKIP') {\n  sd.pendingFill = null; sd.pendingFillTime = null;\n  return [{ json: { action: orderCtx.action||'hold', orderStatus: 'SKIPPED', trade: orderCtx.tradeContext||{}, btcSignal: orderCtx.btcSignal, portfolio: orderCtx.portfolio, scanSummary: orderCtx.scanSummary } }];\n}\n\nconst binanceResp = $input.first().json;\nconst action      = orderCtx.action;\nconst tradeCtx    = orderCtx.tradeContext || {};\n\n// ── COMPREHENSIVE FAILURE DETECTION ─────────────────────────\n// Detect ALL failure modes:\n// 1. Binance API error: { code: -1xxx, msg: '...' }\n// 2. n8n continueOnFail HTTP error: { error: '...', statusCode: 4xx }\n// 3. n8n wraps as: { message: '...', name: 'NodeApiError', httpCode: '4xx' }\n// 4. Empty response (no orderId, no status) — order did NOT go through\n// 5. Non-FILLED status\nconst isBinanceError   = binanceResp.code && Number(binanceResp.code) < 0;\nconst isHttpError      = binanceResp.statusCode && Number(binanceResp.statusCode) >= 400;\nconst isNodeError      = binanceResp.name === 'NodeApiError' || binanceResp.name === 'NodeOperationError';\nconst hasOrderId       = !!binanceResp.orderId;\nconst isFilled         = binanceResp.status === 'FILLED' || binanceResp.status === 'PARTIALLY_FILLED';\nconst isMissingFill    = !hasOrderId || !isFilled;\n\n// Any of these conditions = order did NOT reach Binance or was rejected\nconst orderFailed = isBinanceError || isHttpError || isNodeError || isMissingFill;\n\nif (orderFailed) {\n  sd.pendingFill     = null;  // always release guard — never leave bot stuck\n  sd.pendingFillTime = null;\n  // Build a diagnostic error message to see what Binance actually returned\n  let errCode = binanceResp.code || binanceResp.statusCode || binanceResp.httpCode || '?';\n  let errMsg  = binanceResp.msg || (typeof binanceResp.message === 'string' ? binanceResp.message : '') || (typeof binanceResp.error === 'string' ? binanceResp.error : '');\n  if (!errMsg) errMsg = JSON.stringify(binanceResp).slice(0, 400);\n  const diagMsg = 'ORDER FAILED [' + errCode + ']: ' + errMsg + ' | sym=' + (orderCtx.symbol||'?') + ' side=' + (orderCtx.orderSide||'?') + ' qty=' + (orderCtx.quantityValue||'?');\n  return [{ json: {\n    action, orderStatus: 'ERROR',\n    binanceError: { code: errCode, msg: errMsg },\n    trade: { ...tradeCtx, symbol: tradeCtx.symbol || orderCtx.symbol },\n    btcSignal:   orderCtx.btcSignal,\n    portfolio:   orderCtx.portfolio,\n    scanSummary: orderCtx.scanSummary,\n    message:     diagMsg\n  } }];\n}\n\nconst fills            = Array.isArray(binanceResp.fills) ? binanceResp.fills : [];\nconst executedQty      = parseFloat(binanceResp.executedQty || 0);\nconst cummulativeQuote = parseFloat(binanceResp.cummulativeQuoteQty || 0);\nconst orderStatus      = binanceResp.status;\nconst orderId          = binanceResp.orderId;\nconst symbol           = binanceResp.symbol || orderCtx.symbol;\nconst side             = orderCtx.orderSide;\n\nlet avgFillPrice = 0;\nif (fills.length > 0) {\n  const tv = fills.reduce((s, f) => s + parseFloat(f.qty||0) * parseFloat(f.price||0), 0);\n  const tq = fills.reduce((s, f) => s + parseFloat(f.qty||0), 0);\n  avgFillPrice = tq > 0 ? tv / tq : 0;\n} else if (executedQty > 0) {\n  avgFillPrice = cummulativeQuote / executedQty;\n}\n\n// Safety: if avgFillPrice is still 0 after a real fill, something is very wrong\nif (avgFillPrice <= 0) {\n  sd.pendingFill = null; sd.pendingFillTime = null;\n  return [{ json: {\n    action, orderStatus: 'ERROR',\n    binanceError: { code: '?', msg: 'avgFillPrice=0 despite FILLED status' },\n    trade: { ...tradeCtx, symbol: tradeCtx.symbol || orderCtx.symbol },\n    btcSignal:   orderCtx.btcSignal, portfolio: orderCtx.portfolio, scanSummary: orderCtx.scanSummary,\n    message: 'ORDER PRICE ERROR: fill price was 0 — orderId=' + orderId + ' status=' + orderStatus\n  } }];\n}\n\nconst totalCommission = fills.reduce((s, f) => s + parseFloat(f.commission||0), 0);\nconst commissionAsset = fills.length > 0 ? fills[0].commissionAsset : 'BNB';\n\nlet enrichedTrade = { ...tradeCtx };\n\nif (side === 'BUY') {\n  // Real fill confirmed — safe to set position\n  const realQty    = executedQty > 0 ? executedQty : cummulativeQuote / avgFillPrice;\n  const tradeValue = cummulativeQuote > 0 ? cummulativeQuote : realQty * avgFillPrice;\n  const fillEntry  = avgFillPrice;\n  // tpPct/slPct from openPos() are in % (e.g. 2.0 means 2%) — convert to decimal for position tracking\n  const tpDecimal  = tradeCtx.tpPct ? tradeCtx.tpPct / 100 : 0.02;\n  const slDecimal  = tradeCtx.slPct ? tradeCtx.slPct / 100 : 0.01;\n  sd.position = {\n    symbol:     symbol,\n    entry:      fillEntry,\n    qty:        realQty,\n    tradeValue,\n    tpPct:      tpDecimal,\n    slPct:      slDecimal,\n    highWater:  fillEntry,\n    entryTime:  new Date().toISOString(),\n    entryScore: tradeCtx.score || 0\n  };\n  sd.todayTrades = (sd.todayTrades || 0) + 1;\n  sd.pendingFill  = null;  // release the re-entry guard — order filled successfully\n  sd.pendingEntry = null;  // clear any pending switch entry — we're now in position\n  enrichedTrade = {\n    ...tradeCtx,\n    symbol,\n    realFillPrice:   +fillEntry.toFixed(8),\n    realQty:         +realQty.toFixed(6),\n    realQuoteSpent:  +tradeValue.toFixed(4),\n    realCommission:  +totalCommission.toFixed(8),\n    commissionAsset, orderId, orderStatus\n  };\n\n} else if (side === 'SELL') {\n  const entryPrice = (sd.position && sd.position.entry) || tradeCtx.entryPrice || 0;\n  const heldQty    = (sd.position && sd.position.qty)   || tradeCtx.qty        || executedQty || 0;\n  const fillPrice  = avgFillPrice > 0 ? avgFillPrice : tradeCtx.exitPrice || entryPrice;\n  const grossPnl   = (fillPrice - entryPrice) * heldQty;\n  const entryFee   = entryPrice * heldQty * 0.001;\n  const exitFee    = fillPrice  * heldQty * 0.001;\n  const netPnl     = grossPnl - entryFee - exitFee;\n  const pnlPct     = entryPrice > 0 ? (fillPrice - entryPrice) / entryPrice * 100 : 0;\n  const holdMins   = sd.position && sd.position.entryTime\n    ? +((Date.now() - new Date(sd.position.entryTime).getTime()) / 60000).toFixed(1)\n    : (tradeCtx.holdMins || 0);\n\n  if (sd.capital !== undefined) sd.capital = (sd.capital || 300) + netPnl;\n  sd.pnl     = (sd.pnl    || 0) + netPnl;\n  sd.trades  = (sd.trades || 0) + 1;\n  if (netPnl >= 0) sd.wins   = (sd.wins   || 0) + 1;\n  else             { sd.losses = (sd.losses || 0) + 1; sd.lastLossTime = Date.now(); }\n  sd.position = null;\n  // Note: if action==='close_switch', sd.pendingEntry is preserved (set by Decision Engine)\n\n  enrichedTrade = {\n    ...tradeCtx,\n    symbol,\n    realFillPrice:  +fillPrice.toFixed(8),\n    realQty:        +heldQty.toFixed(6),\n    realNetPnl:     +netPnl.toFixed(4),\n    realPnlPct:     +pnlPct.toFixed(3),\n    realFees:       +(entryFee + exitFee).toFixed(4),\n    realCommission: +totalCommission.toFixed(8),\n    commissionAsset, holdMins, orderId, orderStatus,\n    capital:  +(sd.capital||0).toFixed(2),\n    totalPnl: +(sd.pnl||0).toFixed(2),\n    wins:      sd.wins   || 0,\n    losses:    sd.losses || 0,\n    winRate:   +((sd.trades||0) > 0 ? (sd.wins||0) / (sd.trades||0) * 100 : 0).toFixed(1)\n  };\n}\n\nreturn [{ json: {\n  action, orderStatus,\n  trade:       enrichedTrade,\n  btcSignal:   orderCtx.btcSignal,\n  portfolio:   orderCtx.portfolio,\n  scanSummary: orderCtx.scanSummary\n} }];"
      },
      "id": "3304792b-8c69-4211-8c61-870c796a4d42",
      "name": "Process Order Fill",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        11536,
        -880
      ]
    },
    {
      "parameters": {
        "jsCode": "const inp = $input.first().json;\nconst a   = inp.action;\nconst t   = inp.trade || {};\nconst p   = inp.portfolio || {};\nconst btc = inp.btcSignal || {};\nconst ind = t.indicators || {};\nconst NL  = '\\n';\nconst now3    = new Date(Date.now() + 3 * 3600000);\nconst timeStr = now3.toISOString().slice(11, 16) + ' UTC+3';\nconst btcLine = btc.trend ? 'BTC: ' + btc.trend.toUpperCase() + ' score:' + btc.score + ' RSI:' + btc.rsi + ' ROC5:' + btc.roc5 + '%' + NL : '';\n\nlet msg = '';\n\nif (inp.orderStatus === 'ERROR') {\n  msg = '\\u26A0\\uFE0F <b>ORDER ERROR</b> ' + timeStr + NL + (inp.message || 'Binance API error') + NL + 'Symbol: ' + (t.symbol||'?') + ' | Action: ' + (a||'?');\n\n} else if (inp.orderStatus === 'SKIPPED') {\n  // Low balance or unhandled — return no items so Send Telegram Alert is skipped\n  return [];\n\n} else if (a === 'buy') {\n  const fillPrice = t.realFillPrice || t.entryPrice || '?';\n  const spent     = t.realQuoteSpent || t.tradeValue || '?';\n  msg = '\\uD83D\\uDE80 <b>ENTRY: ' + (t.symbol||'?') + '</b> ' + timeStr + NL +\n    'Score: ' + (t.score||0) + ' | +' + (+(t.change24h||0)).toFixed(2) + '% today' + NL +\n    'Fill: $' + fillPrice + ' | Size: $' + spent + NL +\n    'TP: +' + (t.tpPct||'?') + '% | SL: -' + (t.slPct||'?') + '%' + NL +\n    (ind.rsi ? 'RSI: ' + ind.rsi + ' | VolAccel: ' + ind.volAccel + 'x' + NL : '') +\n    btcLine +\n    'Capital: $' + (t.capital||p.capital||'?') + ' | Trade #' + (t.todayTrades||1) + ' today';\n\n} else if (a === 'close_switch') {\n  const fillPrice = t.realFillPrice || t.exitPrice || '?';\n  const netPnl = t.realNetPnl !== undefined ? t.realNetPnl : (t.netPnl || 0);\n  const pnlPct = t.realPnlPct !== undefined ? t.realPnlPct : (t.pnlPct || 0);\n  const fees   = t.realFees   !== undefined ? t.realFees   : (t.fees   || 0);\n  msg = '\\uD83D\\uDD04 <b>SWITCH: Closed ' + (t.symbol||'?') + '</b> ' + timeStr + NL +\n    'Fill: $' + fillPrice + ' | ' + (pnlPct>=0?'+':'') + pnlPct + '% | Net: $' + netPnl + NL +\n    'Fees: $' + fees + ' | Held: ' + (t.holdMins||0) + ' min' + NL +\n    btcLine +\n    'Opening best pair next cycle...';\n\n} else if (a === 'close_tp') {\n  const fillPrice = t.realFillPrice || t.exitPrice || '?';\n  const netPnl = t.realNetPnl !== undefined ? t.realNetPnl : (t.netPnl || 0);\n  const pnlPct = t.realPnlPct !== undefined ? t.realPnlPct : (t.pnlPct || 0);\n  const fees   = t.realFees   !== undefined ? t.realFees   : (t.fees   || 0);\n  msg = '\\u2705 <b>TAKE PROFIT: ' + (t.symbol||'?') + '</b> ' + timeStr + NL +\n    'Entry: $' + (t.entryPrice||'?') + ' \\u2192 Fill: $' + fillPrice + NL +\n    'Held: ' + (t.holdMins||0) + 'min | <b>' + (pnlPct>=0?'+':'') + pnlPct + '% | Net: $' + netPnl + '</b>' + NL +\n    'Fees: $' + fees + ' | ' + (t.reason||'TP') + NL +\n    'W/L: ' + (t.wins||0) + 'W/' + (t.losses||0) + 'L (' + (t.winRate||0) + '%)' + NL +\n    btcLine +\n    'Capital: $' + (t.capital||p.capital||'?') + ' | Total P&L: $' + (t.totalPnl||p.totalPnl||0);\n\n} else {\n  const fillPrice = t.realFillPrice || t.exitPrice || '?';\n  const netPnl = t.realNetPnl !== undefined ? t.realNetPnl : (t.netPnl || 0);\n  const pnlPct = t.realPnlPct !== undefined ? t.realPnlPct : (t.pnlPct || 0);\n  const fees   = t.realFees   !== undefined ? t.realFees   : (t.fees   || 0);\n  msg = '\\uD83D\\uDED1 <b>STOP LOSS: ' + (t.symbol||'?') + '</b> ' + timeStr + NL +\n    'Entry: $' + (t.entryPrice||'?') + ' \\u2192 Fill: $' + fillPrice + NL +\n    'Held: ' + (t.holdMins||0) + 'min | ' + pnlPct + '% | Net: $' + netPnl + NL +\n    'Fees: $' + fees + ' | ' + (t.reason||'SL') + NL +\n    btcLine +\n    'Capital: $' + (t.capital||p.capital||'?') + ' | Total P&L: $' + (t.totalPnl||p.totalPnl||0);\n}\n\nif (!msg) return [];\nreturn [{ json: { message: msg, action: a, trade: t } }];"
      },
      "id": "4c46a249-ffa7-46cc-8b6a-303cf9dc820b",
      "name": "Format Trade Message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        11728,
        -880
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.telegram.org/bot7024024817:AAEACrmkruvIdoxZnusEaFi_k1thD5RwV1I/sendMessage",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "chat_id",
              "value": "-1003626322463"
            },
            {
              "name": "text",
              "value": "={{ $json.message }}"
            },
            {
              "name": "parse_mode",
              "value": "HTML"
            }
          ]
        },
        "options": {}
      },
      "id": "39a5b974-2d82-4aaa-9611-fe7d82bd5da4",
      "name": "Send Telegram Alert",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        11952,
        -880
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "// Sign the Binance account balance URL — pure-JS HMAC (crypto blocked in n8n sandbox)\nconst API_KEY    = 'kFYcAx3hJjlAtxbtSjl0DuY1s6zIXLm0YNnwhEqueWMkHvOEAkyBfAAGGfJTfoJS';\nconst SECRET_KEY = 'x7Em12CzxC7bTWmCUah7W6Q8DAFiZgXkmzRRLJnxnaJyAdmQtcCkU4Spun7QjYMt';\n\nfunction sha256(msgBytes) {\n  const K = [0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2];\n  let h = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];\n  const ml = msgBytes.length * 8;\n  msgBytes.push(0x80);\n  while (msgBytes.length % 64 !== 56) msgBytes.push(0);\n  for (let i = 56; i >= 0; i -= 8) msgBytes.push((ml / Math.pow(2, i)) & 0xff);\n  for (let i = 0; i < msgBytes.length; i += 64) {\n    const w = [];\n    for (let j = 0; j < 16; j++) w[j] = (msgBytes[i+j*4]<<24)|(msgBytes[i+j*4+1]<<16)|(msgBytes[i+j*4+2]<<8)|msgBytes[i+j*4+3];\n    for (let j = 16; j < 64; j++) {\n      const s0 = (w[j-15]>>>7|w[j-15]<<25)^(w[j-15]>>>18|w[j-15]<<14)^(w[j-15]>>>3);\n      const s1 = (w[j-2]>>>17|w[j-2]<<15)^(w[j-2]>>>19|w[j-2]<<13)^(w[j-2]>>>10);\n      w[j] = (w[j-16]+s0+w[j-7]+s1) >>> 0;\n    }\n    let [a,b,c,d,e,f,g,hh] = h;\n    for (let j = 0; j < 64; j++) {\n      const S1 = (e>>>6|e<<26)^(e>>>11|e<<21)^(e>>>25|e<<7);\n      const ch = (e&f)^(~e&g);\n      const t1 = (hh+S1+ch+K[j]+w[j]) >>> 0;\n      const S0 = (a>>>2|a<<30)^(a>>>13|a<<19)^(a>>>22|a<<10);\n      const maj = (a&b)^(a&c)^(b&c);\n      const t2 = (S0+maj) >>> 0;\n      hh=g; g=f; f=e; e=(d+t1)>>>0; d=c; c=b; b=a; a=(t1+t2)>>>0;\n    }\n    h = h.map((v,i)=>[a,b,c,d,e,f,g,hh][i]+v>>>0);\n  }\n  return h.map(v=>v.toString(16).padStart(8,'0')).join('');\n}\nfunction strToBytes(s) { return Array.from(s).map(c => c.charCodeAt(0) & 0xff); }\nfunction hmacSha256(key, msg) {\n  let k = strToBytes(key);\n  const blockSize = 64;\n  if (k.length > blockSize) { const h = sha256([...k]); k = h.match(/../g).map(x => parseInt(x,16)); }\n  while (k.length < blockSize) k.push(0);\n  const ipad = k.map(b => b ^ 0x36);\n  const opad = k.map(b => b ^ 0x5c);\n  const innerBytes = sha256([...ipad, ...strToBytes(msg)]).match(/../g).map(x => parseInt(x,16));\n  return sha256([...opad, ...innerBytes]);\n}\n\nconst ts  = Date.now();\nconst qs  = 'recvWindow=5000&timestamp=' + ts;\nconst sig = hmacSha256(SECRET_KEY, qs);\nconst url = 'https://api.binance.com/api/v3/account?' + qs + '&signature=' + sig;\n\n// Pass the full Decision Engine output through so Build Order Request can read it\nconst dec = $input.first().json;\nreturn [{ json: { url, apiKey: API_KEY, decision: dec, action: dec.action, trade: dec.trade } }];"
      },
      "id": "b5587571-66ed-4601-9eff-acb8a53d0ace",
      "name": "Sign Balance URL",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        10384,
        -880
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-MBX-APIKEY",
              "value": "={{ $json.apiKey }}"
            }
          ]
        },
        "options": {
          "timeout": 8000
        }
      },
      "id": "bff22ae8-9c32-4492-9bb1-bdc51526e45c",
      "name": "Fetch Real Balance",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        10592,
        -880
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "url": "=https://api.binance.com/api/v3/exchangeInfo?symbol={{ $('Sign Balance URL').first().json.trade.symbol || 'BTCUSDT' }}",
        "options": {
          "timeout": 5000
        }
      },
      "id": "a1ca06b0-846b-414f-a3c2-bc674d591473",
      "name": "Fetch Exchange Info",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        10832,
        -880
      ],
      "continueOnFail": true
    },
    {
      "parameters": {},
      "id": "c0db6597-6a2f-4572-951e-d198a9b728a7",
      "name": "Hold — No Action",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        10384,
        -640
      ]
    },
    {
      "parameters": {
        "jsCode": "// ============================================================\n// FORMAT HOLD STATUS — fires every cycle when not trading\n// Toggle SEND_STATUS to enable/disable Telegram status messages\n// ============================================================\n\nconst SEND_STATUS = false;  // silent — Telegram only on ENTRY/TP/SL/SWITCH\n\nconst sd  = $getWorkflowStaticData('global');\nconst dec = $('Decision Engine').first().json;\nconst btc = dec.btcSignal || {};\nconst pos = sd.position;\nconst NL  = '\\n';\n\nlet posLine = 'Position: none';\nif (pos) {\n  const heldMins = pos.entryTime\n    ? +((Date.now() - new Date(pos.entryTime).getTime()) / 60000).toFixed(1)\n    : 0;\n  const heldPrice = dec.bestCandidate && dec.bestCandidate.symbol === pos.symbol\n    ? (dec.bestCandidate.price || pos.entry)\n    : pos.entry;\n  const pct = pos.entry > 0 ? +((heldPrice - pos.entry) / pos.entry * 100).toFixed(2) : 0;\n  posLine = 'Pos: ' + pos.symbol + ' ' + (pct >= 0 ? '+' : '') + pct + '% | ' + heldMins + 'min';\n}\n\nconst best = dec.bestCandidate || {};\nconst checks = dec.entryChecks || {};\nconst checksLine = checks.pass !== undefined\n  ? 'Checks: ' + checks.pass + '/5 RSI:' + (checks.rsiOk ? '\\u2713' : '\\u2717') +\n    ' EMA:' + (checks.emaTrendOk ? '\\u2713' : '\\u2717') +\n    ' Slope:' + (checks.slopeOk ? '\\u2713' : '\\u2717') +\n    ' Vol:' + (checks.volOk ? '\\u2713' : '\\u2717') +\n    ' Wick:' + (checks.noReversal ? '\\u2713' : '\\u2717')\n  : 'Checks: no kline data';\n\nconst scan = dec.scanSummary || [];\nconst scanLine = scan.length > 0\n  ? 'Top3: ' + scan.slice(0,3).map(c => c.symbol.replace('USDT','') + ' ' + (c.score||0) + 'pt').join(' | ')\n  : 'Scan: 0 candidates';\n\nconst holdReasons = (dec.trade && dec.trade.holdReasons) ? dec.trade.holdReasons : [];\nconst holdLine = holdReasons.length > 0 ? '\\u26D4 Skip: ' + holdReasons.join(', ') : '';\n\nconst now3 = new Date(Date.now() + 3 * 3600000);\nconst msg = '\\u23F1 <b>SCAN</b> ' + now3.toISOString().slice(11, 16) + ' UTC+3' + NL +\n  'Capital: $' + (+(sd.capital||0).toFixed(2)) + ' | Today: ' + (sd.todayTrades||0) + ' trades' + NL +\n  posLine + NL +\n  'BTC: ' + (btc.trend||'?').toUpperCase() + ' score:' + (btc.score||0) + ' RSI:' + (btc.rsi||0) + ' ROC5:' + (btc.roc5||0) + '%' + NL +\n  'Best: ' + (best.symbol||'none') + ' score:' + (best.score||0) + ' +' + (+(best.change24h||0)).toFixed(2) + '%' + NL +\n  scanLine + NL +\n  checksLine + NL +\n  holdLine;\n\nreturn [{ json: { message: msg, sendMessage: SEND_STATUS } }];"
      },
      "id": "69026a6e-78fd-4a78-8172-aa45b30ea460",
      "name": "Format Hold Status",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        10608,
        -640
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "id": "cond-send-status",
              "leftValue": "={{ $json.sendMessage }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "692ef1c6-03a4-4f19-b4e7-5a93b992511f",
      "name": "Should Send Status?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        10832,
        -640
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.telegram.org/bot7024024817:AAEACrmkruvIdoxZnusEaFi_k1thD5RwV1I/sendMessage",
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "chat_id",
              "value": "-1003626322463"
            },
            {
              "name": "text",
              "value": "={{ $json.message }}"
            },
            {
              "name": "parse_mode",
              "value": "HTML"
            }
          ]
        },
        "options": {}
      },
      "id": "9db7b00f-ff85-4afe-aa0a-af6354ae4454",
      "name": "Send Status Telegram",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        11104,
        -656
      ]
    },
    {
      "parameters": {},
      "id": "f7f30528-8886-496c-a984-723a82c00fd5",
      "name": "Hold Final",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        11120,
        -448
      ]
    }
  ],
  "pinData": {},
  "connections": {
    "Every 1 Minute": {
      "main": [
        [
          {
            "node": "Fetch All Tickers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch All Tickers": {
      "main": [
        [
          {
            "node": "Pick Best Candidate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick Best Candidate": {
      "main": [
        [
          {
            "node": "Fetch BTC Klines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch BTC Klines": {
      "main": [
        [
          {
            "node": "Analyze BTC Trend",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze BTC Trend": {
      "main": [
        [
          {
            "node": "Fetch Klines for Winner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Klines for Winner": {
      "main": [
        [
          {
            "node": "Decision Engine",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Decision Engine": {
      "main": [
        [
          {
            "node": "Has Trade Action?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Trade Action?": {
      "main": [
        [
          {
            "node": "Sign Balance URL",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Hold — No Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sign Balance URL": {
      "main": [
        [
          {
            "node": "Fetch Real Balance",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Real Balance": {
      "main": [
        [
          {
            "node": "Fetch Exchange Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Exchange Info": {
      "main": [
        [
          {
            "node": "Build Order Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Order Request": {
      "main": [
        [
          {
            "node": "Place Binance Order",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Place Binance Order": {
      "main": [
        [
          {
            "node": "Process Order Fill",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Order Fill": {
      "main": [
        [
          {
            "node": "Format Trade Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Trade Message": {
      "main": [
        [
          {
            "node": "Send Telegram Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hold — No Action": {
      "main": [
        [
          {
            "node": "Format Hold Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Hold Status": {
      "main": [
        [
          {
            "node": "Should Send Status?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should Send Status?": {
      "main": [
        [
          {
            "node": "Send Status Telegram",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Hold Final",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate"
  },
  "versionId": "961c1a7c-9f71-4c68-8d2a-d70e3399411e",
  "meta": {
    "instanceId": "e09d1f0f8b9f78e80ac70b6dc8726dd263b0b6ffc4300b0eee3b58f6da316f29"
  },
  "id": "Q448jqONEWufSf8N",
  "tags": []
}