Live app: quant-backtest-lab.onrender.com
A portfolio-oriented quant backtesting project with:
- React + TypeScript frontend
- C# ASP.NET Core backend
- Python strategy execution with user-authored code and dynamic custom params
- Real historical US equities data from free Stooq CSV endpoints
- WebSocket streaming via SignalR for live run progress, charts, and metrics
- Backtest controls for symbols/date range/strategy params
- Strategy editor on Overview:
- single
strategy.pyMonaco editor - dynamic custom parameter builder (string/number/boolean/json)
- one-click quant template import (momentum, mean reversion, trend + inverse vol)
- fullscreen editor mode + local autosave in browser
- single
- Momentum long/short engine (event-driven daily loop, periodic rebalance)
- Live metrics cards and live additional statistics streamed from backend
- Equity curve vs SPY benchmark
- Drawdown and returns distribution charts
- Transactions table
src/: frontendbackend/: C# API, backtest engine, market data service, SignalR hub
- Bun (or npm/pnpm, but repo is currently using Bun lockfile)
- Docker
- Optional (non-Docker backend dev): .NET SDK 8.0+, Python 3.10+,
pandas,numpy
bun install
cp .env.example .env
bun devFrontend runs on http://localhost:5173 by default.
# from repo root
docker build -t quant-backtest-backend ./backend
docker run --rm -p 5055:5055 -e PORT=5055 quant-backtest-backendBackend will run on http://127.0.0.1:5055.
If you use a different port, update VITE_API_BASE in .env.
- Terminal 1 (frontend):
bun dev- Terminal 2 (backend):
docker build -t quant-backtest-backend ./backend
docker run --rm -p 5055:5055 -e PORT=5055 quant-backtest-backendPOST /api/runs-> start a runGET /api/runs/{runId}-> get finalized resultGET /api/runs/{runId}/transactions-> get transactionsWS /hubs/runs-> SignalR progress/events
Live SignalR payloads include run progress, equity points, drawdown points, daily return points, and live metric/stat snapshots.
The frontend sends:
strategyCode: string(single python file contents)strategyParams: Record<string, unknown>(user-defined params)
Preferred strategy entrypoint:
def generate_signals(context, params):
# return one of:
# 1) DataFrame long-form: columns ['date', 'symbol', 'signal']
# 2) DataFrame wide-form: index=date, cols=symbol, values=signal
# 3) Series: symbol -> signal (applied to latest date)
# 4) dict: either {date: {symbol: signal}} or {symbol: signal}
...Context passed to generate_signals:
context["dates"]: ordered date list (YYYY-MM-DD)context["symbols"]: symbol universecontext["close"]: pandas DataFrame of closes (date x symbol)context["returns"]: pandas DataFrame of daily returns (date x symbol)context["datasets"]: helper object withload("market_daily", date_start=None, date_end=None)
Legacy support is kept for class Strategy(...): get_signals(...) and compute_signals(...).
If custom Python fails, the run fails by default unless fallbackToBuiltinOnPythonError is set to true.