Article 1 of 7 — Building Momentum Systems Without Fooling Yourself. Next: The Hypothesis Register and the Stage-Gate Funnel
The problem this article solves is not a statistical one. It is the problem that comes before statistics: how to arrange the software so that the number of hypotheses tested is knowable at all. The “Illusion” posts argued that sequential optimization spends degrees of freedom you never record. You cannot fix that with a better test if the code itself makes it impossible to say what was tried. Every edit to a signal, every filter bolted onto an entry, every quick Python re-implementation “just to check something” is a trial that leaves no trace. So the first decision in the framework is architectural: where logic is allowed to live, in what form, and who is allowed to judge it.
A library of primitives, not a collection of strategies
All strategy logic lives in C#, as a library of building blocks for MultiCharts .NET. There are no strategies in the library, in the sense of a single object that says “buy here, sell there, size this much.” There are primitives, organized in four layers that do not know about each other.
The first layer is entry signals, which I call probes. A probe answers one question: given the bars up to now, is there a reason to propose a long, a short, or nothing. It does not know how the position will be exited, whether the market is in a tradeable state, or how large the bet will be. The second layer is exit logic: given an open position, when does it close. The third is regime filters and features — measures of efficiency, persistence, choppiness, volatility state — which produce a number or a state, not a trading decision. The fourth is sizing, which takes a proposed side and a confidence and returns a quantity.
A strategy is a composition of one element from each layer, assembled on the platform. The layers are combined at the edges, never fused. This is the ordinary advice about separation of concerns, but it has a specific purpose here that ordinary software hygiene does not. If a probe is a self-contained primitive, I can run it over the whole universe with a fixed exit and no filter and obtain a clean baseline. If a regime measure is a feature and not a condition wired into the entry, I can hand it to a downstream model and ask whether it carries information, rather than watching a Sharpe ratio move when I toggle it. The architecture is what makes the questions of Articles 4 and 5 askable in the first place.
Why a new idea is a new primitive
The rule that surprises people most is this: an existing primitive is never edited to try an idea. If I think a crossover would work better with a different smoothing, that is a new probe, with its own name, sitting next to the old one. If I think a breakout should require confirmation, that is a third probe. The library grows; it never mutates. There are around sixty regime primitives at the moment, and the number is a fact I can state because nothing was ever overwritten.
The reason is the accounting. An edit is a trial that erases its predecessor. Once the old version is gone, the comparison that motivated the change is gone with it, and so is the count. Six months later I have a probe that “works”, a vague memory of having tuned it, and no way to reconstruct how many versions were discarded on the way. The deflated Sharpe ratio of Part 2 needs the number of trials as input; a library that mutates cannot supply it. A library that only appends can, and does, because the register of Article 2 points at primitives by name and each name is a distinct object that has existed since the day it was created.
There is a second, more practical reason. When primitives are immutable, a trade list produced two years ago can be regenerated today from the same named building blocks, and if it cannot, that itself is a finding.
The judge that cannot trade
Python is the other half of the framework, and it is defined by what it is forbidden to do. Python never contains a strategy. There is no Python port of a probe “to iterate faster”, no twin implementation to cross-check the C# one, no pandas version of the exit logic. This is a rule, not a preference, and it is enforced by the simple expedient that Python is never given a primitive to express an entry, an exit, or a size: it has prices and a trade list, and nothing in it can turn the one into the other.
What Python does instead is judge. It consumes the trade list exported by the platform, reconstructs profit and loss from raw prices multiplied by contract point value, computes the metrics I actually care about, and runs the statistical machinery: labelling, cross-validation, random benchmarks, Monte Carlo, the multiple-testing corrections. It also holds the hypothesis register. Everything that decides whether a result is believable happens here; nothing that generates a result does.
The temptation to break this rule is constant and always sounds reasonable. The platform is slow; a vectorized reimplementation would run the whole universe in seconds; surely a quick check does no harm. But two implementations drift, and the drift is never announced. It shows up as a signal that is “roughly the same” in both environments, with the Python version quietly evaluated on the bar of the signal instead of the bar after, or filling at a level the platform could never have reached. Every conclusion drawn from the twin then applies to a strategy that does not exist. Worse, the twin is where the sequential optimization of the “Illusion” posts happens, because it is fast enough to make trying things frictionless. Keeping Python incapable of expressing a strategy removes the frictionless path. It is slower. That is the point.
The contract between the two: the canonical trade list
If the two environments do not share code, they must share a data contract, and that contract is the trade list. The platform exports it; Python reads it; neither side is allowed to know anything else about the other.
The format is deliberately plain. One row per event. Entries and exits are separate rows, not a single “trade” row with both ends, because a row per event represents partial exits, scale-ins and the odd platform artefact without a special case. Timestamps are to the second, even on daily bars, because the moment at which the platform believes a fill occurred is exactly what a validation layer needs and exactly what a coarser timestamp destroys. Alongside the list, Python is given raw prices and contract specifications and nothing else. The platform’s own equity curve is not used. Python builds its own from prices and point values, and if the two disagree, the disagreement is the first thing investigated, not the last.
This is what turns a backtest into an auditable object: one artefact crosses the boundary, and every number downstream is a function of it plus public data.
The parity test and the two semantics I had to discover
A contract is only worth something if both parties honour it, so the trade list is checked. The parity test takes the platform’s exported list and asks Python to reproduce it: given the same prices and the same barrier geometry, do the labels and the exits Python reconstructs land where the platform put them. The match is very high. It was not, at first.
Two execution semantics of the platform had to be discovered and matched explicitly, and neither was documented in a place I would have thought to look. The first is that barriers and fills are evaluated from the bar after the fill, not the bar of the fill. An order that fills on a given bar cannot be stopped out on that same bar; the stop becomes live one bar later. The naive reconstruction — check the barriers on every bar including the entry bar — disagrees with the platform on precisely the trades where the entry bar was wide, which are disproportionately the trades that matter. The second is that a gap through a stop or a target fills at the open, not at the level. If the market opens beyond the stop, the platform fills you where the market is, not where you wished it were. A reconstruction that fills at the level is systematically kinder on losses and systematically stingier on gains, and in a momentum system, where gaps in the direction of the trend are common, the bias is not symmetric and does not wash out.
Neither of these is exotic. Both are exactly the sort of detail a validation layer assumes away without noticing, and that is what makes them dangerous. A Python twin would have embedded the naive semantics and validated a strategy that trades on a fill the platform never gives. Having no twin, the only way to make the numbers agree was to find out what the platform actually does and encode it in the judge. The parity test is how the judge learns the rules of the game it is judging, and it is rerun whenever either side changes.
What this buys
The architecture buys one thing: countability. Probes, exits, features and sizing rules are named, immutable objects. A strategy is a composition of names. A result is a trade list produced by that composition and judged by a layer that could not have produced it. Every path by which a hypothesis can touch data runs through a primitive that exists by name and a trade list that exists as a file. Nothing is tried in a scratch notebook and forgotten.
That is the precondition for the next article, which is about the register itself: what counts as a trial, what does not, how a hypothesis is admitted before it is allowed near the data, and how a descriptive batch across the whole universe is counted as one trial rather than twenty-five. The architecture makes trials countable. The register does the counting.
quanthedgeai — the research arm of algosworksai. Systematic research on mid-frequency instruments.
Get the monthly Market Regime Note
Regimes, volatility and correlations across major futures markets — with the code behind the charts. Free.
Subscribe →