Chart UI

Use this guide when you want to run the React/Vite chart workspace and inspect candles loaded into the local DuckDB database. The UI talks to the FastAPI backend under /api/v1.

The top bar includes a workspace selector and a create-workspace field. Live paper trade plans and category backtests use the selected workspace. See multiuser.md for the workspace API guide and current limitations.

Quick Start

  1. Download and insert candles into file.db.
  2. Start the backend with uv run uvicorn main:app --host 127.0.0.1 --port 8000.
  3. Start the frontend with npm run dev.
  4. Open the local Vite URL and inspect candles, ratios, categories, or scanner results.

Use the Downloader guide first if file.db does not yet contain candles.

Requirements

  • Python dependencies installed with uv.
  • Node dependencies installed with npm install.
  • A DuckDB database containing binance_candles, usually file.db.
  • Backend running on 127.0.0.1:8000.
  • Frontend dev server running on 127.0.0.1:5173.

Install dependencies:

uv sync --dev
npm install

Start The Backend

Run:

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

Keep this terminal open while using the UI. Stop the backend with Ctrl-C.

Check backend health:

curl http://127.0.0.1:8000/api/v1/health

Expected healthy response shape:

{
  "status": "ok",
  "database": {
    "path": "file.db",
    "connected": true,
    "readonly": true
  }
}

If the backend cannot read file.db, the UI will not have market data.

Start The Frontend

In another terminal, run:

npm run dev

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

Open:

http://127.0.0.1:5173/

The Vite dev server proxies /api requests to the backend at http://127.0.0.1:8000 by default.

If 5173 is already in use, Vite automatically chooses the next available port, such as 5174. Use the URL printed by npm run dev.

Use A Different Backend

Use this when 8000 is already occupied or you intentionally start the backend on another port.

Start the backend on another port:

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

Set VITE_PROXY_TARGET when starting the frontend:

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

You can also bypass the proxy by setting VITE_API_BASE at build/runtime:

VITE_API_BASE=http://127.0.0.1:8000/api/v1 npm run dev

Verify the scanner through the frontend proxy:

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

If the backend is on 8001 and the UI was started with VITE_PROXY_TARGET=http://127.0.0.1:8001, the command above should still work through the frontend server.

Mock Server Mode

The repo includes a mock-server path for the OpenAPI spec. Start the mock server:

npm run mock

Then start the frontend against the mock server:

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

When VITE_PROXY_TARGET includes 4010, the Vite proxy rewrites /api/v1 to the mock server root.

Main Screen

The UI opens directly to the chart workspace.

Top area:

  • Current symbol title, for example BTCUSDT.
  • Status indicator:
    • green dot: last request succeeded,
    • red dot: request failed.
  • Status text such as Loading chart, Market data ready, Ratio ready, or an HTTP error.

Chart Modes

Use the mode switch above the controls:

  • Candles: shows OHLC candles, volume bars, and strategy signal markers.
  • Ratio: shows the ratio candle series for a numerator/denominator pair.
  • Category: shows an equal-weighted category index built from the local symbol category file.

Market Scanner

The side scanner panel has two modes:

  • Single: ranks symbols that are clearly trending upward according to moving-average stack, moving-average slope, recent returns, and extension from the medium moving average.
  • Ratio: ranks long-short pairs where the numerator is outperforming the denominator.

On desktop-width screens, the scanner stays beside the chart and scrolls inside its own panel. On smaller screens, it moves below the chart.

See uptrend-scanner.md for the full scanner user guide, including score rules, API examples, and troubleshooting.

Controls:

  • Single / Ratio: switches between single-symbol and pair scanning.
  • Quote: filters symbols by quote asset, for example USDT.
  • Bench: in Ratio mode, selects denominator symbols such as BTC, ETH, or BTC+ETH.
  • Min Score: only shows symbols whose trend score is at least this value.
  • Limit: controls the maximum number of rows returned.
  • Refresh: reruns the scanner request.

The single-symbol scanner table shows:

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

Click a scanner row to open that symbol in candle mode.

The ratio scanner table shows:

  • Pair: numerator/denominator pair and relative trend state.
  • Score: explainable relative trend score.
  • 30D: recent ratio return.
  • 90D: longer ratio return.
  • Ext: distance above the ratio medium moving average.

Click a pair row to open ratio mode with the numerator and denominator filled.

The scanner endpoints are:

GET /api/v1/scanner/uptrends
GET /api/v1/scanner/ratio-uptrends

Candle Mode

In Candles mode:

  • Symbol: selected market symbol.
  • Interval: one of 1d, 4h, 1h, or 15m.
  • Strategy: selected strategy marker source.
  • Range buttons:
    • 1M: last 30 days,
    • 3M: last 90 days,
    • 1Y: last 365 days,
    • All: up to 5000 candles.

The chart displays:

  • candlesticks,
  • volume bars,
  • moving-average indicator overlays,
  • strategy markers.

Indicators

In Candles mode, the Indicators bar appears above the chart.

Available defaults:

  • SMA 20: enabled by default.
  • SMA 50: enabled by default.
  • SMA 200: disabled by default.
  • EMA 20: disabled by default.

