Skip to the content.

Layer 5: Scanner Template

Copy-paste boilerplate for connecting a new engine to the SKYNET pipeline.


Minimal Scanner

/**
 * my_scanner.mjs
 * Your engine description here
 */

import { readCandleCache } from './core/candle_cache.mjs';
import { calculateRSI } from './core/signals.mjs';
import { bitgetGet } from './core/bitget_rest.mjs';
import { sendTelegram } from './core/telegram_notifier.mjs';
import { logSignal } from './core/signal_logger.mjs';
import { isPumpActive } from './core/pump_guard.mjs';
import { checkPosition, registerPosition } from './core/position_registry.mjs';

const STRATEGY = 'MY_SCANNER';
const SCAN_INTERVAL_MS = 5 * 60 * 1000;  // 5 minutes
const MIN_VOLUME_USD = 1_000_000;

// ── Config ────────────────────────────────────────────────────────────────
const TP_PCT = 0.06;   // 6% take profit
const SL_PCT = 0.03;   // 3% stop loss

// ── Universe ──────────────────────────────────────────────────────────────
async function fetchUniverse() {
  const tickers = await bitgetGet('/api/v2/mix/market/tickers?productType=USDT-FUTURES');
  return tickers
    .filter(t => parseFloat(t.quoteVolume || t.usdtVolume || 0) >= MIN_VOLUME_USD)
    .map(t => ({
      symbol: t.symbol,
      lastPr: parseFloat(t.lastPr),
      high24h: parseFloat(t.high24h),
      low24h: parseFloat(t.low24h),
      quoteVolume: parseFloat(t.quoteVolume || t.usdtVolume || 0),
    }));
}

// ── Scoring ───────────────────────────────────────────────────────────────
function scoreCoin(ticker, candles1H, candles5m) {
  let score = 0;
  const details = [];

  // Example: RSI oversold
  const closes = candles1H?.map(c => c.close) || [];
  if (closes.length >= 14) {
    const rsi = calculateRSI(closes, 14);
    const currentRSI = rsi[rsi.length - 1];
    if (currentRSI < 35) {
      score += 1;
      details.push(`RSI ${currentRSI.toFixed(1)}`);
    }
  }

  return { score, details };
}

// ── Alert ─────────────────────────────────────────────────────────────────
function buildAlert(ticker, score, details) {
  const price = ticker.lastPr;
  const sl = price * (1 - SL_PCT);
  const tp = price * (1 + TP_PCT);

  return `🚀 <b>MY_SCANNER — ${ticker.symbol} LONG</b>\n` +
    `Score: ${score}\n` +
    `Details: ${details.join(', ')}\n` +
    `Entry: $${price.toFixed(4)}\n` +
    `SL: $${sl.toFixed(4)} (-${(SL_PCT*100).toFixed(1)}%)\n` +
    `TP: $${tp.toFixed(4)} (+${(TP_PCT*100).toFixed(1)}%)`;
}

// ── Main Loop ─────────────────────────────────────────────────────────────
async function scan() {
  console.log(`[${STRATEGY}] Starting scan...`);

  const universe = await fetchUniverse();
  console.log(`[${STRATEGY}] Universe: ${universe.length} coins`);

  for (const coin of universe) {
    try {
      // 1. Fetch candles from cache
      const candles1H = readCandleCache(coin.symbol, '1H');
      const candles5m = readCandleCache(coin.symbol, '5m');

      // 2. Score
      const { score, details } = scoreCoin(coin, candles1H, candles5m);
      if (score < 1) continue;

      // 3. Guard against pumps (for SHORT signals only)
      // if (await isPumpActive(coin.symbol)) continue;

      // 4. Check position registry
      if (checkPosition(coin.symbol)) continue;

      // 5. Build alert
      const alertText = buildAlert(coin, score, details);

      // 6. Send Telegram
      await sendTelegram(alertText);

      // 7. Log to HyperOpt
      logSignal({
        engine: STRATEGY.toLowerCase(),
        symbol: coin.symbol,
        side: 'LONG',
        price: coin.lastPr,
        tpPct: TP_PCT,
        slPct: SL_PCT,
        metadata: { score, details }
      });

      // 8. Register position
      registerPosition(coin.symbol, 'LONG');

      console.log(`[${STRATEGY}] ✅ Signal fired: ${coin.symbol}`);

    } catch (err) {
      console.error(`[${STRATEGY}] Error scanning ${coin.symbol}:`, err.message);
    }
  }

  console.log(`[${STRATEGY}] Scan complete`);
}

// ── Run ───────────────────────────────────────────────────────────────────
scan();
setInterval(scan, SCAN_INTERVAL_MS);

Required Steps

1. Save File

# Save to:
/app/trading_engine/scripts/my_scanner.mjs

2. Add to PM2 Ecosystem

Edit /app/trading_engine/ecosystem.config.cjs:

{
  name: 'my-scanner',
  script: 'scripts/my_scanner.mjs',
  cwd: '/app/trading_engine',
  autorestart: true,
  restart_delay: 5000,
  max_memory_restart: '150M',
}

3. Start

pm2 start ecosystem.config.cjs --only my-scanner
pm2 save

4. Verify

pm2 logs my-scanner --lines 20

Standard Pattern

Every SKYNET scanner follows this lifecycle:

graph LR
    A[fetchUniverse] --> B[fetchCandles]
    B --> C[scoreCoin]
    C --> D{score > threshold?}
    D -->|No| E[Skip]
    D -->|Yes| F[checkPosition]
    F --> G[isPumpActive]
    G --> H[buildAlert]
    H --> I[sendTelegram]
    I --> J[logSignal]
    J --> K[registerPosition]

Key Rules

  1. Always use readCandleCache() first — REST is fallback only
  2. Always call logSignal() — HyperOpt requires every signal
  3. Guard SHORTs with isPumpActive() — Don’t short into pumps
  4. Check position_registry — Don’t stack opposing positions
  5. Handle errors per-coin — One bad symbol shouldn’t kill the scan
  6. Use setInterval(scan, INTERVAL) — Don’t use while(true)