Uptrend Scanner

Use this guide when you want to filter downloaded Binance symbols down to candidates that are clearly trending upward. The scanner uses only candles already loaded into local DuckDB table binance_candles.

The scanner is a research tool. It is not financial advice, and a high score does not mean a symbol should be bought.

Quick Start

  1. Download enough candle history into file.db.
  2. Start the FastAPI backend.
  3. Start the chart UI if you want to scan visually.
  4. Open the Market Scanner panel or call GET /api/v1/scanner/uptrends.
  5. Click a scanner row in the UI to inspect the symbol or ratio chart.

Requirements

  • Historical candles downloaded and inserted into file.db.
  • Enough history for each symbol. The default scanner needs at least 220 candles per symbol.
  • FastAPI backend running.
  • React chart UI running if you want to use the scanner visually.

Start the backend:

uv run uvicorn main:app --host 127.0.0.1 --port 8000

Keep the backend terminal open while scanning. Stop it with Ctrl-C.

Start the UI:

npm run dev

Keep the frontend terminal open while using the UI. Stop it with Ctrl-C.

Open:

http://127.0.0.1:5173/

If 5173 is already in use, Vite prints another local URL, such as http://127.0.0.1:5174/. Use the printed URL.

If 8000 is already in use, start the backend on another port:

uv run uvicorn main:app --host 127.0.0.1 --port 8001

Then start the UI with the backend proxy target:

VITE_PROXY_TARGET=http://127.0.0.1:8001 npm run dev

What The Scanner Finds

The scanner looks for symbols where:

  • the latest close is above the medium moving average,
  • the short moving average is above the medium moving average,
  • the medium moving average is above the long moving average,
  • the medium and long moving averages are rising,
  • recent returns are positive,
  • the price is not too extended above the medium moving average.

Default settings:

interval: 1d
short_window: 20
medium_window: 50
long_window: 200
slope_window: 20
recent_return_window: 30
long_return_window: 90
max_extension_pct: 35
min_score: 7
min_candles: 220
quote_asset: USDT
limit: 50

Score

Each symbol receives a trend_score.

Rules:

+2 if close > SMA50
+2 if SMA50 > SMA200
+2 if SMA20 > SMA50
+1 if SMA50 is rising versus 20 candles ago
+1 if SMA200 is rising versus 20 candles ago
+1 if 30-candle return > 0
+1 if 90-candle return > 0
-1 if close is more than 35% above SMA50

Trend states:

score >= 7: clear_uptrend
score 5-6: watchlist
score < 5: ignore

By default the API only returns rows with trend_score >= 7.

Use The Scanner In The UI

The chart workspace includes a Market Scanner panel beside the main chart. The scanner results scroll inside that side panel, so you can inspect symbols without moving away from the chart. On smaller screens, the scanner moves below the chart.

Use the scanner mode switch:

  • Single: ranks individual symbols by absolute uptrend criteria.
  • Ratio: ranks numerator/denominator pairs by relative uptrend criteria.

Controls:

  • Quote: filters symbols by quote asset, for example USDT, USDC, BTC, or All.
  • Bench: in Ratio mode, selects denominator benchmarks such as BTC, ETH, or BTC+ETH.
  • Min Score: filters out symbols below the selected trend score.
  • Limit: controls the maximum number of rows returned.
  • Refresh: reruns the scanner request.

Columns:

  • Symbol: candidate symbol and trend state.
  • Score: scanner trend score.
  • 30D: return over the recent-return window.
  • 90D: return over the long-return window.
  • Ext: percent distance above the medium moving average.
  • Last: latest candle date used by the scanner.

Click a row to open that symbol in candle mode.

In Ratio mode, clicking a row opens the ratio chart for that pair. A rising ratio means the numerator is outperforming the denominator; it does not necessarily mean the numerator is rising in USDT terms.

Use The Scanner API

Endpoint:

GET /api/v1/scanner/uptrends

Ratio pair endpoint:

GET /api/v1/scanner/ratio-uptrends

Basic request:

curl 'http://127.0.0.1:8000/api/v1/scanner/uptrends'

Scan USDT daily symbols and return the top 25:

curl 'http://127.0.0.1:8000/api/v1/scanner/uptrends?interval=1d&quote_asset=USDT&limit=25'

Show weaker watchlist candidates too:

curl 'http://127.0.0.1:8000/api/v1/scanner/uptrends?min_score=5'

Use a stricter scan:

curl 'http://127.0.0.1:8000/api/v1/scanner/uptrends?min_score=8&max_extension_pct=20'

Use shorter windows for faster-moving markets:

curl 'http://127.0.0.1:8000/api/v1/scanner/uptrends?short_window=10&medium_window=30&long_window=100&min_candles=120'

Scan USDT symbols against BTC and ETH:

