Real-time stock pattern scanner that connects to Polygon.io/Massive.com websocket and alerts when patterns are detected. Features professional-grade filters, market context awareness, and a web-based control panel.
- Real-time data: Connects to Polygon/Massive websocket for live market data
- Pattern scanning: Detects various technical patterns in real-time with professional-grade filters
- Web UI: Full-featured control panel for managing tickers, scan frequency, and patterns
- Market context: Tracks SPY/QQQ bias and daily trends for directional alignment
- Universe filters: Liquidity, volatility, and in-play (RVOL) filtering
- Risk management: Centralized 2:1 R:R framework with pattern-specific overrides
- Analytics: Signal outcome tracking for performance evaluation
- Historical backfill: Automatically backfills data when starting mid-session
- Configurable alerts: Console alerts by default, extensible to Discord/Telegram/SMS
# Install dependencies
npm install
# Copy environment file and add your API key
cp .env.example .env
# Edit .env and add your POLYGON_API_KEY
# Run the scanner (starts both scanner and web UI)
npm run devThen open your browser to http://localhost:3000 to access the web control panel.
Detects gap-up patterns that hold above previous day's close throughout the session.
- Signal: Stock gaps up 2%+ and maintains above previous close
- Filters: Requires in-play (RVOL), bullish/neutral market, avoids daily resistance
- Best for: Morning strength, trend continuation
- Bias: Long
- R:R: 2:1
Detects price breaking above the 5-minute opening range (09:30-09:35 ET) with increased volume.
- Signal: Price breaks above OR high with 1.2x+ opening range volume
- Filters: Requires in-play, bullish/neutral market, daily uptrend preferred, rejection filter
- Best for: Early momentum breakouts
- Bias: Long
- R:R: 2:1
Detects price breaking below the 5-minute opening range (09:30-09:35 ET) with increased volume.
- Signal: Price breaks below OR low with 1.2x+ opening range volume
- Filters: Requires in-play, bearish/neutral market, daily downtrend preferred, rejection filter
- Best for: Early weakness/short setups
- Bias: Short
- R:R: 2:1
Detects when price breaks above session high, signaling potential breakout.
- Signal: Price makes new high for current session (close-based, not wick-only)
- Filters: Avoids late-session exhaustion, daily resistance, requires market alignment
- Best for: Breakout momentum, trend continuation
- Bias: Long
- R:R: 2:1
Detects when price breaks below session low, signaling potential breakdown.
- Signal: Price makes new low for current session (close-based, not wick-only)
- Filters: Avoids late-session exhaustion, daily support, requires market alignment
- Best for: Breakdown momentum, short continuation
- Bias: Short
- R:R: 2:1
Detects weak stocks that fade below VWAP and stay there throughout the session.
- Signal: 60-80%+ of bars trade below VWAP with VWAP test-and-fail
- Filters: Requires VWAP test-and-fail, avoids daily support, bearish/neutral market, avoids extreme moves
- Severity levels: EMERGING (60-69%) → CONFIRMED (70-79%) → EXTREME (80%+)
- Best for: Risk-off environments, persistent weakness
- Bias: Short
- R:R: 2:1
The scanner includes a full-featured web control panel at http://localhost:3000:
- Status Dashboard: Real-time scanner status, ticker counts, scan statistics, market bias
- Ticker Management: Add/remove tickers dynamically without restarting
- Scan Frequency Control: Adjust scan interval from 30 seconds to 5 minutes
- Pattern Toggles: Enable/disable patterns in real-time
- Active Signals: View all active signals with context badges (RVOL, market bias, daily trend)
- Signal Details: Entry, stop, target, R:R ratio, and contextual metadata
The scanner automatically filters tickers based on:
- Price: Minimum $5 (configurable)
- Liquidity: Minimum average daily dollar volume (default: $20M)
- Volatility: Minimum ATR threshold (default: $0.10)
- In-Play Detection: Relative volume (RVOL) filtering for high-intent patterns
- Compares current session volume vs 20-day average
- Default threshold: 1.5x RVOL
The scanner tracks market context for better signal quality:
- SPY/QQQ Bias: Monitors index direction (bullish/neutral/bearish)
- Daily Trend: Tracks 20/50-day MA relationships
- Daily Levels: Identifies proximity to daily resistance/support
- Directional Alignment: Only takes long patterns in bullish/neutral markets, short patterns in bearish/neutral markets
All patterns use a centralized risk framework:
- Consistent R:R: Default 2:1 risk/reward ratio
- Structural Stops: Stops placed at logical invalidation levels
- Pattern-Specific Overrides: Can customize R multiples per pattern
- ATR-Based Utilities: Optional ATR-based stop calculation
Signal outcomes are automatically tracked:
- MFE/MAE: Max favorable/adverse excursion in R multiples
- Stop/Target Hits: Tracks which signals hit stops vs targets
- Context Tagging: Market bias, RVOL, time of day, daily trend
- Performance Summaries: Win rate and average R by pattern
- Persistent Storage: Outcomes saved to
data/analytics/outcomes.json
# Required
POLYGON_API_KEY=your_key_here
# Optional - comma-separated list of tickers
SCAN_TICKERS=AAPL,MSFT,GOOGL,AMDThe scanner uses config.json for persistent configuration:
{
"tickers": ["AAPL", "MSFT", "GOOGL"],
"scanIntervalMs": 300000,
"patterns": {
"Gap and Hold": { "enabled": true },
"Opening Range Breakout": { "enabled": false },
"New Session High": { "enabled": true }
}
}Configuration can be managed via the web UI or by editing config.json directly.
If SCAN_TICKERS is not set, the scanner watches:
- Indices: QQQ, SPY, IWM (required for market context)
- Mega-cap tech: AAPL, MSFT, NVDA, AMZN, GOOGL, META, TSLA
- Tech/Semis: AMD, INTC, AVGO, NFLX, CRM
- Crypto-adjacent: ETHE, GBTC, MSTR, MARA, CLSK, RIOT
- Blue chips: BA, CAT, JPM, GS, V, MA
- Volatility: UVXY, SOXL, TQQQ
Note: SPY and QQQ are automatically included for market context tracking.
┌─────────────────┐
│ Polygon WS │ → 1-min bars for tickers
└────────┬────────┘
↓
┌─────────────────┐
│ Market State │ → In-memory sliding window (300 bars per ticker)
└────────┬────────┘
↓
┌─────────────────┐
│ Universe │ → Filters (price, liquidity, RVOL, ATR)
│ Filters │
└────────┬────────┘
↓
┌─────────────────┐
│ Market Context │ → SPY/QQQ bias, daily trends, key levels
└────────┬────────┘
↓
┌─────────────────────────────────┐
│ Pattern Scanners (concurrent) │
│ - Gap and Hold │
│ - Opening Range Breakout │
│ - Opening Range Breakdown │
│ - New Session High │
│ - New Session Low │
│ - VWAP Fade │
└────────┬────────────────────────┘
↓
┌─────────────────┐
│ Risk Utils │ → Centralized R:R calculation
└────────┬────────┘
↓
┌─────────────────┐
│ Analytics │ → Outcome tracking & performance
└────────┬────────┘
↓
┌─────────────────┐
│ Alert System │ → Console, Discord, etc.
└─────────────────┘
↓
┌─────────────────┐
│ Web UI │ → Control panel & signal display
└─────────────────┘
When starting mid-session, the scanner automatically:
- Fetches previous day's close using Polygon daily aggregates (efficient single call)
- Backfills today's bars from 09:30 ET to current time using minute aggregates
- Populates market state before starting real-time scanning
This ensures patterns work correctly even when starting mid-day, as they have access to:
- Previous day's close (for gap calculations)
- Today's session open (09:30 ET)
- Session high/low from 09:30
- Complete RTH bars for accurate VWAP and metadata
- All metadata (prevDayClose, todayOpen, VWAP) uses RTH bars only (09:30-16:00 ET)
- Extended hours bars are received but filtered out of calculations
- All timestamps normalized to Eastern Time (ET)
- Works correctly regardless of server location
- Handles EDT/EST transitions automatically
- Best practice: Start before 09:30 ET to accumulate full session data
- Patterns need 20-50 bars to function; wait ~30 minutes after startup mid-session
- Historical backfill handles mid-session starts automatically
- All data stored in RAM (sliding window of 300 bars per ticker)
- Crash = lose all state (no persistence)
- Memory usage: ~50MB for 500 tickers
- Analytics outcomes are persisted to disk
- SPY and QQQ must be in your watchlist for market bias tracking
- Market context defaults to neutral if SPY/QQQ data unavailable
- Daily trend calculations use simplified MA from intraday bars (for production, consider fetching daily aggregates)
# Development with auto-reload (scanner + web UI)
npm run dev
# Build for production
npm run build
# Run production build
npm start
# Run without auto-reload
npm run watchThe web UI communicates with the scanner via REST API:
GET /api/status- Scanner status and statisticsGET /api/tickers- List configured tickersPOST /api/tickers- Update tickersGET /api/scan-frequency- Get scan intervalPOST /api/scan-frequency- Update scan intervalGET /api/patterns- List all patterns with enabled statePOST /api/patterns/:name/enable- Enable a patternPOST /api/patterns/:name/disable- Disable a patternGET /api/signals- Get active signalsGET /api/market-context- Get current market bias
Create a new pattern file in src/patterns/:
// src/patterns/my-pattern.ts
import { Pattern, TickerState, Signal } from './types';
import { universeFilters } from '../filters/universe-filters';
import { marketContextService } from '../context/market-context';
import { calculateRisk, getPatternRMultiple } from '../risk/risk-utils';
export const MyPattern: Pattern = {
name: 'My Pattern',
description: 'Description of what it detects',
minBars: 20,
shouldScan(state: TickerState): boolean {
// Pre-filter logic
if (state.bars.length < 20) return false;
// Apply universe filters
if (!universeFilters.shouldScan(state, true)) return false; // require in-play
// Check market alignment
if (!marketContextService.isDirectionAligned('long')) return false;
return true;
},
scan(state: TickerState): Signal | null {
// Pattern detection logic
// Use risk utils for consistent R:R
const entry = current.close;
const stop = someStructureLevel * 0.998;
const rMultiple = getPatternRMultiple(this.name);
const riskResult = calculateRisk({
entry,
stop,
rMultiple
}, 'long');
if (riskResult.risk <= 0) return null;
// Return signal with context metadata
return {
ticker: state.ticker,
pattern: this.name,
timestamp: current.timestamp,
time: current.time,
entry: riskResult.entry,
stop: riskResult.stop,
target: riskResult.target,
confidence: calculatedConfidence,
metadata: {
rMultiple: riskResult.rMultiple,
marketBias: marketContextService.getMarketContext()?.overallBias,
rvol: universeFilters.getFilterStatus(state).rvol,
isInPlay: universeFilters.getFilterStatus(state).isInPlay
}
};
}
};Then register it in src/index.ts:
import { MyPattern } from './patterns/my-pattern';
// In registerPatterns()
registry.register(MyPattern);And add it to the default config in src/config/config-manager.ts:
patterns: {
'My Pattern': { enabled: false }
}- Scan frequency: Default 5 minutes balances responsiveness vs CPU usage
- Universe filters: Reduce false signals by filtering low-quality tickers
- Market context: Prevents taking trades against market direction
- In-play filtering: Focuses on high-probability setups with volume confirmation
MIT