Engineering model
Correctness is a process, not a claim.
Any library can assert that its numbers are right. What makes the assertion worth anything is the machinery standing behind it — what the code is checked against, what it refuses to do, and what it admits it has not established. This is that machinery.
- Layers
- 4
- Tests
- 650
- Evidence kinds
- 6
- Defects caught
- 17
- Release gates
- 9
01Architecture
four layers, one direction of dependency
The package is a stack. Each layer may use the layers below it and never the reverse, so the dependency graph is acyclic and a change to portfolio construction cannot reach into the estimators. Lookahead safety is the one concern that cuts across all four — which is why it is its own module rather than a convention inside each.
Module layering
Arrows point the only way dependencies are allowed to run.
The coercion boundary
Every public function accepts a NumPy array, a pandas Series, or a
DataFrame; computes in NumPy; and returns a container matching what it was
given. A frame in, a Series of per-asset results out. A 1-D array in, a 1-D
array out. That contract lives in one internal module, so the wrapping rules are written once
rather than re-improvised in forty places — and when it was wrong, it was wrong in exactly
one place and fixed there.
Failure is typed
Everything raised derives from QuantKitError, split into
ValidationError, AlignmentError,
InsufficientDataError, ConstraintError,
OptimizationError, TimingError and
LookaheadError. Callers can catch the whole library with one clause or
discriminate precisely. Messages name the offending value and say what to do about it —
never just that something went wrong.
02Two models do the real work
information sets, and share-level accounting
The information model
A backtest bar runs in three ordered steps, and the ordering is the whole design:
for pos, bar in enumerate(index):
# 1. decide — build a view truncated at this bar, call the strategy
# 2. execute — fill any decision whose lag has now elapsed
# 3. mark — record equity as cash + shares · close[pos]Because holdings are marked at every close, a position opened earlier automatically earns this bar's return. There is no separate “apply returns” step that could double-count or be skipped — a whole category of backtest bug is removed by not having the step.
The view handed to the strategy is truncated by observation time, not by the index label the vendor stamped on the row. Arrival times are computed once when a panel is registered and verified monotonic, so each bar's cut is a binary search rather than a scan — the loop stays linear in the number of bars, not quadratic.
The accounting model
Positions are tracked in shares and cash, never in weights. Weights are derived for reporting only. At execution price p, with pre-trade shares n and cash c:
E_pre = c + nᵀp
n_tgt = w · E_pre / p
c' = c − Δnᵀp − C(Δn, p)
E_post = c' + n_tgtᵀp = E_pre − CEquity falls by exactly the cost charged and by nothing else. That identity is asserted at every rebalance of every test backtest. Weight-space accounting would have to re-derive drift each period and would accumulate error; share-space accounting is exact, and every number in a result can be reproduced with a calculator from the price series.
A deliberate imprecision, documented rather than hidden. Target shares are sized on pre-trade equity and the cost is then deducted from cash, so realised weights overshoot the target by E ⁄ (E − C) — about 5bp at 5bp cost and full turnover. The exact alternative requires solving a fixed point, because the cost depends on the trade which depends on the cost. The closed-form rule was chosen so every figure stays hand-checkable, and the consequence is written down.
03The evidence model
six kinds of check, and they are not equal
Test count is a vanity metric. What matters is what each test compares against, because that determines what a pass actually rules out. These are ordered by how much independence they carry — the first four can detect an error in the implementation; the last two cannot detect an error shared by both sides of the comparison.
-
Literal transcriptions of published papers independent
Explicit double loops, no vectorisation, no shared code with the library. Ledoit–Wolf (2004, both targets) and Chen et al. (2010) reimplemented from the papers and compared to the fast path. Agreement: 2.7e-20.
-
Third-party reference libraries independent
scikit-learn,
pandas.ewm,pandas.rolling,numpy.linalg.lstsq,scipy.stats. Agreement from 5.4e-20 to 5.3e-17. -
Closed-form solutions independent
Derived analytically, not from the code under test. The numerical optimizer must reproduce the tangency and minimum-variance portfolios where the budget is the only binding constraint. Plus optimality checks that need no formula at all: the first-order condition, 200 random feasible perturbations failing to beat the solution, and marginal risk matching a central-difference derivative.
-
Hand calculations independent
Arithmetic done on paper and written into the test docstring, so a failure points at a specific step. The reference backtest — four bars, two assets — is worked through including intermediate share counts and financed cash balances.
-
Property tests self-consistency
Invariants over adversarially generated input: risk contributions summing to total volatility, drawdown bounded in [−1, 0] and causal, volatility positively homogeneous, Sharpe scale-invariant. Deterministic seeds, so a run always passes or always fails.
-
Internal identities self-consistency
The accounting identity, the fast kernel matching its own exact kernel, round trips returning their input. These caught real defects — but they cannot detect an error present on both sides.
Why the slow transcriptions earn their cost. They 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 — and no amount of staring at the output would have revealed it. Only a second implementation that shared no code could.
04What the process actually caught
seventeen defects, and the shape they share
Seven were found while writing the tests; ten more in a dedicated adversarial pass before release that hunted specifically for plausible output rather than crashes. Nearly every one had the same shape: a routine returned something that looked like a valid answer where it should have refused or reported “undefined”.
| Failure mode | Example found | Caught by |
|---|---|---|
| Wrong maths, plausible output | Shrinkage θ term using the wrong power — intensity 0.91 instead of 0.75 | paper transcription |
| Sign inversion | A levered wipeout returned sign-flipped weights: a long position reported as a short | adversarial probe |
| Degenerate answer, reported as success | Optimizers accepting no budget constraint returned the empty portfolio with success=True | adversarial probe |
| Numerical noise read as signal | R² of −15 for a constant series; a Sharpe ratio of 5e15 instead of infinity | edge-case test |
| Silent disagreement with the reference | One NaN poisoning every later row of the fast rolling kernel, where pandas flags only the affected windows | adversarial probe |
| Silent reordering | Alignment reordering one input to match an unsorted other, so rolling windows ran over scrambled time | adversarial probe |
| Unit assumption | Frequency inference reading a datetime index's raw integers without checking its resolution | reference test |
| Non-finite escaping | Weight normalisation overflowing to [inf, nan] on a denormal normaliser | adversarial probe |
| Lost labels | Weight constructors dropping the pandas index, treating a per-asset vector as a row | unit test |
| Dead code hiding a limit | A branch x if c else x whose docstring claimed a range the code never covered | linter |
Each has a regression test written as the smallest case that would have caught the original. The performance work found its own class of defect: an earlier version of the causality audit compared results row by row through pandas, which made the suite verifying the flagship feature take over seven minutes — expensive enough that it would eventually have stopped being run.
05The release pipeline
nine gates, all of which must pass
Nothing here is exotic. The point is that it runs in full, that a failure blocks, and that no gate is reported as passing when its tooling is absent.
- Tests
- 650 passed
- Isolation
- per-module
- Lint
- ruff clean
- Format
- 74 files
- Types
- mypy clean
- Build
- wheel + sdist
- Metadata
- twine passed
- Clean room
- wheel install
- CLI
- 9 commands
Two of those deserve a note. Isolation runs every test module on its own, so a hidden order dependency cannot hide behind a green full-suite run. Clean room installs the built wheel into a fresh interpreter and exercises the library and CLI from there, which is what catches a missing package data file that the source tree happens to supply.
The suite is deterministic by construction: no network, no wall clock, no unseeded randomness, and property tests pinned to fixed seeds. A failing run means the code changed, not that the day did.
06Performance, where it was earned
measured against itself, never against another library
Three optimisations came out of profiling rather than guessing. Each is QuantKit against an earlier QuantKit, with the before figure kept.
| Change | Mechanism | Before | After | Factor |
|---|---|---|---|---|
| Causality audit comparison | Row-by-row pandas .iloc replaced with one vectorised comparison | >7 min | 3.9 s | ~110× |
| Backtest bar loop | An index-equality fast path (a strictly stronger check) plus an O(1) arrival lookup | 67.7 µs/bar | 34.8 µs/bar | 1.94× |
| Constrained optimizer | Objective scaled to order 1, so the solver's absolute tolerance means something | 969 ms | 61.7 ms | 15.7× |
The third was not really a performance fix. A daily covariance makes the variance objective about 1e-4, and SLSQP's tolerance is absolute — so the solver was stopping after an improvement of 1e-8 relative, leaving the weights wrong in the fourth decimal. Dividing by the mean asset variance changes nothing mathematically and fixed both axes at once: agreement with the closed form went from 8.3e-4 to 8.1e-7, and the solve got 15× faster as a side effect. Accuracy and speed are not always opposed.
And the direction that went the other way. The rolling kernel measures 1.4–1.9× slower than pandas', because pandas dispatches to a purpose-built compiled kernel while QuantKit computes prefix sums in NumPy — an implementation that can be read and checked line by line against its own exact kernel. Both are O(n); it is a constant factor. This is reported rather than omitted, and no general speed claim is made for QuantKit anywhere. If rolling moments dominate your workload, use pandas — the two agree to 5.3e-17, which is precisely why substituting it is safe.
07What the model deliberately excludes
scope is a design decision
A library that does fewer things can make stronger claims about them. These absences are chosen, not pending.
- No live market data. A test suite that depends on a vendor API cannot be verified reproducibly — the numbers move under you, and a failure tells you nothing about your code.
- No strategies or alpha models. The reference strategies exist to exercise the engine and serve as templates. None is a claim about returns.
- No silent repair. Nothing is filled, coerced, clipped or renormalised behind your back. A NaN in a return series is a modelling decision only the caller can make.
- No hidden iteration. Where an exact answer would need a fixed point, the closed-form approximation is used and its consequence documented, so results stay reproducible by hand.
- No proof where there is only evidence. The causality audit is a sampled probe and says so; only the information set is a guarantee.