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
- Always use
readCandleCache()first — REST is fallback only - Always call
logSignal()— HyperOpt requires every signal - Guard SHORTs with
isPumpActive()— Don’t short into pumps - Check
position_registry— Don’t stack opposing positions - Handle errors per-coin — One bad symbol shouldn’t kill the scan
- Use
setInterval(scan, INTERVAL)— Don’t use while(true)