// ============================================================ // PICK BEST CANDIDATE — scores from ticker data only // ============================================================ const MIN_VOLUME_USDT = 200000; const MIN_CHANGE_PCT = 0.3; const MAX_CHANGE_PCT = 50.0; const MIN_PRICE = 0.00001; const EXCLUDE = new Set([ 'USDC','BUSD','TUSD','DAI','FDUSD','USDP','USDS','USDD','GUSD','FRAX', 'USTC','USDN','LUSD','SUSD','CUSD','ZUSD','EURS','EURT','GYEN','BIDR', 'BVND','IDRT','USDJ','UST','BTC','ETH','BNB' ]); // Handle both: single item with full array, and n8n splitting array into multiple items const allItems = $input.all(); let tickers; if (allItems.length > 1) { // n8n split the HTTP response array into individual items tickers = allItems.map(item => item.json).filter(t => t && t.symbol); } else { const raw = allItems[0].json; if (Array.isArray(raw)) { tickers = raw; } else { const vals = Object.values(raw); tickers = (vals.length > 0 && vals[0] && typeof vals[0] === 'object' && vals[0].symbol) ? vals : []; } } const sd = $getWorkflowStaticData('global'); const heldSymbol = sd.position ? sd.position.symbol : null; let heldCurrentPrice = sd.position ? sd.position.entry : 0; const candidates = []; for (const t of tickers) { const symbol = t.symbol || ''; const change24h = parseFloat(t.priceChangePercent || 0); const volume = parseFloat(t.quoteVolume || 0); const price = parseFloat(t.lastPrice || 0); const high24h = parseFloat(t.highPrice || price); const low24h = parseFloat(t.lowPrice || price); if (symbol === heldSymbol) heldCurrentPrice = price; // always capture current price of held coin if (!symbol.endsWith('USDT')) continue; const base = symbol.replace('USDT', ''); let excluded = false; for (const ex of EXCLUDE) { if (base === ex || base.startsWith(ex) || base.endsWith(ex)) { excluded = true; break; } } if (excluded) continue; if (!/^[A-Z0-9]+$/.test(base)) continue; // reject non-ASCII/unicode symbols (scam tokens) if (price < MIN_PRICE || volume < MIN_VOLUME_USDT) continue; if (change24h < MIN_CHANGE_PCT || change24h > MAX_CHANGE_PCT) continue; const ch = Math.abs(change24h); let stageScore = 0; if (ch >= 3 && ch < 8) stageScore = 25; else if (ch >= 8 && ch < 15) stageScore = 20; else if (ch >= 15 && ch < 25) stageScore = 12; else if (ch >= 25 && ch < 40) stageScore = 5; else if (ch >= 0.5) stageScore = 8; const range = high24h - low24h; const rangePct = range > 0 ? (price - low24h) / range : 0.5; let rangeScore = 0; if (rangePct < 0.40) rangeScore = 15; else if (rangePct < 0.60) rangeScore = 12; else if (rangePct < 0.75) rangeScore = 6; else if (rangePct < 0.90) rangeScore = 2; let volScore = 0; if (volume >= 10000000) volScore = 10; else if (volume >= 5000000) volScore = 8; else if (volume >= 2000000) volScore = 6; else if (volume >= 500000) volScore = 3; const score = stageScore + rangeScore + volScore; const preScore = score * Math.log10(Math.max(volume, 1)); candidates.push({ symbol, change24h, volume, price, high24h, low24h, rangePct: +rangePct.toFixed(3), score, preScore, scoringDetail: { stageScore, rangeScore, volScore, change24h, rangePct: +rangePct.toFixed(3) } }); } candidates.sort((a, b) => b.preScore - a.preScore); const scanSummary = candidates.slice(0, 8).map(c => ({ symbol: c.symbol, score: c.score, change24h: c.change24h, rangePct: c.rangePct, price: c.price })); if (candidates.length === 0) { return [{ json: { bestSymbol: heldSymbol || 'BTCUSDT', noCandidates: true, heldSymbol, heldCurrentPrice, scanSummary: [] }}]; } const best = candidates[0]; const roomToHigh = best.high24h > best.price ? (best.high24h - best.price) / best.price : 0.02; const tpPct = Math.max(Math.min(roomToHigh * 0.6, 0.05), 0.015); const slPct = 0.01; return [{ json: { bestSymbol: best.symbol, bestCandidate: best, heldSymbol, heldCurrentPrice, tpPct: +tpPct.toFixed(4), slPct: +slPct.toFixed(4), noCandidates: false, scanSummary }}];