Documentation
Install it, run it on your own data, decide for yourself.
The engine is open source and runs offline. Part one is for whoever writes the code. Part two is for whoever has to act on what comes out of it.
Part one · The operating system
Install
Two runtime dependencies, numpy and scipy, both already present in a research environment. Importing the package makes no network call and needs no account.
pip install alphaengine
# factor decomposition and cointegration need statsmodels
pip install 'alphaengine[factors]'Everything in part one works on a laptop with the wifi off, forever, at no cost. It is not a trial and nothing in it expires. See pricing for where the paid line falls and why.
Your first sweep
The library does not backtest anything. It runs your backtest once per combination in a grid, then measures the result against the size of the search that produced it.
from alphaengine import sweep
r = sweep(
my_backtest, # YOUR function
{"fast": [5, 10, 20], "slow": [50, 100, 200]}, # the search
data=prices, # passed through untouched
)
r.n_trials
# 9That number is why the API is shaped this way. The correction that makes a Sharpe ratio honest needs to know how many variants you tested. Ask a person for it and you get the number that flatters them — not from dishonesty, but because nobody counts what they threw away. Running the grid makes the count len(grid), so the question never has to be asked.
n_trials is a property with no setter. There is no argument anywhere in this package that lets you tell it how many things you tried.
Reading the surface
Before any verdict, look at the neighbourhood. A result sitting on a broad plateau is robust. A single spike surrounded by failures is a result fitted to its own parameters.
r.surface()
# {
# 'shape': 'knife_edge',
# 'n_ok': 9,
# 'n_failed': 0,
# 'best_sharpe': 1.9143,
# 'median_sharpe': 0.221,
# 'share_within_20pct_of_best': 0.1111,
# 'plateau_centre': None,
# 'reading': 'One configuration performs and its neighbours do not. '
# 'Usually a fitted result.',
# }- shape
- plateau, ridge or knife_edge. Set by what share of the grid lands within 20% of the best result: 30% or more is a plateau, 10% or more a ridge, below that a knife edge.
- share_within_20pct_of_best
- The number behind the shape, so you are free to disagree with the thresholds.
- plateau_centre
- Per parameter, where the robust region sits and how wide it is. The actionable half: it tells you where to re-centre, not merely that your number was flattered.
- reading
- The shape in one sentence. The same string every time, so it can be quoted.
Reading the verdict
The verdict deflates the best result for the search that actually produced it, and reports what it needed in order to say so.
v = r.verdict()
v["n_trials"] # 9
v["n_trials_source"] # 'derived_from_grid'
v["deflated_sharpe"] # 0.3106
v["verdict"] # 'inconclusive'
v["data_hash"] # a content hash of what it ran on
v["min_track_record_length"] # how long this record must run to beat luckn_trials_source travels with the figure on purpose. A count that was asserted and a count that was counted are different claims, and every deflated number downstream rests on which one it is. When this package produces the figure the source is always derived_from_grid, because there is no other way to produce it here.
Probability of backtest overfitting is reported alongside, and is deliberately not the gate:
v["selection"]
# {
# 'question': 'was the choice among configurations informative?',
# 'not_a_verdict_on_the_edge': True,
# 'pbo': 0.48, ...
# }PBO answers a different question, and returns roughly 0.5 for genuinely near-tied top configurations even when the underlying edge is real. Treating it and the deflated Sharpe as two readings of the same thing would be a mistake, so the shape of the return says they are not.
The study file
A study is a JSON artifact holding what was tried, what came back, and a content hash of the data it ran on. Readable in a text editor, diffable, and versioned so it still parses in two years.
r.save("study.json")
# PosixPath('study.json')
from alphaengine import load
s = load("study.json")
s.schema_version # load() refuses an unknown MAJOR version
s.data_hash # a label can be changed to escape a history; an array cannot
s.n_trials_source # 'derived_from_grid'The version rule here is stricter than semver: a bump is required whenever a computed valuechanges, even when the signature is untouched. Once the numbers are published they are a contract, and somebody’s saved study has to reproduce.
A study written on a laptop with no account opens unchanged in the portal. That is the entire join between the two halves of this page.
Wiring your own backtest
Your function is called once per combination and must return a sequence of per-period returns. It is called as backtest_fn(data=data, **params) when data is given, and backtest_fn(**params) when it is not.
def my_backtest(*, data, fast, slow):
fast_ma = data.close.rolling(fast).mean()
slow_ma = data.close.rolling(slow).mean()
signal = (fast_ma > slow_ma).shift(1)
return (signal * data.close.pct_change()).dropna().to_numpy()
r = sweep(my_backtest, {"fast": [5, 10, 20], "slow": [50, 100, 200]}, data=px)- data
- Passed through untouched. Only hashed — never inspected, never stored.
- store_params
- OFF by default. Hashes are always kept, so runs stay comparable without the parameter values leaving your machine. The grid is often bigger intellectual property than the return series.
- on_error
- 'record' (the default) marks a failing combination and continues, because one bad corner of a grid should not lose the other ninety-nine results. 'raise' propagates.
The result holds the full trial matrix, which is what PBO needs and what a single-point run structurally cannot produce. That is the argument for running a grid rather than calling a calculator on one number.
What is in the core
Everything below is a pure function — no clock read, no environment read, no data-layer import. That is guaranteed by a test that scans the module tree without importing it.
from alphaengine.core import (
deflated_sharpe, # corrected for the size of the search
probabilistic_sharpe_ratio,
expected_max_sharpe, # what the best of N looks like under no edge
min_track_record_length, # how long before it beats luck
pbo_cscv, # probability of backtest overfitting
cpcv_score, # combinatorial purged cross-validation
performance_report,
compute_var_cvar,
run_backtest,
score_backtest,
technical_features,
)- VaR and CVaR
- Always POSITIVE loss magnitudes, with CVaR at least VaR. Sign conventions are the most common source of a wrong risk number, so this one is fixed everywhere.
- Calmar
- Geometric. Annualised return compounds rather than averaging.
- Sortino
- The denominator is target semideviation over all N periods, not the standard deviation of the negative subset.
Factor decomposition and cointegration live behind the factors extra, so statsmodels is not a hard dependency of the core.
What never leaves the machine
The package makes no network call. In the paid platform the same rule is enforced rather than promised: a market series is the feed and is never stored, while a price you transacted at is your record and is yours to keep.
- Your market data
- Never stored. Supplied per call, computed, discarded.
- Your model
- Never ingested. We do not take your alpha logic.
- Your parameter grid
- Hashed by default; values kept only if you ask for it.
- Your judgment
- Stored — theses, decisions, limits, your record. Scoped to you, encrypted, exportable, deletable.
The full line, item by item, is on the Trust page.
Part two · The portal
What the portal is for
Part one runs on one machine and answers one question: does this hold up? The portal exists for everything that happens after that answer has to reach a second person.
A fund at this size has a specific failure, and all three seats named it independently when we researched them: the most valuable thing anyone produces is a spoken explanation, the decision record does not exist, and every review is a reconstruction assembled backwards from a profit and loss number everyone already knows. The portal’s only job is to make that record a byproduct of the work instead of a chore after it.
Nothing in the portal should ask you to retype something the system already knows. If a screen makes you re-enter a number that exists upstream, that screen is wrong and we would like to hear about it.
The five objects
The whole product is five things that reference each other. Each points at the one below it by identity, never by a copy, so any figure can be traced back to the run that produced it.
- Study
- What was tried, how many ways, on what data. Produced by a sweep, not by typing. Frozen when published, and read as published rather than live.
- Thesis
- A study, plus what has to be true, what it is worth, what would falsify it, and the dates that settle it.
- Position
- A thesis, plus the size you chose, the limit that actually bound, your conviction at the time, and the fills against it.
- Book
- Positions rolled up to a sleeve, against the limits set for that sleeve, with every breach and what was decided about it.
- Packet
- All of the above for a period — frozen, countersigned, and exportable to somebody who was not in the room.
Two rules hold the chain together. Nothing is retyped at a handoff. And a figure that cannot carry the run that produced it does not travel.
Solo, Pod, Fund
Solo — one person, the whole loop
You are the quant and the PM. The handoffs still exist; they just happen six months later, when you have to explain to yourself why you are long that name. Solo makes the record for that conversation.
Runs persist as you work, with no save button to remember. Studies accumulate, including the ones that failed, so you stop rediscovering last quarter’s dead end. Theses carry what would change your mind, and the system tells you when it does. Over time the record learns what your conviction has actually been worth, and sizes to that rather than to how you feel today.
Pod — quants and a PM
The crossing this tier exists for is quant to PM. A study is handed across with its provenance attached, and the PM gets the surface they actually need: what was tried this week, how many ways, on what data, what died, and whether the survivor holds up.
The PM works in theses and positions. Sizing happens against the limits that apply, and the reason for the size is recorded at the moment of the decision — because whether a position is small from low conviction or from a bound limit are opposite facts about your process, and a return series cannot tell them apart afterwards.
What leaves is a ranked signal file: ticker, score, rank, target weight, dated and versioned. Your order management system turns that into orders. We never send a share count anywhere.
Fund — a CIO over several sleeves
A risk budget can only tighten. The budget a CIO sets and the mandate a PM already holds are merged on the server, taking the stricter of each limit — so a PM cannot decline a budget by not sending it, and a missing limit is silence rather than permission.
Breaches get a disposition: cut, waive or hold, with a reason, a name and an expiry. A limit with no recorded decision behind it is the single thing an operational due diligence team asks to see and nobody can produce.
Periods close. The review packet, the letter commentary, the committee pack and the diligence responses assemble from evidence already collected, and a countersignature attaches to frozen content rather than to a pointer at something still editable.
A screen-by-screen guide lands with the portal rebuild now under way. Until then this page describes what each seat does and what the objects mean, which is the half that will not change. See pricing or read the data boundary.