A small quantitative trading research platform: daily market data with a local cache, a handful of classic systematic strategies, a cost-aware backtest engine with walk-forward analysis, simple risk overlays, a paper-trading broker, and self-contained HTML tearsheets — all driven from one CLI.
The web/ directory contains a browser app built on the same engine, aimed at
someone with no quant background. It recommends simulated trades into a
paper portfolio funded with pretend money; no real money is ever involved, and
nothing in it is investment advice.
- Today — answer two questions (pretend budget, risk level) and see the exact paper orders the strategy blend would place today, in plain English, with every number explained.
- Portfolio — track the paper portfolio over time against simply having bought SPY on the same start date.
- Why — the evidence: a backtest of the blend over the last five years of free market data, computed live in your browser, costs included, with the caveats spelled out.
- Learn — a plain-language guide to every screen and every term.
The TypeScript engine in web/lib/engine/ is a port of the Python engine in
quantdesk/, held to it by parity tests: fixtures produced from the Python
engine must match the TypeScript output within 1e-8 relative tolerance
(web/test/parity.test.ts), including the same lookahead-canary convention.
Prices come from the same free sources (Yahoo first, Stooq fallback); when
both are unreachable the app falls back to a clearly labeled built-in demo
dataset.
To run it (requires Node 18+):
cd web
npm install
npm run dev # http://localhost:3000npm test runs the engine, parity, API, and store suites; npm run build
makes the production build.
This is a research and learning tool, nothing more.
- Everything here is simulated or paper trading. There is no live-trading path in this codebase: the only external broker integration is Alpaca's paper API, and the adapter rejects any other endpoint by construction.
- Markets are largely efficient. The strategies included (momentum, mean reversion, moving-average crossovers) are decades-old published ideas. Any edge they once had is widely known and may be fully arbitraged away, especially after realistic costs.
- Backtests overstate. Survivorship in the ticker list, optimistic fill assumptions, and parameter choices made with hindsight all flatter results. The default cost assumptions (1 bp commission, 2 bps slippage) are guesses that understate real frictions for anything less liquid than a megacap.
- Past performance does not predict future results.
- Nothing here is investment advice. Do not trade real money based on output from this software.
Requires Python 3.10+.
git clone <repo-url>
cd quantdesk
pip install -e .
quantdesk demoquantdesk demo downloads daily prices for a default universe of 20 large US
stocks (plus SPY as the benchmark) from the free data sources described under
Data, runs every strategy net of costs, prints a comparison table,
and writes a tearsheet to reports/demo.html. The first run is slow because
it populates the local cache; later runs reuse it. If no source can be
reached, the demo still finishes on seeded synthetic random walks, with every
table and the tearsheet title loudly labeled SYNTHETIC DATA.
To run the tests:
pip install -e ".[dev]"
pytestNo test touches the network.
All commands accept --help for the full option list. --tickers takes a
comma-separated list (--tickers AAPL,MSFT,NVDA) and defaults to the built-in
universe. Dates are YYYY-MM-DD.
Download daily close prices into the local SQLite cache and summarize coverage per ticker.
quantdesk fetch AAPL MSFT NVDA --start 2018-01-01 --end 2024-12-31
quantdesk fetch # default universe plus SPYRun one strategy over the chosen universe, net of commission and slippage, benchmarked against SPY. Optionally apply a 10% annualized volatility target and write an HTML tearsheet.
quantdesk backtest --strategy tsmom --tickers AAPL,MSFT,NVDA --start 2018-01-01
quantdesk backtest --strategy xsmom --vol-target --report reports/xsmom.htmlRun every registered strategy on identical data and print one table of metrics, net of costs.
quantdesk compare --tickers AAPL,MSFT,NVDA,AMZN,GOOGL,META --start 2018-01-01--synthetic replaces the fetch with seeded random-walk data for offline
smoke runs; the output is loudly labeled and meaningless as research.
Print the latest target weights for a strategy and the paper orders that would rebalance the local paper portfolio into them. This only prints suggestions; nothing is executed.
quantdesk scan --strategy dmaExecute the suggested rebalance against the local paper broker at the latest
close, with slippage applied against you. State persists in
data/paper.sqlite across runs — but that path is relative to the directory
you run from, so always run from the same directory (or pass --db with an
absolute path) to keep one continuous portfolio.
quantdesk paper-apply --strategy dmaShow paper-broker cash, equity, open positions marked at the latest cached close, and recent fills.
quantdesk paper-statusThe full pipeline on the default universe: fetch, compare all strategies, and
render reports/demo.html for volatility-targeted time-series momentum.
quantdesk demo| Key | Strategy | Idea |
|---|---|---|
tsmom |
Time-series momentum | Long each asset whose own trailing return (skipping the most recent month) is positive. |
xsmom |
Cross-sectional momentum | Long the top N assets ranked by trailing return, equal weight. |
meanrev |
Mean reversion | Long assets whose short-term return z-score is deeply negative; exit when it normalizes. |
dma |
Dual moving average | Long where the fast moving average is above the slow one. |
Each strategy's docstring describes both the economic rationale and the known failure modes — momentum crashes at sharp reversals, mean reversion loses when a "dip" is actually new information, crossovers whipsaw in sideways markets.
quantdesk/
data.py Yahoo/Stooq downloaders, source chaining, SQLite price cache
strategies.py Signal generators that emit daily target weights
risk.py Volatility targeting, position caps, drawdown guard
backtest.py Execution-lag engine, cost model, walk-forward analysis
metrics.py CAGR, Sharpe, Sortino, max drawdown, Calmar, hit rate
broker.py SQLite paper broker and an Alpaca paper-only adapter
report.py Self-contained HTML tearsheets (inline SVG, no external assets)
cli.py Typer commands wiring the modules together
Data flows one way: prices come from data.py, strategies turn prices into
weights, risk.py optionally rescales those weights, backtest.py turns
prices plus weights into returns and metrics, and report.py renders the
result. The broker is a separate ledger fed by the same target weights.
The backtest engine enforces a strict no-lookahead convention, documented here because every strategy and overlay must respect it:
rets = prices.pct_change()— simple daily returns.weights_used = weights.shift(1)(missing values filled with 0): a weight decided at the close of day t earns day t+1's return. A signal can never profit from the same bar that produced it.gross_t = sum(weights_used_t * rets_t)turnover_t = sum(|weights_t - weights_{t-1}|)cost_t = turnover_t * (commission_bps + slippage_bps) / 10000, charged on day t (defaults: 1 bp commission, 2 bps slippage).net = gross - cost, andequity = cumprod(1 + net).
The test suite includes a lookahead "canary": weights set to the sign of the next day's return — a cheating oracle — must produce roughly zero performance once run through the engine. If that test ever shows spectacular returns, the engine is leaking tomorrow's information into today.
Strategy weights themselves must only use information up to and including the row they are written on, and the sum of absolute weights per row must stay at or below 1.0 before any risk overlay is applied.
Prices come from two free sources tried in order: Yahoo Finance's unofficial
v8 chart API first, then Stooq's daily CSV endpoint as a fallback. Both are
keyless; both can throttle or change without notice, which is exactly why
there are two. Yahoo's close column uses the adjusted close (comparable to
Stooq's split-adjusted series); its open/high/low/volume stay unadjusted,
which is fine here because every computation downstream consumes closes only.
Bars are cached in data/cache.sqlite, so repeated runs do not refetch. The
cache path is relative to the directory you run from — a run from elsewhere
starts a fresh cache — and fetch prints the resolved path it used. Tickers
are written in their plain exchange spelling (for example BRK-B); the data
layer maps them to each source's format (BRK-B for Yahoo, brk-b.us for
Stooq). Gaps are forward-filled up to 3 days; leading missing history is
dropped per ticker. When the sources have no new bars for an already-cached
ticker (a weekend or holiday run, or an outage), the cached history is served
instead of erroring, and a single failing ticker is dropped with a warning
rather than aborting the rest. Free data has errors and missing sessions —
treat results accordingly.
Both endpoints sometimes front their data with rate limits or
browser-verification pages, which this client does not attempt to bypass;
when every source fails, requests raise a DataError naming each source's
problem. Tickers with cached history fall back to the cache when that
happens; only when no requested ticker has any usable data does the error
propagate. The demo command falls back to clearly labeled synthetic data in
that case, and compare --synthetic runs offline by design. All other
commands report the error and exit.
PaperBroker keeps cash, positions, and fill history in a local SQLite file
(data/paper.sqlite by default; override with --db). The default path is
relative to the current working directory, so each directory gets its own
ledger; paper-status prints the resolved path so a missing portfolio is
traceable to the directory it lives in. Fills are simulated at the latest
close with slippage applied against you.
The optional Alpaca adapter talks only to https://paper-api.alpaca.markets
and raises if constructed with any other base URL. Credentials are read from
the ALPACA_KEY_ID and ALPACA_SECRET_KEY environment variables (see
.env.example); they are never logged and must never be committed.
MIT — see LICENSE.