Toggle an indicator checkbox to add or remove its line overlay from the candle chart. Indicators reload when you change symbol, interval, or chart range.

Indicator requests use:

POST /api/v1/indicators/preview

Indicator presets for the current chart come from:

GET /api/v1/symbols/{symbol}/intervals/{interval}/indicators

The metrics row shows:

  • Last: latest close,
  • Session: latest candle open-to-close percentage,
  • High: latest candle high,
  • Low: latest candle low,
  • Volume: latest candle volume.

The lower Signals panel lists strategy markers with side, label, and date.

Ratio Mode

In Ratio mode:

  • Numerator: long-leg symbol.
  • Denominator: short-leg symbol.
  • Interval: shared interval.
  • Strategy: still visible, but ratio mode displays ratio candles rather than signal markers.
  • Range buttons work the same as candle mode.

The ratio endpoint joins candles by timestamp and computes ratio OHLC values.

The metrics row shows:

  • Ratio: latest ratio close,
  • Range: ratio change across the selected range,
  • Base Close: latest numerator close,
  • Quote Close: latest denominator close,
  • Pair: selected numerator/denominator pair.

The lower Pair panel labels the numerator as the long leg and denominator as the short leg.

Category Mode

In Category mode:

  • View: choose Index for one category or Ratio for category-vs-category relative performance.
  • Category: selected sector or theme from configs/symbol-categories.yaml.
  • Numerator: in ratio view, the category being compared.
  • Denominator: in ratio view, the benchmark category.
  • Max Borrow APR: in ratio view, optionally excludes denominator components whose Binance margin borrow APR is above the selected limit.
  • Interval: shared candle interval.
  • Range buttons work the same as candle mode.

The category index is equal-weighted. Each component symbol is normalized to 100 at its first visible candle, then the backend averages all available component values at each timestamp. This makes the line read like category performance rather than absolute price.

The category index uses the same Quote setting as the scanner panel. For example, with Quote = USDT, Layer 1 only includes category members whose symbol quote asset is USDT.

When Category mode is active, the right side panel changes from the market scanner to the selected category’s component list. Use the panel’s Quote control to change the component universe. Click a component symbol to open that symbol in candle mode. In ratio view, the panel shows separate numerator and denominator component groups.

The metrics row shows:

  • Index: latest normalized category index value,
  • Range: index change across the selected range,
  • Components: number of component symbols present at the latest timestamp,
  • Universe: number of symbols selected from the category file,
  • Quote: quote-asset filter used for the index.

The category endpoints are:

GET /api/v1/categories
GET /api/v1/categories/{category}/index
GET /api/v1/category-ratios

The category ratio endpoint divides the numerator category index by the denominator category index on matching timestamps. A rising ratio means the numerator category is outperforming the denominator category over the selected range.

Borrow filtering applies only to the denominator category because that side is the synthetic short leg. When Max Borrow APR is Off, category ratios are computed entirely from local DuckDB data. When it is set to a percentage, the backend reads Binance margin next-hourly borrow rates and excludes denominator symbols above the limit. This requires the backend environment variables:

BINANCE_API_KEY
BINANCE_API_SECRET

API Request Panel

The lower-right API Request panel shows the exact endpoint used for the current chart.

Examples:

/api/v1/candles?symbol=BTCUSDT&interval=1d&from=...&to=...&limit=1000&order=asc
/api/v1/ratios?base_symbol=BTCUSDT&quote_symbol=ETHUSDT&interval=1d&from=...&to=...&limit=1000&order=asc

Use this panel when debugging backend responses or reproducing a request with curl.

Data Requirements

The symbol dropdown comes from:

GET /api/v1/symbols

Only symbols already present in DuckDB appear in the UI.

The interval selector is currently fixed in the frontend:

1d, 4h, 1h, 15m

If you select an interval that is not present for the symbol in DuckDB, the backend returns an error and the UI shows Request failed.

Verify Data Before Opening UI

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) from binance_candles group by symbol order by symbol limit 20\").fetchall())"

Check one candle series:

curl 'http://127.0.0.1:8000/api/v1/candles?symbol=BTCUSDT&interval=1d&limit=5&order=desc'

Check the symbol catalog:

curl http://127.0.0.1:8000/api/v1/symbols

Troubleshooting

Catalog load failed

The frontend could not load /api/v1/symbols or /api/v1/strategies.

Check:

curl http://127.0.0.1:8000/api/v1/health
curl http://127.0.0.1:8000/api/v1/symbols

Request failed

The selected symbol, interval, range, or ratio request failed.

Common causes:

  • backend is not running,
  • file.db is missing,
  • binance_candles table is empty,
  • selected interval has no rows,
  • ratio symbols do not overlap in time.

Symbol dropdown only shows BTCUSDT

That is the frontend fallback while the catalog is loading or after catalog loading fails. Check the backend and DuckDB data.

The chart is empty

Check that the selected symbol and interval have rows:

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

Ratio mode fails

Both symbols must have candles for the same interval and overlapping timestamps. Check:

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

Frontend shows stale data

Refresh the browser after inserting new data. The UI fetches data when controls change or the page loads; it does not currently stream live updates.