QuantLab is a modular Python platform for designing, backtesting, and validating systematic strategies under configurable, explicitly modelled execution and transaction-cost assumptions. It turns a financial hypothesis into a reproducible, bias-aware experiment: you download and validate market data, build features and signals, run vectorised backtests with explicitly modelled costs, measure performance and risk, validate out-of-sample with walk-forward analysis, and generate an honest research report β all driven by one YAML config, with a Python/notebook API for deeper robustness analysis (sensitivity, bootstrap, permutation).
The project focuses on rigorous and reproducible empirical research rather than maximizing historical performance.
π Documentation
- Delayed-execution barrier β signals are strictly shifted before returns; the separation between signal at t, position at t+1 and realised return is enforced and unit-tested. This mechanically prevents a position from earning the return that ends on the same bar its own signal was formed on β the common look-ahead leak β but does not, by itself, guarantee that a custom strategy's own feature/signal construction is causal (e.g. a strategy that reads future rows directly out of its input data); that remains the strategy author's own responsibility.
- Configurable modelled costs β explicit commission, spread and (constant or volume-based) slippage; every result reports gross vs net.
- Walk-forward validation β expanding/rolling windows, parameter selection on validation only and out-of-sample stitching; separate robustness tools provide sensitivity heatmaps, bootstrap, permutation and stress tests.
- Risk analytics β Sharpe, Sortino, Calmar, max drawdown, VaR/CVaR, exposures, benchmark alpha/beta, and more, implemented from first principles.
- Automated reports β self-contained HTML research report with an honest
limitations section, written to the experiment directory automatically by
every
quantlab backtest/walk-forwardCLI run; the dashboard generates the same report in memory for on-screen preview and download instead of writing it to disk. CallingBacktestEnginedirectly through the Python API requires the explicitresult.to_html(...)call shown below.
One BaseStrategy interface behind six reusable, tested strategies: buy &
hold, time-series momentum, cross-sectional momentum, mean reversion
(z-score, RSI, percentile, Bollinger and distance-to-MA), trend following,
and pairs trading (hedge ratio + ADF-gated spread). See
docs/strategies.md for each strategy's own
assumptions and parameters.
Robust Cross-Sectional Momentum Across Liquid Multi-Asset ETFs β
Can cross-sectional momentum generate stable out-of-sample risk-adjusted
returns across liquid ETFs after transaction costs and volatility targeting?
One example of the kind of study the platform is built for; walk through it in
notebooks/02_momentum_research.ipynb
and notebooks/05_robustness_analysis.ipynb.
reports/generated/ is a build artefact, deliberately excluded from version
control β the full HTML report at
reports/generated/cross_sectional_momentum_etfs/report.html is not
checked into this repository and will not exist until you generate it
yourself:
quantlab walk-forward --config configs/momentum_sp500.yamlUse walk-forward, not quantlab backtest β the latter deliberately clears
the walk-forward CSVs and out-of-sample metrics this report includes. This
produces the full 26-fold report the "walk-forward out-of-sample" row below
is drawn from; the other rows in the table come from their own respective
experiments/configs.
Representative outputs from the shipped configurations, including negative results, computed from real Yahoo Finance / Binance data (2008β2025 for ETFs, 2018β2025 for BTC), net of modelled transaction costs:
| Experiment | Universe | Period | CAGR | Sharpe | Max DD | Fills |
|---|---|---|---|---|---|---|
| Cross-sectional momentum (example above) | 8 multi-asset ETFs | 2008β2025 | 5.5% | 0.43 | β15.8% | 1,514 |
| β³ walk-forward out-of-sample | same | 26 folds | 4.6% | 0.31 | β18.5% | β |
| Mean reversion (z-score) | 5 equity ETFs | 2010β2025 | 4.4% | 0.25 | β34.6% | 3,351 |
| Pairs trading (EWA/EWC, vs SPY) | 2 country ETFs | 2010β2025 | 0.9% | β0.35 | β8.0% | 1,314 |
| Trend following (BTC) | BTCUSDT | 2018β2025 | 30.3% | 0.93 | β43.9% | 1,563 |
Every number above β including the negative Sharpe on the pairs trade β is
reported as computed. These are historical and conditional: they depend
on the period, the assumptions, the cost model, the parameters, and data
quality. They are not predictions. See each report's Limitations section
and docs/limitations.md.
Five executed research notebooks under notebooks/, run for
real against cached Yahoo/Binance data :
01_data_quality.ipynbβ coverage, gaps, return sanity checks.02_momentum_research.ipynbβ a full study end-to-end, including walk-forward.03_mean_reversion_research.ipynbβ RSI vs Bollinger vs z-score.04_pairs_trading_research.ipynbβ hedge ratio, ADF test, spread trading.05_robustness_analysis.ipynbβ sensitivity heatmap, bootstrap, stress tests, permutation test.
Regenerate them (after quantlab download for each config) with
python scripts/build_notebooks.py.
Interactive explanations, assumptions, parameter behavior, diagnostics, and
testing tools for each of the six implemented strategies. Each
strategy opens its own detail page with a live, editable parameter
laboratory (against real downloaded data, a local CSV, or a bundled offline
synthetic dataset for zero-setup exploration). See
docs/strategy_explorer.md.
streamlit run src/quantlab/dashboard/app.pyA Backtest / Walk-forward / Strategies mode switch sits above the
sidebar. Walk-forward mode runs the same train/validation/test parameter
selection as quantlab walk-forward, with its own sidebar (windows,
expanding mode, optimization metric, parameter-grid picker) and
Results/Trades/Robustness/Report tabs built from the stitched out-of-sample
result, driven by a live progress bar with an ETA while a run is in flight.
Both modes' Robustness tab includes stress tests, block bootstrap, a Monte
Carlo permutation test and a 2-parameter sensitivity heatmap, individually or
via "Run all robustness tests" β in Walk-forward mode, sensitivity and most
stress-test scenarios re-run the whole selection process per cell/scenario;
commission/slippage stress scenarios instead re-score cached candidate
weights under the new costs, re-selecting each fold's winner (see
docs/validation.md for exactly what's
cached vs. recomputed).
Multiple data sources (Yahoo Finance for equities/ETFs/indices, Binance for crypto OHLCV) are normalised to one canonical schema and cached as Parquet before reaching any strategy.
flowchart LR
A[Market Data Sources] --> A2[Raw Data Inspection]
A2 --> B[Data Cleaning]
B --> C[Final Validation]
C --> E[Strategy using Feature Functions]
E --> F[Portfolio Allocation]
F --> G[Constraints]
G --> H[Execution Model]
H --> I[Backtest Engine]
I --> J[Risk Metrics]
I --> K[Trade Log]
I --> L[Equity Curve]
J --> M[Validation]
K --> N[Reporting]
L --> N
M --> N
N --> O[Dashboard]
Data β Inspection β Cleaning & Validation β Feature Functions β Strategy
Signals β Portfolio Allocation β Execution Costs β Backtest
Accounting β Risk & Performance β Validation & Reporting
Three interfaces sit on top of this pipeline: a Python API, a Typer CLI, and the Streamlit dashboard above.
QuantLab supports Python 3.12 and 3.13, the two versions exercised by CI.
git clone https://github.com/sefaav/QuantLab.git
cd QuantLab
# Linux / macOS
python -m venv .venv
source .venv/bin/activate
# Windows (PowerShell)
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -e ".[dev,dashboard,yahoo,extra,docs,notebooks]"Fully self-contained β runs offline against the synthetic demo data already
committed under data/raw/ (no network access needed):
from quantlab.config import ExperimentConfig
from quantlab.data.loader import DataLoader
from quantlab.strategies.momentum import CrossSectionalMomentumStrategy
from quantlab.portfolio.allocator import InverseVolatilityAllocator
from quantlab.execution.execution_model import ExecutionModel
from quantlab.backtesting.engine import BacktestEngine
config = ExperimentConfig.from_yaml("configs/demo_offline.yaml")
data, _report = DataLoader().load(config) # loads data/raw/{SPY,QQQ,TLT,GLD}.csv
strategy = CrossSectionalMomentumStrategy(
lookback_period=189,
skip_period=21,
top_fraction=0.5,
long_short=False,
)
allocator = InverseVolatilityAllocator(volatility_window=63, maximum_weight=0.60)
result = BacktestEngine().run(
data=data, # canonical long OHLCV frame
strategy=strategy,
allocator=allocator,
execution_model=ExecutionModel.from_config(config.execution),
config=config,
)
print(result.summary())
result.to_html("reports/generated/demo/report.html")To run the same experiment on real Yahoo Finance / Binance data instead, swap
in configs/momentum_sp500.yaml (or another shipped config) after making its
remote data available with
quantlab download --config configs/momentum_sp500.yaml; that command
reuses a valid local cache when one already covers the requested period.
quantlab download --config configs/momentum_sp500.yaml
quantlab backtest --config configs/momentum_sp500.yaml
quantlab walk-forward --config configs/momentum_sp500.yaml
quantlab stress-test --config configs/momentum_sp500.yaml
quantlab bootstrap --config configs/momentum_sp500.yaml
quantlab permutation-test --config configs/momentum_sp500.yaml
quantlab sensitivity --config configs/momentum_sp500.yaml
quantlab robustness --config configs/momentum_sp500.yaml
quantlab report --experiment cross_sectional_momentum_etfs
quantlab dashboard
quantlab --helpstress-test/bootstrap/permutation-test/sensitivity each run one
robustness technique (with a matching --n-iterations/--block-size/
--param-x etc. override); robustness runs every technique enabled under
a config's robustness: block in one pass. All five branch on
validation.method: with walk_forward, each starts from the same
walk-forward-stitched out-of-sample result rather than a single backtest, so
the evidence never silently comes from a different validation method than
the one configured. In that mode, sensitivity and most stress-test
scenarios re-run the whole selection process; commission/slippage stress
scenarios instead re-score cached candidate weights under the new costs,
re-selecting each fold's winner (see
docs/validation.md for exactly what's
cached vs. recomputed). bootstrap and permutation-test do neither: they
resample or permute the walk-forward's already-realised out-of-sample
return series statistically, without touching the selection process at all.
walk-forward, stress-test, sensitivity and robustness show a live
progress bar with an ETA in the terminal, and checkpoint their progress to
disk as they go β an interruption (Ctrl+C, a crash, closing the terminal)
resumes automatically on the next matching run instead of starting over.
Pass --fresh to discard a checkpoint and start clean.
Each backtest, walk-forward or report run writes a structured artefact
folder under the generated-reports directory. In a source checkout this is
reports/generated/<experiment>/; after a regular package installation it is
~/.quantlab/reports/generated/<experiment>/. The folder contains the config
snapshot, metrics, equity curve, trades, positions, figures, HTML report, data
hash, best-effort Git state and installed dependency versions. Combined with
the pinned uv.lock (restored without modification by uv sync --locked),
these artefacts detect input, code and dependency changes, but do not capture
the complete operating-system environment (see docs/limitations.md).
Unit + integration tests, >82% core-library coverage target (CI-enforced,
--cov-fail-under=82), Ruff + mypy
clean. CLI and dashboard behaviour are exercised separately by integration
and Streamlit AppTest tests. The direct commands work in PowerShell, Linux
and macOS:
python -m pytest -m "not network"
python -m pytest -m "not network" --cov=quantlab --cov-report=term-missing
python -m ruff check src tests scripts
python -m ruff format --check src tests scripts
python -m mypy src tests scriptsGNU Make is optional. When it is installed, make test, make coverage,
make lint and make type-check call the same tools.
Adjusted prices, potential survivorship bias (current-composition universes),
no real market impact, simplified slippage, no taxes or regulatory constraints,
simplified liquidity, possible data-snooping, and non-predictive historical
results. These are documented in docs/limitations.md
and emitted automatically into every report.
Found a bug, have a question, or want to suggest an improvement? Please open an issue on GitHub β feedback and bug reports are always welcome.
This project is intended for educational and research purposes only. It does not constitute investment advice, and historical performance does not guarantee future results. The goal is not to claim a profitable strategy β it is to demonstrate a rigorous, reproducible research process.
Built by sefaav. Find more projects on GitHub.
MIT β see LICENSE.