curl 'http://127.0.0.1:8000/api/v1/scanner/ratio-uptrends?interval=1d&benchmark_symbols=BTCUSDT,ETHUSDT&limit=25'

Scan specific pairs:

curl 'http://127.0.0.1:8000/api/v1/scanner/ratio-uptrends?base_symbols=SOLUSDT,BNBUSDT&quote_symbols=BTCUSDT'

Query Parameters

  • interval: candle interval. Default: 1d.
  • quote_asset: optional symbol suffix filter. Default: USDT.
  • limit: max result rows, from 1 to 500. Default: 50.
  • min_score: minimum trend score, from 0 to 10. Default: 7.
  • min_candles: minimum loaded candles per symbol. Default: 220.
  • short_window: short moving-average window. Default: 20.
  • medium_window: medium moving-average window. Default: 50.
  • long_window: long moving-average window. Default: 200.
  • slope_window: moving-average slope lookback. Default: 20.
  • recent_return_window: recent return lookback. Default: 30.
  • long_return_window: longer return lookback. Default: 90.
  • max_extension_pct: extension threshold for the overextended penalty. Default: 35.

Moving-average windows must satisfy:

short_window < medium_window < long_window

Response Fields

Example response shape:

{
  "interval": "1d",
  "timezone": "UTC",
  "params": {
    "short_window": 20,
    "medium_window": 50,
    "long_window": 200,
    "slope_window": 20,
    "recent_return_window": 30,
    "long_return_window": 90,
    "max_extension_pct": 35,
    "min_score": 7,
    "min_candles": 220,
    "quote_asset": "USDT"
  },
  "items": [
    {
      "symbol": "BTCUSDT",
      "trend_score": 9,
      "trend_state": "clear_uptrend",
      "time": 1774915200,
      "close": 105000.0,
      "sma_short": 98000.0,
      "sma_medium": 91000.0,
      "sma_long": 76000.0,
      "medium_slope_pct": 4.2,
      "long_slope_pct": 7.8,
      "recent_return_pct": 12.4,
      "long_return_pct": 38.1,
      "extension_pct": 15.4,
      "candle_count": 820,
      "first_time": 1704067200,
      "last_time": 1774915200
    }
  ]
}

Important fields:

  • trend_score: total score used for sorting and filtering.
  • trend_state: clear_uptrend, watchlist, or ignore.
  • sma_short, sma_medium, sma_long: moving averages used by the score.
  • medium_slope_pct: medium moving-average change over slope_window.
  • long_slope_pct: long moving-average change over slope_window.
  • recent_return_pct: close-to-close return over recent_return_window.
  • long_return_pct: close-to-close return over long_return_window.
  • extension_pct: percent distance from close to medium moving average.
  • candle_count: number of loaded candles for that symbol and interval.
  • first_time, last_time: available local data range as Unix seconds.

Sorting

Results are sorted by:

trend_score desc
long_return_pct desc
recent_return_pct desc
symbol asc

This favors sustained trend strength before short-term movement.

Verify Data Before Scanning

Check available symbols and intervals:

uv run python -c "import duckdb; con=duckdb.connect('file.db'); print(con.execute(\"select symbol, list(distinct interval order by interval), count(*) from binance_candles group by symbol order by symbol limit 20\").fetchall())"

Check whether a symbol has enough daily candles:

uv run python -c "import duckdb; con=duckdb.connect('file.db'); print(con.execute(\"select symbol, interval, count(*), min(open_time), max(open_time) from binance_candles where symbol = 'BTCUSDT' and interval = '1d' group by 1,2\").fetchall())"

Troubleshooting

No symbols match the scanner filters

Common causes:

  • not enough candles for min_candles,
  • selected interval is not loaded,
  • selected quote asset has no matching symbols,
  • min_score is too strict.

Try:

curl 'http://127.0.0.1:8000/api/v1/scanner/uptrends?min_score=5&min_candles=100'

invalid_scanner_params

Check that:

  • all window values are positive,
  • short_window < medium_window < long_window,
  • limit is between 1 and 500,
  • min_score is between 0 and 10.

Request failed in the UI

Check the backend:

curl http://127.0.0.1:8000/api/v1/health
curl 'http://127.0.0.1:8000/api/v1/scanner/uptrends?limit=5'

If your backend is on another port, start the UI with:

VITE_PROXY_TARGET=http://127.0.0.1:8001 npm run dev

You can also verify the frontend proxy:

curl 'http://127.0.0.1:5173/api/v1/scanner/uptrends?limit=1'

If Vite selected a different frontend port, replace 5173 with the printed port.

Interpreting Results

A high score means the local historical data currently matches the scanner’s uptrend rules. It does not account for order-book liquidity, news, future volatility, trading fees, or whether the trend is overextended beyond the one extension penalty.

Use the scanner to make a shorter watchlist, then inspect the chart and run separate strategy or backtest checks before making trading decisions.