Skip to content

Repository files navigation

SwingSignal

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.


Features

  • 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.

Getting started

# 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:3000

Other 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 tests

Environment variables

Copy .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.

Provider notes

  • Yahoo Finance (default) — free, no key or signup, daily + hourly candles with deep history. It is an unofficial endpoint (the same one the Python yfinance library 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.


Data freshness

Every response carries freshness metadata, surfaced in the UI as a badge:

  • dataSourceyahoo / massive / alphavantage / mock
  • lastCandleTimestamp — always shown next to the chart and on the signal card
  • isStale / 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.


How the signal is scored

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".

Trend (25)

  • 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.

Momentum (20)

  • 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.

Volume (20)

  • 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.

Support / Resistance (20)

Levels come from lib/analysis/supportResistance.ts:

  1. Detect pivot highs/lows (symmetric window).
  2. Cluster nearby pivots within an ATR-based tolerance (~½ ATR).
  3. Score each cluster by touches, recency, rejection candles, and volume at the level.
  4. 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.

Risk / Reward (15)

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.

Signal categories

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.

How the strategy was calibrated

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.


Indicators calculated

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.


Swing Screener

/screener scans a ticker universe server-side and ranks the surviving setups.

How it works (lib/analysis/screener.ts):

  1. 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.
  2. Skip tickers with fewer than 60 candles or failed fetches (counted as "skipped", never silently dropped).
  3. Run the same signal engine the dashboard uses — there is no separate screener scoring system.
  4. 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.
  5. 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.


Backtesting

lib/analysis/backtest.ts replays the engine bar-by-bar.

  • No look-ahead: at each historical bar i, the engine only sees candles[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 in tests/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.


Project structure

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

Design & UX principles

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.


Known limitations

  • 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 localStorage watchlist.

Future improvements

  • 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).

Disclaimer

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages