A minimalistic but communicative swing-trading technical-analysis dashboard. SwingSignal helps you spot potential swing setups with an ideal holding period of 1–10 trading days (and flags longer "extended swing" setups when the structure supports it). It is purely technical: only price and volume feed the signal score — no fundamentals, news, analyst ratings, earnings, or social sentiment.
⚠️ Research tool, not financial advice. SwingSignal does not predict prices or guarantee profit. Every signal is an estimate derived from historical price and volume. Always do your own research and manage risk.
- Dashboard — ticker search, watchlist sidebar (persisted in
localStorage), candlestick chart, signal summary card, support/resistance panel, volume panel, risk/reward panel, and recent signal history. - Chart — TradingView Lightweight Charts with volume bars, toggleable overlays (EMA 9, EMA 21, SMA 50, SMA 200, Bollinger Bands, S/R zones, buy/sell markers), a timeframe selector (1D / 1H) and chart ranges (3M / 6M / 1Y).
- Signal card — label (Strong Buy → Avoid), 0–100 confidence, trade horizon, entry zone, stop/invalidation, targets, risk/reward, plain-English reasons, and explicit weaknesses/warnings.
- Swing Screener — scans a universe (S&P 500 sample, Nasdaq 100 sample, your watchlist, or manual tickers) with the same signal engine and ranks the setups. Ranks technical setups only — it does not predict price movement.
- Backtest page — replays the signal engine over historical candles with no look-ahead and reports win rate, average return, average R, max drawdown, trade count, and best/worst trade.
- Data-provider abstraction — swap between Yahoo Finance (free, keyless), Massive/Polygon, Alpha Vantage, or deterministic mock data. With zero configuration the app uses free Yahoo Finance data, and degrades to labeled demo data if that fails.
# 1. Install
npm install
# 2. (Optional) configure a data provider
cp .env.example .env.local
# No key is required — by default the app uses free Yahoo Finance data.
# Add a Massive/Polygon or Alpha Vantage key to use those instead.
# 3. Run
npm run dev
# open http://localhost:3000Other scripts:
npm run build # production build
npm run start # run the production build
npm run lint # eslint
npm run typecheck # tsc --noEmit
npm test # vitest unit testsCopy .env.example → .env.local. All keys are optional.
| Variable | Purpose |
|---|---|
DATA_PROVIDER |
yahoo | massive | alphavantage | mock (unset = auto) |
MASSIVE_API_KEY |
Key for a Massive/Polygon-compatible aggregates API |
MASSIVE_BASE_URL |
Base URL (default https://api.polygon.io) |
ALPHAVANTAGE_API_KEY |
Key for Alpha Vantage |
Provider resolution (in lib/data/index.ts): honor DATA_PROVIDER if usable
(yahoo/mock need no key) → else use whichever keyed provider has a key → else
Yahoo Finance. If a live provider errors mid-request, the API route degrades to
clearly-labeled demo data rather than showing nothing.
- Yahoo Finance (default) — free, no key or signup, daily + hourly candles
with deep history. It is an unofficial endpoint (the same one the Python
yfinancelibrary uses): no SLA, informal rate limits, quotes typically ~15 min delayed. Fine for personal swing research; not for commercial products. - Massive (formerly Polygon.io) — high quality, but paid plans only since the 2025 rebrand (no free tier).
- Alpha Vantage — free tier is ~25 requests/day with daily bars only and responses often capped near 100 points, which is too shallow for SMA 200; supported mainly for users with a paid key.
Mock data is deterministic per ticker (seeded PRNG), so the same symbol always renders the same chart — handy for demos and tests. It is always labeled "Demo data" in the UI and never presented as live.
Every response carries freshness metadata, surfaced in the UI as a badge:
dataSource—yahoo/massive/alphavantage/mocklastCandleTimestamp— always shown next to the chart and on the signal cardisStale/staleReason— set when the newest candle is older than the freshness threshold (~4 days for daily, 6 hours for hourly)
When data is stale, the signal engine reduces confidence by 40% and adds a visible warning. When data is mock, the card says so explicitly.
The engine (lib/analysis/signalEngine.ts) is deterministic and explainable.
It never relies on a single indicator — it weighs confluence across five
categories that sum to 100:
| Category | Max points | What it rewards |
|---|---|---|
| Trend | 25 | Price above EMA 21 & SMA 50; EMA 9 > EMA 21 > SMA 50; above SMA 200 |
| Momentum | 20 | RSI 45–65 (healthy); MACD above signal; rising MACD histogram |
| Volume | 20 | Breakout volume ≥ 1.3× avg; above-avg volume at support; rising OBV |
| Support/Resist. | 20 | Sitting on a high-quality support; MA confluence; open overhead room |
| Risk/Reward | 15 | ≥ 1.5R acceptable, ≥ 2R strong, ≥ 3R excellent |
Every point awarded produces a short reason, shown under "Why this signal".
- Bullish when price is above EMA 21 and SMA 50; stronger when EMA 9 > EMA 21 > SMA 50 (aligned uptrend). A confirmed uptrend can earn the full 20 structure points (+5 for being above SMA 200).
- Reversal pathway: a confirmed basing structure — a double bottom or
higher low (latest pivot low vs. the lowest prior low, within 0.75 ATR),
with price reclaiming EMA 9/21 and RSI recovering from a washout or MACD
improving — earns 12 trend points even though the stack is not yet bullish
(see
lib/analysis/reversal.ts). Capped below a true uptrend on purpose. - Bearish when price is below EMA 21 and SMA 50.
- Being below SMA 200 is penalised unless price is reclaiming the faster averages or a confirmed reversal structure is present.
- RSI 45–65 is the healthy continuation zone.
- RSI > 70 is overextended and scores low unless breakout volume ≥ 1.3×.
- RSI < 35 only counts as a bounce setup when price is near support.
- MACD above its signal adds confidence; below reduces it; a rising histogram (3 bars) adds momentum credit.
- Breakouts through resistance want ≥ 1.3× the 20-day average; low-volume breakouts are marked lower confidence.
- Bounces from support are stronger on above-average volume.
- Volume contraction on a pullback is bullish; expansion into a breakdown is bearish. OBV trend confirms accumulation/distribution.
Levels come from lib/analysis/supportResistance.ts:
- Detect pivot highs/lows (symmetric window).
- Cluster nearby pivots within an ATR-based tolerance (~½ ATR).
- Score each cluster by touches, recency, rejection candles, and volume at the level.
- Classify as support (below price) or resistance (above price).
Each level exposes: price, type, confidence (0–100), touch count, last-touched date, an explanation, and whether price is near it. If nothing clean is found the UI says the chart is messy / low-confidence rather than drawing arbitrary lines.
From lib/analysis/riskReward.ts, using only current structure (no look-ahead):
- Entry near current price (retest / continuation).
- Stop: when a support sits within 2 ATRs below entry, the stop goes just below that support (structure-based invalidation); otherwise an ATR stop (~1.5 ATR). The stop is the invalidation level shown on every card.
- Target 1: the first resistance at least 1 ATR above entry — minor "lids" closer than that are skipped (they are friction, not targets), though a very close lid still costs R/R points. Target 2: the next resistance beyond, or an extended measured move.
- ≥ 1.5R required for a normal Buy, ≥ 2R for Strong Buy. If price is crowded against resistance, the R/R score (and overall confidence) is reduced.
| Label | Rough criteria |
|---|---|
| Strong Buy | Confidence 80+, bullish trend, clean structure, valid R/R ≥ 2 |
| Buy | Confidence 65+ (continuation) or 70+ (reversal setup), valid R/R ≥ 1.5 |
| Watch | Confidence 50–64, setup forming, needs confirmation |
| Hold | Mixed / neutral structure |
| Trim | Near resistance with slowing momentum or rising selling volume |
| Sell | Close below a high-confidence support / bearish MACD / weak volume |
| Avoid | Messy chart, weak volume, poor R/R, or conflicting indicators |
Trade horizon (1–3d / 3–5d / 5–10d / extended swing) is estimated from how many ATRs of room there is to Target 1.
Engine knobs (lib/analysis/engineConfig.ts) were chosen by racing configurations
through the look-ahead-safe backtester with npx tsx scripts/research.ts
(10 liquid US tickers × ~3.2 years of daily candles, Buy/Strong-Buy entries,
10-bar max hold, stop-assumed-filled-first). Snapshot from the July 2026 run:
| Configuration | Trades | Win% | Avg R |
|---|---|---|---|
| Original engine (nearest-lid targets) | 7 | 29% | −0.24 |
| + smart targets & structure stops | 382 | 31% | +0.29 |
| + reversal Buys at 65 confidence | 425 | 30% | +0.24 |
| + reversal Buys at 70 (default) | 407 | 30% | +0.28 |
The headline finding: with targets set to the nearest resistance, the ≥1.5R Buy gate was almost never satisfiable, so the original engine produced ~2 trades per year across ten names. Skipping sub-ATR lids when picking targets and using structure-based stops fixed that. The reversal pathway adds early-stage base entries; requiring them to clear 70 confidence kept expectancy close to the continuation-only config.
Honest caveats: this is in-sample calibration on ten large caps over one period — not a validated edge, and not predictive of future results. It ignores fees, slippage, and gaps. Re-run the script yourself; if code changes make these numbers stale, they should be regenerated or removed.
lib/analysis/indicators.ts (all pure & unit-tested): EMA 9/21, SMA 20/50/200,
RSI 14 (Wilder), MACD 12/26/9, ATR 14 (Wilder), Bollinger Bands 20/2, Volume
SMA 20, relative volume, OBV, and per-candle body/wick structure.
/screener scans a ticker universe server-side and ranks the surviving setups.
How it works (lib/analysis/screener.ts):
- Fetch ~400 daily candles per ticker through a server-side cache
(
lib/data/candleCache.ts) — daily candles are reused for the rest of the same US-market day, so re-running a scan does not spam the provider. - Skip tickers with fewer than 60 candles or failed fetches (counted as "skipped", never silently dropped).
- Run the same signal engine the dashboard uses — there is no separate screener scoring system.
- Apply the user's filters: allowed signal labels (default Strong Buy / Buy / Watch), min confidence (default 65), min relative volume (default 1.0×), min risk/reward (default 1.5R), optional stale-data exclusion.
- Rank and return the top results.
How ranking works (computeRankScore, deterministic and unit-tested):
signal tier dominates (Strong Buy > Buy > Watch), then confidence (×5), then
risk/reward (≤200 pts), relative volume (≤80), ATR-scaled distance to the
nearest resistance (≤60, with a −60 penalty for sitting directly under a lid),
and nearest-support quality (≤40). Penalties: stale data −400, overextended-RSI
warning −100, and −25 per conflicting-signal warning (capped at −100).
Why screener results may differ from a live chart: scans use cached daily candles (up to one market day old by design) and quotes from the default Yahoo source are ~15 minutes delayed, so intraday moves after the cache fill won't be reflected until the next market day or a server restart. Each row shows its data freshness (fresh / stale / demo).
Universe files (lib/universes/): the S&P 500 and Nasdaq 100 lists are
samples of ~20 liquid names each, not real constituent lists (see the TODO
in those files). Real index membership changes over time and should be fetched
from a reliable source in production.
Limits & rate limits: scans are capped at 60 tickers and run with small concurrency. If the data provider rate-limits mid-scan, affected tickers are skipped and the UI suggests a smaller universe, a custom watchlist, or a higher-limit provider. When the mock provider is active, scan results are labeled demo and are generated from synthetic data — they are not real buy signals.
The screener is a technical-analysis research tool. It does not predict future prices and is not financial advice. Always verify the chart, volume, market context, and your own risk before trading.
lib/analysis/backtest.ts replays the engine bar-by-bar.
- No look-ahead: at each historical bar
i, the engine only seescandles[0..i]. Indicators, levels, and the signal are recomputed on that truncated slice, so no future price/level can leak into a past decision. This is enforced by a unit test intests/backtest.test.ts. - Exits resolve forward using only each later bar's high/low (stop assumed to fill first when a bar spans both stop and target — conservative).
- Reports: trades, win rate, average return, average R multiple, max drawdown (compounded equity curve), and best/worst trade.
Backtesting is historical research only. It does not model slippage, fees, gaps, or liquidity, and past behaviour does not guarantee future performance.
app/
page.tsx # Dashboard (accepts /?ticker=XYZ deep links)
screener/page.tsx # Swing Screener
backtest/page.tsx # Backtest page
api/candles/route.ts # Server-side market-data route
api/screener/route.ts # Server-side screener scan route
components/ # TickerSearch, Watchlist, SwingChart, SignalCard,
# LevelsPanel, VolumePanel, RiskRewardPanel,
# SignalHistory, BacktestSummary, ui.tsx
lib/
data/ # provider.ts, yahooProvider.ts, mockProvider.ts,
# massiveProvider.ts, alphaVantageProvider.ts,
# candleCache.ts, index.ts
analysis/ # indicators.ts, supportResistance.ts, volumeAnalysis.ts,
# signalEngine.ts, riskReward.ts, reversal.ts,
# engineConfig.ts, screener.ts, backtest.ts, index.ts
universes/ # sp500.ts, nasdaq100.ts (sample lists, see TODOs)
types/ # market.ts, signals.ts
format.ts, useCandles.ts
scripts/ # research.ts (config sweep), diagnose.ts (per-ticker dump)
tests/ # vitest unit tests + fixtures
.claude/skills/swing-signal-maintainer/SKILL.md
Minimalistic but communicative: clean neutral surfaces, rounded cards, soft borders, confidence bars, and subtle (never neon) signal colors. Every card answers what's happening, how confident, why, what invalidates it, what's the risk/reward, and is the data fresh? The app is responsive for laptop and mobile, with loading skeletons and graceful error states for invalid/unknown tickers.
- Long-side setups only — bearish structure yields Trim/Sell/Avoid, not shorts.
- Mock data is synthetic and for demonstration; do not trade on it.
- Intraday (1H) history depth depends on the provider (Yahoo caps hourly data at ~730 days; keyed providers depend on your plan).
- The default Yahoo source is an unofficial endpoint with no SLA — expect occasional rate-limiting, and ~15-minute-delayed quotes.
- No persistence beyond the browser
localStoragewatchlist.
- Broker integration for paper trading only (no live order routing).
- Alerts for watchlist tickers when a signal changes.
- Multi-timeframe confirmation (e.g., 1D trend + 1H entry timing).
- Real index constituents for the screener (the S&P 500 / Nasdaq 100 lists are currently ~20-name samples).
- Portfolio / risk-sizing calculator (position size from stop distance).
- More advanced backtesting (walk-forward, regime filters, fees/slippage).
SwingSignal is a technical-analysis research tool. It is not financial advice, does not predict prices, and does not guarantee any outcome. Trading involves risk of loss. You are responsible for your own decisions.