Python library
A quant library that refuses to guess.
QuantKit computes and audits quantitative-finance quantities. Its two commitments are numerical correctness — every disputed convention is an explicit parameter with a documented default — and lookahead safety, built on information sets rather than data frames.
- Tests
- 650
- Failures
- 0
- Modules
- 45
- Formulas
- 65
- Defects found
- 17
Lint, format and type checks clean · deterministic suite · no network, no wall clock
01What it is
and, just as importantly, what it is not
It is
- A measurement library: returns, risk, covariance, factor models, portfolio construction, costs, backtesting
- A set of lookahead-safe historical APIs
- An audit tool — it will tell you when a signal reads the future
- Explicit about every convention libraries disagree on
It is not
- A trading bot or execution system
- An alpha library or strategy collection
- A brokerage or market-data integration
- A price predictor of any kind
QuantKit ships no live market-data connection, by design. A library whose tests depend on a vendor API cannot be verified reproducibly. Its bundled datasets are simulated from a known factor model — they are validation fixtures, not evidence about any market.
02The lookahead problem
the flagship feature
A backtest that reads the future does not fail. It succeeds — spectacularly — and tells you
nothing. The usual defence is discipline: remember to lag the signal, remember that the
rolling mean at t already contains t. Discipline is exactly what
fails at 2am.
QuantKit offers three defences instead, and is careful never to conflate them. They differ in kind, not just in strength.
| Layer | Kind of protection | What it actually establishes |
|---|---|---|
| InformationSet | Prevention by construction | The strategy receives an object from which future rows are absent. Reading them is not forbidden — it is impossible. |
| Timing | Runtime enforcement | Observation, decision and execution are separate fields. Every recorded decision satisfies observation ≤ decision ≤ execution, checked at construction. |
| assert_causal | Heuristic probe | Recomputes a function without the future and reports whether the answer moved — on the inputs and cut points probed. A necessary-condition test. |
assert_causal does not prove the absence of lookahead. It runs a function on
truncated and perturbed inputs and reports whether the output moved. A leak on an unprobed
row, an unexercised branch, or a code path the test data never reaches will pass. Only
InformationSet is a guarantee about arbitrary strategy code.
What the audit does catch, reliably, is the entire family of bugs that affect every row — which is nearly all of them in practice:
# each of these has shipped in a real backtest
assert_causal(lambda p: p.rolling(20).mean(), prices) # passes
assert_causal(lambda p: p.shift(-1), prices) # LookaheadError
assert_causal(lambda p: p.rolling(21, center=True).mean(), prices)
assert_causal(lambda p: (p - p.mean()) / p.std(), prices)
assert_causal(lambda p: p[::-1].cummax()[::-1], prices)Data with a reporting lag is declared, and the point-in-time cut respects it — a fundamental filed 45 days after its period end becomes visible 45 days late, not on the date the vendor stamped it:
run_backtest(prices, strategy, panels={
"fundamentals": Panel("fundamentals", filings,
observation_lag=pd.Timedelta(days=45)),
})Same-bar execution — filling at the close you decided from — is refused unless you
pass allow_same_bar=True, so the assumption lands in your code and in the emitted
timeline rather than in a footnote nobody reads.
03The execution lag is measured, not asserted
a behavioural check on the engine
With execution_lag=1, a decision made at bar t fills at the
close of t+1 — so the first return the new book earns is bar
t+2's. That is a testable claim. Give a strategy perfect foresight at
each horizon and see where it pays.
Value of perfect foresight, by horizon
Final equity as a multiple of an honest equal-weight book. Log scale — the differences span two orders of magnitude.
04Conventions it refuses to guess
where two libraries disagree on the same data
Two implementations of “annualized volatility” can differ by 5% and neither is wrong — they made different, unstated choices. Every one of these is a named parameter in QuantKit, and every CLI command prints the convention it used alongside the number.
| Ambiguity | QuantKit default | Alternative |
|---|---|---|
| Standard deviation | ddof=1 (sample, pandas) | ddof=0 (NumPy) |
| Downside-deviation denominator | "full" (÷ N) | "downside" |
| VaR / ES sign | "loss" (positive) | "return" |
| Turnover | one-way (½Σ|Δw|) | two-way |
| Scalar risk-free rate | annual, geometric | "simple" |
| Vector risk-free rate | already per-period | — |
| Spread cost | half the quoted spread | cross_full=True |
| Trading year | 252 | calendar_daily, or any number |
| Execution | next bar's close | next open · k bars · same bar |
One documented claim turned out to be false, and saying so is the point of the exercise. “Arithmetic annualized return always exceeds the CAGR” is wrong. AM ≥ GM holds per period, always — but annualizing applies different transforms (linear scaling versus compounding), and over 252 periods the convexity of compounding can reverse the ordering. Both directions are now in the docs and in the tests.
05How it is validated
two kinds of evidence, deliberately kept apart
Independent validation compares a result against something QuantKit did not produce: a third-party library, a literal transcription of a published formula, or a closed-form solution derived analytically. That is the evidence the implementation is right.
Self-consistency validation checks that QuantKit agrees with itself — an identity holds, a fast kernel matches its own exact kernel, a round trip returns its input. These catch real defects, but cannot detect an error shared by both sides. The distinction is marked on every section of the validation document.
| QuantKit | Checked against | Kind | Agreement |
|---|---|---|---|
| ledoit_wolf_constant_correlation | Literal transcription of Ledoit & Wolf (2004), JPM 30(4) | independent | 2.7e-20 |
| ledoit_wolf_identity | sklearn.covariance.ledoit_wolf | independent | 2.7e-20 |
| oracle_approximating_shrinkage | Chen et al. (2010), eq. 23, literal | independent | 8.1e-20 |
| ewma_covariance | pandas.ewm(adjust=True).cov() | independent | 5.4e-20 |
| rolling_var / rolling_std | pandas.rolling() | independent | 5.3e-17 |
| min_variance (SLSQP) | Closed form Σ⁻¹1 ⁄ 1ᵀΣ⁻¹1 | independent | 8.1e-07 |
| factor_exposures | numpy.linalg.lstsq | independent | 1e-10 |
| risk_contribution | Euler identity Σ CRC = σₚ | self-consistency | 2.8e-17 |
| Backtest accounting | Identity E_post = E_pre − cost | self-consistency | exact |
The literal transcriptions earned their keep
The reference implementations in the test suite are deliberately slow: explicit double loops, no vectorisation, no shared code with the library. That is what makes agreement meaningful — and it is what caught the worst bug in the project. The vectorised Ledoit–Wolf θ term used Σ x²ᵢ xⱼ where the derivation requires Σ x³ᵢ xⱼ. It produced entirely plausible shrinkage intensities — 0.91 instead of 0.75 — that no eyeball would ever have caught.
Seventeen real defects were found this way in total: seven while writing the tests, ten more in a dedicated adversarial audit before release. Every one shared a shape — a routine returning something that looked like a valid answer where it should have refused. A levered wipeout coming back sign-flipped, so a long position reported as a short. An R² of −15 for a constant series. A single NaN silently poisoning every subsequent row of a rolling window. Each has a regression test written as the smallest case that would have caught it.
06What is in it
eleven modules, one Python API
returns
Simple, log and cumulative returns; wealth index; CAGR; excess and active returns.
risk
Volatility, downside deviation, Sharpe, Sortino, Calmar, Omega, drawdowns, VaR (historical / Gaussian / Cornish–Fisher), expected shortfall.
rolling
Causal trailing volatility, beta, covariance, correlation, Sharpe, drawdown and VaR — with a fast prefix-sum kernel and an exact reference kernel.
covariance
Sample, EWMA (RiskMetrics), Ledoit–Wolf on two targets, OAS, PSD repair and conditioning diagnostics.
factors
OLS exposures with OLS or Newey–West HAC errors, rolling exposures, return attribution, systematic/specific risk decomposition.
portfolio
Weights, drift, Euler risk contributions, closed forms, and constrained SLSQP optimization including risk parity and the efficient frontier.
costs
Fixed, proportional, spread, per-asset linear and square-root market impact — composable with +. Turnover that accounts for drift.
backtest
Timing policy, share-level accounting, engine, and results carrying a full timing audit trail.
lookahead
Information sets, timing triples, lagging utilities and causality auditing.
validation
Structural and numerical checks plus a non-raising data-quality audit.
data
Four bundled deterministic datasets and validating CSV/Parquet readers.
07Using it
Python API is primary; the CLI is for quick audits
Python 3.10 or newer; numpy, pandas and scipy are the only runtime dependencies.
pip install -e .import quantkit as qk
prices = qk.data.load_sample_prices()
returns = qk.simple_returns(prices)
qk.sharpe_ratio(returns["ALFA"], risk_free=0.02, freq="daily") # 1.019
qk.max_drawdown(returns["ALFA"]) # -0.322
qk.value_at_risk(returns["ALFA"], 0.99) # 0.0366 — a loss
result = qk.run_backtest(prices, qk.backtest.EqualWeight(),
rebalance="monthly",
costs=qk.ProportionalCost(bps=5))
print(result.summary())$ quantkit metrics --dataset sample_prices --risk-free 0.02
$ quantkit risk --dataset sample_prices --confidence 0.99
$ quantkit optimize --dataset sample_prices --objective min-variance --long-only
$ quantkit backtest --dataset sample_prices --strategy min-variance --cost-bps 5
$ quantkit lookahead --prices my_prices.csv --window 20 --exhaustive
optimize and lookahead exit non-zero when the optimizer fails to
converge or a causality violation is found, so they compose into CI.
08Known limitations
stated plainly, not buried
A correctness claim is only worth what its caveats admit. These are on the front page of the repository too.
| Limitation | Detail |
|---|---|
| No live market data | By design. Bundled datasets are simulated validation fixtures and say nothing about any market. |
| Causality auditing is a probe | assert_causal samples. A leak on an unprobed row or an unexercised branch will pass. |
| Market impact is uncalibrated | The functional form is tested; the coefficient η is a scenario input. Published estimates span 0.3–1.0 — wider than the parameter itself. |
| Benchmarks are machine-specific | Measured on one Apple-silicon laptop with one NumPy/pandas/BLAS combination. No general speed claim is made. The rolling kernel measures 1.4–1.9× slower than pandas' compiled kernel, and that is reported rather than omitted. |
| Optimization inherits its inputs | Closed forms invert the covariance and inherit its conditioning; group and leverage constraints use non-smooth formulations that report failure cleanly. |
| Backtest sizing overshoot | Targets are sized on pre-trade equity, so realised weights exceed target by E ⁄ (E − C). Chosen so every number stays reproducible by hand. |
| PSD repair is eigenvalue clipping | Optimal only among matrices sharing the input's eigenvectors — not Higham's nearest-correlation algorithm. |
The slower-than-pandas result is kept deliberately. pandas dispatches to a purpose-built compiled kernel; QuantKit computes prefix sums in NumPy because that implementation is transparent and checkable line by line against its own exact kernel. Both are O(n) — it is a constant factor, not a complexity difference. If rolling moments dominate your workload, use pandas; QuantKit agrees with it to 5.3e-17, which is exactly why substituting it is safe.