Skill: cedarscy/crypto-wave-scanner (version 1.0.0)

--- name: crypto-wave-scanner description: Visual crypto futures wave scanner that detects optimal entry and exit points across 10 coins in real time. Scores each coin 0-6 using EMA9/21/50 stack, RSI momentum, volume confirmation, 1h trend, and MACD. Launches a live browser dashboard with candlestick charts, EMA overlays, RSI subplots, entry limit price, TP targets, and stop levels. Also includes a CLI scanner for terminal output. Use when trading crypto futures and needing to identify which coins are starting a wave (enter) vs exhausting a wave (exit). Supports Binance Futures public API — no authentication required for scanning. --- # Crypto Wave Scanner Detects crypto futures wave entries and exits using a 6-signal scoring system. Outputs a live browser dashboard (candlestick charts + indicators) and a CLI scanner. ## Quick Start ### Browser Dashboard (recommended) ```bash # Start the local server python3 scripts/serve.py # Opens automatically at http://localhost:7890/wave-scanner.html ``` Dashboard auto-refreshes every 60 seconds. Filter by timeframe (5m/15m/1h/4h) and minimum score. ### CLI Scanner ```bash python3 scripts/wave_scanner.py python3 scripts/wave_scanner.py --min-score 5 # strong setups only python3 scripts/wave_scanner.py --symbols BTC ETH SOL # specific coins ``` ## Scoring System (0-6) Each coin is scored on 6 signals: | # | Signal | Bullish Condition | |---|--------|-------------------| | 1 | EMA Stack | EMA9 > EMA21 > EMA50 | | 2 | RSI Zone | RSI 50–80 (sweet spot) | | 3 | RSI Direction | RSI rising vs previous candle | | 4 | Volume | Last candle volume > 1.3× 10-candle avg | | 5 | 1H Trend | EMA9 > EMA21 on 1h chart | | 6 | MACD | MACD histogram positive | **Grades:** 5-6 = 🔥 STRONG SETUP · 4 = ✅ VALID · 3 = ⚠️ WEAK · <3 = ❌ SKIP ## Entry / Exit Rules **Enter when:** Score ≥ 5 AND RSI 50-75. Place LIMIT order at EMA9 level (shown in dashboard). **Skip when:** RSI > 85 (wave mature). RSI > 88 = do not add longs. **Exit when (any one):** - EMA9 crosses below EMA21 - RSI > 85 AND price closes below EMA9 - Volume drops for 3 consecutive candles on new highs - Large bearish engulfing candle ## Assets - `assets/wave-scanner.html` — self-contained browser dashboard (TradingView Lightweight Charts) - `scripts/wave_scanner.py` — CLI scanner with full signal breakdown - `scripts/serve.py` — local HTTP server launcher ## Data Source Uses public Binance Futures API (`https://fapi.binance.com`) — no API key needed for scanning. For testnet trading with the Binance Futures Testnet, swap base URL to `https://testnet.binancefuture.com`. ## Coins Scanned by Default BTCUSDT, ETHUSDT, SOLUSDT, XRPUSDT, BNBUSDT, AVAXUSDT, LINKUSDT, DOGEUSDT, LTCUSDT, DOTUSDT

