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
- Download enough candle history into
file.db. - Start the FastAPI backend.
- Start the chart UI if you want to scan visually.
- Open the Market Scanner panel or call
GET /api/v1/scanner/uptrends. - 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
220candles 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 exampleUSDT,USDC,BTC, orAll.Bench: inRatiomode, selects denominator benchmarks such asBTC,ETH, orBTC+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"e_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"e_symbols=BTCUSDT'
Query Parameters
interval: candle interval. Default:1d.quote_asset: optional symbol suffix filter. Default:USDT.limit: max result rows, from1to500. Default:50.min_score: minimum trend score, from0to10. 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, orignore.sma_short,sma_medium,sma_long: moving averages used by the score.medium_slope_pct: medium moving-average change overslope_window.long_slope_pct: long moving-average change overslope_window.recent_return_pct: close-to-close return overrecent_return_window.long_return_pct: close-to-close return overlong_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_scoreis 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,limitis between1and500,min_scoreis between0and10.
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.