path: assets/wave-scanner.html
content: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>🌊 Cedars Wave Scanner</title>
<script src="https://unpkg.com/lightweight-charts@4.1.3/dist/lightweight-charts.standalone.production.js"></script>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { background: #0a0e1a; color: #e0e6f0; font-family: 'Segoe UI', monospace; }

  header {
    background: #0d1526;
    border-bottom: 1px solid #1e3050;
    padding: 14px 24px;
    display: flex; align-items: center; justify-content: space-between;
  }
  header h1 { font-size: 20px; color: #4fc3f7; letter-spacing: 1px; }
  header .meta { font-size: 12px; color: #607d9b; }

  #controls {
    padding: 12px 24px;
    background: #0d1526;
    border-bottom: 1px solid #1a2d45;
    display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
  }
  select, button {
    background: #132030; color: #c0d4e8;
    border: 1px solid #1e3a5f; border-radius: 6px;
    padding: 6px 14px; font-size: 13px; cursor: pointer;
  }
  button { background: #1565c0; color: #fff; font-weight: bold; border: none; }
  button:hover { background: #1976d2; }
  button.danger { background: #c62828; }
  #status { font-size: 12px; color: #78909c; padding: 6px 0; }

  #summary-bar {
    display: flex; gap: 12px; padding: 10px 24px;
    background: #0b1220; border-bottom: 1px solid #1a2d45; flex-wrap: wrap;
  }
  .sum-badge {
    padding: 4px 14px; border-radius: 20px; font-size: 12px; font-weight: bold;
  }
  .badge-fire  { background: #4a1a00; color: #ff6d00; border: 1px solid #e65100; }
  .badge-ok    { background: #003300; color: #69f0ae; border: 1px solid #00c853; }
  .badge-warn  { background: #3a2800; color: #ffd740; border: 1px solid #ffab00; }
  .badge-skip  { background: #1a1a2e; color: #546e7a; border: 1px solid #37474f; }

  #grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(480px, 1fr));
    gap: 16px; padding: 16px 24px;
  }

  .coin-card {
    background: #0d1526;
    border: 1px solid #1e3050;
    border-radius: 10px;
    overflow: hidden;
    transition: border-color 0.2s;
  }
  .coin-card.fire  { border-color: #e65100; }
  .coin-card.valid { border-color: #00c853; }
  .coin-card.weak  { border-color: #ffab00; }
  .coin-card.skip  { border-color: #37474f; opacity: 0.65; }

  .card-header {
    padding: 10px 14px;
    display: flex; align-items: center; justify-content: space-between;
    border-bottom: 1px solid #1a2d45;
  }
  .card-header .sym { font-size: 15px; font-weight: bold; color: #e0e6f0; }
  .card-header .price { font-size: 14px; color: #90caf9; font-family: monospace; }
  .grade-badge {
    padding: 3px 10px; border-radius: 12px; font-size: 11px; font-weight: bold;
  }
  .grade-fire  { background:#4a1a00; color:#ff6d00; }
  .grade-valid { background:#003300; color:#69f0ae; }
  .grade-weak  { background:#3a2800; color:#ffd740; }
  .grade-skip  { background:#1a1a2e; color:#546e7a; }

  .chart-wrap { position: relative; height: 200px; }
  .rsi-wrap   { position: relative; height: 70px; border-top: 1px solid #1a2d45; }

  .signals-row {
    display: flex; flex-wrap: wrap; gap: 4px; padding: 8px 14px;
    border-top: 1px solid #1a2d45;
  }
  .sig {
    font-size: 10px; padding: 2px 8px; border-radius: 10px;
  }
  .sig.ok   { background: #003300; color: #69f0ae; }
  .sig.fail { background: #1a0000; color: #ef9a9a; }
  .sig.warn { background: #3a2800; color: #ffd740; }

  .entry-info {
    padding: 8px 14px; font-size: 11px; color: #78909c;
    border-top: 1px solid #1a2d45;
    display: flex; gap: 16px; flex-wrap: wrap;
  }
  .entry-info span b { color: #4fc3f7; }

  .score-bar {
    height: 3px;
    background: #1a2d45;
  }
  .score-fill {
    height: 3px;
    transition: width 0.5s;
  }

  #refresh-timer { color: #546e7a; font-size: 11px; }
</style>
</head>
<body>

<header>
  <h1>🌊 Cedars Wave Scanner</h1>
  <div class="meta">
    <span id="refresh-timer">Refreshing...</span> &nbsp;|&nbsp;
    Binance Futures · 15m · <span id="clock"></span>
  </div>
</header>

<div id="controls">
  <select id="sel-interval">
    <option value="5m">5m</option>
    <option value="15m" selected>15m</option>
    <option value="1h">1h</option>
    <option value="4h">4h</option>
  </select>
  <select id="sel-limit">
    <option value="60" selected>60 candles</option>
    <option value="100">100 candles</option>
    <option value="200">200 candles</option>
  </select>
  <select id="sel-minscore">
    <option value="0">Show All</option>
    <option value="3">Score ≥ 3</option>
    <option value="4" selected>Score ≥ 4</option>
    <option value="5">Score ≥ 5 (STRONG only)</option>
  </select>
  <button onclick="refresh()">⟳ Refresh Now</button>
  <span id="status">Loading...</span>
</div>

<div id="summary-bar"></div>
<div id="grid"></div>

<script>
const SYMBOLS = [
  "BTCUSDT","ETHUSDT","SOLUSDT","XRPUSDT",
  "BNBUSDT","AVAXUSDT","LINKUSDT","DOGEUSDT",
  "LTCUSDT","DOTUSDT"
];
const BASE = "https://fapi.binance.com"; // real Binance Futures (public data, no auth)

let autoTimer = null;
let countdown = 60;

function clock() {
  document.getElementById("clock").textContent = new Date().toLocaleTimeString();
}
setInterval(clock, 1000); clock();

function setStatus(msg) { document.getElementById("status").textContent = msg; }

async function fetchKlines(sym, interval, limit) {
  const url = `${BASE}/fapi/v1/klines?symbol=${sym}&interval=${interval}&limit=${limit}`;
  const r = await fetch(url);
  return await r.json();
}

function calcEMA(data, period) {
  const k = 2 / (period + 1);
  let e = data[0];
  for (let i = 1; i < data.length; i++) e = data[i] * k + e * (1 - k);
  return e;
}

function calcEMASeries(data, period) {
  const k = 2 / (period + 1);
  const result = [];
  let e = data[0];
  result.push(e);
  for (let i = 1; i < data.length; i++) {
    e = data[i] * k + e * (1 - k);
    result.push(e);
  }
  return result;
}

function calcRSI(closes, period = 14) {
  if (closes.length < period + 1) return 50;
  let gains = 0, losses = 0;
  for (let i = closes.length - period; i < closes.length; i++) {
    const d = closes[i] - closes[i - 1];
    if (d > 0) gains += d; else losses += Math.abs(d);
  }
  if (losses === 0) return 100;
  const rs = (gains / period) / (losses / period);
  return parseFloat((100 - 100 / (1 + rs)).toFixed(1));
}

function calcRSISeries(closes, period = 14) {
  const result = [];
  for (let i = period; i < closes.length; i++) {
    const slice = closes.slice(i - period, i + 1);
    result.push({ idx: i, rsi: calcRSI(slice, period) });
  }
  return result;
}

function scoreSignal(sym, klines) {
  const closes = klines.map(k => parseFloat(k[4]));
  const vols   = klines.map(k => parseFloat(k[5]));
  const times  = klines.map(k => k[0]);

  const e9  = calcEMA(closes.slice(-20), 9);
  const e21 = calcEMA(closes.slice(-30), 21);
  const e50 = calcEMA(closes.slice(-60), 50);

  const rsiNow  = calcRSI(closes.slice(-16));
  const rsiPrev = calcRSI(closes.slice(-17, -1));
  const rsiRising = rsiNow > rsiPrev;

  const lastVol = vols[vols.length - 1];
  const avgVol  = vols.slice(-11, -1).reduce((a, b) => a + b, 0) / 10;
  const volRatio = (lastVol / avgVol).toFixed(2);

  // 1h EMA trend (approximate from last klines)
  const e9_1h  = calcEMA(closes.slice(-20), 9);
  const e21_1h = calcEMA(closes.slice(-30), 21);
  const trend1h = e9_1h > e21_1h; // same TF here; caller should pass 1h data

  const e12 = calcEMA(closes.slice(-30), 12);
  const e26 = calcEMA(closes.slice(-40), 26);
  const macd = e12 - e26;

  let score = 0;
  const sigs = [];

  // 1. EMA stack
  if (e9 > e21 && e21 > e50) { score++; sigs.push({ label: "EMA9>21>50", ok: true }); }
  else sigs.push({ label: "EMA Stack", ok: false });

  // 2. RSI zone
  if (rsiNow >= 50 && rsiNow <= 80) { score++; sigs.push({ label: `RSI ${rsiNow} ✓`, ok: true }); }
  else if (rsiNow > 80) sigs.push({ label: `RSI ${rsiNow} ⚠`, ok: "warn" });
  else sigs.push({ label: `RSI ${rsiNow}`, ok: false });

  // 3. RSI rising
  if (rsiRising) { score++; sigs.push({ label: "RSI↑ Rising", ok: true }); }
  else sigs.push({ label: "RSI↓ Falling", ok: false });

  // 4. Volume
  if (volRatio >= 1.3) { score++; sigs.push({ label: `Vol ${volRatio}×`, ok: true }); }
  else sigs.push({ label: `Vol ${volRatio}×`, ok: false });

  // 5. Trend (1h approximation — EMA9 > EMA21)
  if (trend1h) { score++; sigs.push({ label: "Trend BULL", ok: true }); }
  else sigs.push({ label: "Trend BEAR", ok: false });

  // 6. MACD
  if (macd > 0) { score++; sigs.push({ label: `MACD+`, ok: true }); }
  else sigs.push({ label: `MACD-`, ok: false });

  let grade, gradeClass;
  if (score >= 5)      { grade = "🔥 STRONG"; gradeClass = "fire"; }
  else if (score >= 4) { grade = "✅ VALID";  gradeClass = "valid"; }
  else if (score >= 3) { grade = "⚠️ WEAK";  gradeClass = "weak"; }
  else                 { grade = "❌ SKIP";   gradeClass = "skip"; }

  const price = closes[closes.length - 1];
  const tp3 = (price * 1.03).toFixed(4);
  const tp5 = (price * 1.05).toFixed(4);
  const entryLimit = e9.toFixed(4);
  const stop = e21.toFixed(4);

  return {
    sym, score, grade, gradeClass, sigs,
    price, rsiNow, rsiPrev, rsiRising,
    e9, e21, e50, macd: macd.toFixed(4), volRatio,
    entryLimit, tp3, tp5, stop,
    closes, vols, times, klines
  };
}

function buildCard(d) {
  const cardId = `card-${d.sym}`;
  const chartId = `chart-${d.sym}`;
  const rsiId   = `rsi-${d.sym}`;

  const scoreColor = d.gradeClass === 'fire' ? '#ff6d00'
    : d.gradeClass === 'valid' ? '#69f0ae'
    : d.gradeClass === 'weak'  ? '#ffd740' : '#546e7a';
  const scorePct = (d.score / 6 * 100).toFixed(0);

  const sigsHTML = d.sigs.map(s => {
    const cls = s.ok === true ? 'ok' : s.ok === 'warn' ? 'warn' : 'fail';
    return `<span class="sig ${cls}">${s.label}</span>`;
  }).join('');

  return `
  <div class="coin-card ${d.gradeClass}" id="${cardId}">
    <div class="score-bar"><div class="score-fill" style="width:${scorePct}%;background:${scoreColor}"></div></div>
    <div class="card-header">
      <div>
        <span class="sym">${d.sym.replace('USDT','')}</span>
        <span class="grade-badge grade-${d.gradeClass}" style="margin-left:8px">${d.grade} ${d.score}/6</span>
      </div>
      <span class="price">$${parseFloat(d.price).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:4})}</span>
    </div>
    <div class="chart-wrap" id="${chartId}"></div>
    <div class="rsi-wrap" id="${rsiId}"></div>
    <div class="signals-row">${sigsHTML}</div>
    <div class="entry-info">
      <span>📍 Limit: <b>$${d.entryLimit}</b></span>
      <span>🎯 TP3%: <b>$${d.tp3}</b></span>
      <span>🎯 TP5%: <b>$${d.tp5}</b></span>
      <span>🛑 Stop: <b>$${d.stop}</b></span>
      <span>RSI: <b>${d.rsiNow}</b> ${d.rsiRising ? '↑' : '↓'}</span>
      <span>Vol: <b>${d.volRatio}×</b></span>
    </div>
  </div>`;
}

function renderCharts(d) {
  const chartEl = document.getElementById(`chart-${d.sym}`);
  const rsiEl   = document.getElementById(`rsi-${d.sym}`);
  if (!chartEl || !rsiEl) return;

  // Main chart
  const chart = LightweightCharts.createChart(chartEl, {
    width: chartEl.clientWidth, height: 200,
    layout: { background: { color: '#0d1526' }, textColor: '#607d9b' },
    grid: { vertLines: { color: '#1a2d45' }, horzLines: { color: '#1a2d45' } },
    timeScale: { borderColor: '#1a2d45', timeVisible: true },
    rightPriceScale: { borderColor: '#1a2d45' },
    crosshair: { mode: 1 },
    handleScroll: false, handleScale: false,
  });

  // Candlesticks
  const candleSeries = chart.addCandlestickSeries({
    upColor: '#26a69a', downColor: '#ef5350',
    borderUpColor: '#26a69a', borderDownColor: '#ef5350',
    wickUpColor: '#26a69a', wickDownColor: '#ef5350',
  });

  const candles = d.klines.slice(-60).map(k => ({
    time: Math.floor(k[0] / 1000),
    open: parseFloat(k[1]), high: parseFloat(k[2]),
    low: parseFloat(k[3]),  close: parseFloat(k[4]),
  }));
  candleSeries.setData(candles);

  // EMA lines
  const closes = d.klines.slice(-60).map(k => parseFloat(k[4]));
  const times  = d.klines.slice(-60).map(k => Math.floor(k[0] / 1000));

  const e9series  = calcEMASeries(closes, 9);
  const e21series = calcEMASeries(closes, 21);
  const e50series = calcEMASeries(closes, 50);

  const ema9line = chart.addLineSeries({ color: '#ff9800', lineWidth: 1, title: 'EMA9' });
  const ema21line = chart.addLineSeries({ color: '#2196f3', lineWidth: 1, title: 'EMA21' });
  const ema50line = chart.addLineSeries({ color: '#9c27b0', lineWidth: 1, title: 'EMA50' });

  ema9line.setData(e9series.map((v, i) => ({ time: times[i], value: v })));
  ema21line.setData(e21series.map((v, i) => ({ time: times[i], value: v })));
  ema50line.setData(e50series.map((v, i) => ({ time: times[i], value: v })));

  // Entry zone line (EMA9 level — horizontal)
  const entryLine = chart.addLineSeries({
    color: '#ffd740', lineWidth: 1, lineStyle: 2, title: 'Entry'
  });
  entryLine.setData(times.slice(-5).map(t => ({ time: t, value: d.e9 })));

  chart.timeScale().fitContent();

  // RSI chart
  const rsiChart = LightweightCharts.createChart(rsiEl, {
    width: rsiEl.clientWidth, height: 70,
    layout: { background: { color: '#0a0e1a' }, textColor: '#546e7a' },
    grid: { vertLines: { color: '#1a2d45' }, horzLines: { color: '#1a2d45' } },
    timeScale: { borderColor: '#1a2d45', timeVisible: false, visible: false },
    rightPriceScale: { borderColor: '#1a2d45', scaleMargins: { top: 0.1, bottom: 0.1 } },
    handleScroll: false, handleScale: false,
  });

  const rsiSeries = rsiChart.addLineSeries({ color: '#e040fb', lineWidth: 1, title: 'RSI' });
  const rsiData = calcRSISeries(closes, 14).map(r => ({ time: times[r.idx], value: r.rsi }));
  rsiSeries.setData(rsiData);

  // RSI levels
  const rsi70 = rsiChart.addLineSeries({ color: '#ef5350', lineWidth: 1, lineStyle: 2 });
  const rsi50 = rsiChart.addLineSeries({ color: '#78909c', lineWidth: 1, lineStyle: 2 });
  const rsi30 = rsiChart.addLineSeries({ color: '#26a69a', lineWidth: 1, lineStyle: 2 });

  const tRange = times.slice(-20);
  rsi70.setData(tRange.map(t => ({ time: t, value: 70 })));
  rsi50.setData(tRange.map(t => ({ time: t, value: 50 })));
  rsi30.setData(tRange.map(t => ({ time: t, value: 30 })));

  rsiChart.timeScale().fitContent();
}

async function refresh() {
  const interval  = document.getElementById("sel-interval").value;
  const limit     = parseInt(document.getElementById("sel-limit").value);
  const minScore  = parseInt(document.getElementById("sel-minscore").value);

  setStatus("🔄 Scanning...");
  document.getElementById("grid").innerHTML = "";
  document.getElementById("summary-bar").innerHTML = "";

  const results = [];

  for (const sym of SYMBOLS) {
    try {
      setStatus(`Fetching ${sym}...`);
      const klines = await fetchKlines(sym, interval, limit);
      const d = scoreSignal(sym, klines);
      results.push(d);
    } catch (e) {
      console.error(sym, e);
    }
  }

  // Sort by score desc
  results.sort((a, b) => b.score - a.score);

  // Summary
  const fire  = results.filter(r => r.score >= 5).length;
  const valid = results.filter(r => r.score === 4).length;
  const weak  = results.filter(r => r.score === 3).length;
  const skip  = results.filter(r => r.score < 3).length;

  document.getElementById("summary-bar").innerHTML = `
    <span class="sum-badge badge-fire">🔥 STRONG: ${fire}</span>
    <span class="sum-badge badge-ok">✅ VALID: ${valid}</span>
    <span class="sum-badge badge-warn">⚠️ WEAK: ${weak}</span>
    <span class="sum-badge badge-skip">❌ SKIP: ${skip}</span>
    <span class="sum-badge" style="background:#0d1526;color:#78909c;border:1px solid #1e3050">
      Best: ${results[0]?.sym.replace('USDT','')} RSI=${results[0]?.rsiNow}
    </span>
  `;

  // Render cards
  const grid = document.getElementById("grid");
  const filtered = results.filter(r => r.score >= minScore);

  if (filtered.length === 0) {
    grid.innerHTML = `<div style="padding:40px;color:#546e7a;text-align:center;grid-column:1/-1">
      No setups found with score ≥ ${minScore}. Lower the minimum score filter.</div>`;
  }

  for (const d of filtered) {
    grid.innerHTML += buildCard(d);
  }

  // Render charts after DOM update
  setTimeout(() => {
    for (const d of filtered) {
      renderCharts(d);
    }
  }, 100);

  setStatus(`✅ Done — ${filtered.length} setups shown · ${new Date().toLocaleTimeString()}`);

  // Auto-refresh countdown
  countdown = 60;
  clearInterval(autoTimer);
  autoTimer = setInterval(() => {
    countdown--;
    document.getElementById("refresh-timer").textContent = `Auto-refresh in ${countdown}s`;
    if (countdown <= 0) {
      clearInterval(autoTimer);
      refresh();
    }
  }, 1000);
}

window.addEventListener("resize", () => {
  // Re-render on resize
  document.querySelectorAll('.chart-wrap, .rsi-wrap').forEach(el => {
    // lightweight-charts auto-resize is handled by the library
  });
});

// Auto-start
refresh();
</script>
</body>
</html>


path: scripts/serve.py
content: """
Crypto Wave Scanner — Local Server
Serves the dashboard at http://localhost:7890/wave-scanner.html
"""
import http.server, socketserver, webbrowser, os, threading, time

PORT = 7890
ASSETS_DIR = os.path.join(os.path.dirname(__file__), '..', 'assets')

class Handler(http.server.SimpleHTTPRequestHandler):
    def log_message(self, format, *args): pass  # silent

def open_browser():
    time.sleep(1.2)
    webbrowser.open(f"http://localhost:{PORT}/wave-scanner.html")

os.chdir(ASSETS_DIR)
threading.Thread(target=open_browser, daemon=True).start()

print(f"🌊 Cedars Wave Scanner running at http://localhost:{PORT}/wave-scanner.html")
print("Press Ctrl+C to stop.")

with socketserver.TCPServer(("", PORT), Handler) as httpd:
    httpd.serve_forever()


path: scripts/wave_scanner.py
content: """
Wave Scanner — Cedars Wave Method
Scans Binance Futures for high-probability wave entry setups
Usage: python3 wave_scanner.py [--alert] [--symbols BTC ETH SOL]
"""
import hmac, hashlib, json, subprocess, sys, argparse
from datetime import datetime

BASE = "https://testnet.binancefuture.com"
SYMBOLS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "BNBUSDT", "AVAXUSDT", "LINKUSDT", "DOGEUSDT"]

def curl_get(path):
    r = subprocess.run(["curl.exe", "-s", BASE + path], capture_output=True)
    return json.loads(r.stdout.decode("utf-8"))

def ema(data, period):
    k = 2 / (period + 1)
    e = data[0]
    for v in data[1:]:
        e = v * k + e * (1 - k)
    return e

def rsi(closes, period=14):
    gains = losses = 0.0
    for i in range(1, period + 1):
        d = closes[i] - closes[i - 1]
        if d > 0: gains += d
        else: losses += abs(d)
    if losses == 0: return 100
    rs = (gains / period) / (losses / period)
    return round(100 - (100 / (1 + rs)), 1)

def avg_volume(klines, n=10):
    vols = [float(k[5]) for k in klines[-n-1:-1]]
    return sum(vols) / len(vols) if vols else 0

def scan_symbol(sym):
    try:
        klines_15m = curl_get(f"/fapi/v1/klines?symbol={sym}&interval=15m&limit=60")
        klines_1h  = curl_get(f"/fapi/v1/klines?symbol={sym}&interval=1h&limit=30")

        closes_15m = [float(k[4]) for k in klines_15m]
        closes_1h  = [float(k[4]) for k in klines_1h]

        price = closes_15m[-1]

        # EMAs on 15m
        e9  = ema(closes_15m[-20:], 9)
        e21 = ema(closes_15m[-30:], 21)
        e50 = ema(closes_15m[-60:], 50)

        # RSI on 15m (last 3 for trend)
        rsi_now  = rsi(closes_15m[-16:])
        rsi_prev = rsi(closes_15m[-17:-1])
        rsi_rising = rsi_now > rsi_prev

        # Volume
        last_vol = float(klines_15m[-1][5])
        avg_vol  = avg_volume(klines_15m, 10)
        vol_ratio = round(last_vol / avg_vol, 2) if avg_vol > 0 else 0

        # 1h trend
        e9_1h  = ema(closes_1h[-15:], 9)
        e21_1h = ema(closes_1h[-25:], 21)
        trend_1h = e9_1h > e21_1h

        # MACD (12/26/9) on 15m
        e12 = ema(closes_15m[-30:], 12)
        e26 = ema(closes_15m[-40:], 26)
        macd = e12 - e26
        macd_positive = macd > 0

        # Scoring — each = 1 point
        score = 0
        signals = []

        # Signal 1: EMA stack
        if e9 > e21 > e50:
            score += 1
            signals.append("EMA9>21>50 ✅")
        else:
            signals.append(f"EMA stack ❌ (9={round(e9,2)} 21={round(e21,2)} 50={round(e50,2)})")

        # Signal 2: RSI zone
        if 50 <= rsi_now <= 80:
            score += 1
            signals.append(f"RSI={rsi_now} ✅ (sweet zone)")
        elif rsi_now < 50:
            signals.append(f"RSI={rsi_now} ❌ (below 50, no momentum)")
        else:
            signals.append(f"RSI={rsi_now} ⚠️ (extended, reduce size)")

        # Signal 3: RSI rising
        if rsi_rising:
            score += 1
            signals.append(f"RSI rising ({rsi_prev}→{rsi_now}) ✅")
        else:
            signals.append(f"RSI falling ({rsi_prev}→{rsi_now}) ❌")

        # Signal 4: Volume
        if vol_ratio >= 1.3:
            score += 1
            signals.append(f"Volume {vol_ratio}x avg ✅")
        else:
            signals.append(f"Volume {vol_ratio}x avg ❌ (need >1.3x)")

        # Signal 5: 1h trend
        if trend_1h:
            score += 1
            signals.append("1h trend BULL ✅")
        else:
            signals.append("1h trend BEAR ❌")

        # Signal 6: MACD
        if macd_positive:
            score += 1
            signals.append(f"MACD positive ({round(macd,2)}) ✅")
        else:
            signals.append(f"MACD negative ({round(macd,2)}) ❌")

        # Entry suggestion
        entry_limit = round(e9, 4)  # limit order at EMA9
        tp_3pct = round(price * 1.03, 4)
        tp_5pct = round(price * 1.05, 4)
        stop = round(e21, 4)

        # Grade
        if score >= 5:   grade = "🔥 STRONG SETUP"
        elif score == 4: grade = "✅ VALID SETUP"
        elif score == 3: grade = "⚠️  WEAK SETUP"
        else:            grade = "❌ SKIP"

        return {
            "symbol": sym,
            "price": price,
            "score": score,
            "grade": grade,
            "rsi": rsi_now,
            "e9": round(e9, 4),
            "e21": round(e21, 4),
            "e50": round(e50, 4),
            "vol_ratio": vol_ratio,
            "macd": round(macd, 4),
            "signals": signals,
            "entry_limit": entry_limit,
            "tp_3pct": tp_3pct,
            "tp_5pct": tp_5pct,
            "stop": stop,
        }
    except Exception as ex:
        return {"symbol": sym, "error": str(ex), "score": 0, "grade": "❌ ERROR"}

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--symbols", nargs="+", default=SYMBOLS)
    parser.add_argument("--min-score", type=int, default=3)
    args = parser.parse_args()

    print(f"\n{'='*60}")
    print(f"  CEDARS WAVE SCANNER — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    print(f"{'='*60}\n")

    results = []
    for sym in args.symbols:
        r = scan_symbol(sym)
        results.append(r)

    # Sort by score
    results.sort(key=lambda x: x.get("score", 0), reverse=True)

    for r in results:
        if r.get("score", 0) < args.min_score:
            continue
        print(f"{'─'*50}")
        print(f"  {r['symbol']} — {r['grade']} ({r['score']}/6)")
        print(f"  Price: {r.get('price')} | RSI: {r.get('rsi')} | Vol: {r.get('vol_ratio')}x")
        print(f"  EMA9={r.get('e9')} EMA21={r.get('e21')} EMA50={r.get('e50')}")
        print(f"  MACD: {r.get('macd')}")
        print(f"  📍 Entry limit: {r.get('entry_limit')}")
        print(f"  🎯 TP (3%): {r.get('tp_3pct')} | TP (5%): {r.get('tp_5pct')}")
        print(f"  🛑 Stop (EMA21): {r.get('stop')}")
        print()
        for sig in r.get("signals", []):
            print(f"     {sig}")
        print()

    # Summary
    hot = [r for r in results if r.get("score", 0) >= 5]
    valid = [r for r in results if r.get("score", 0) == 4]
    print(f"\n{'='*60}")
    print(f"  SUMMARY: {len(hot)} STRONG | {len(valid)} VALID | {len(results)-len(hot)-len(valid)} SKIP")
    if hot:
        print(f"  🔥 BEST: {hot[0]['symbol']} score={hot[0]['score']}/6 RSI={hot[0].get('rsi')}")
    elif valid:
        print(f"  ✅ BEST: {valid[0]['symbol']} score={valid[0]['score']}/6 RSI={valid[0].get('rsi')}")
    print(f"{'='*60}\n")

if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis:  | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)