One Brain, Three Bodies
Every strategy implements one interface, and the identical compiled code runs in the backtester, the paper daemon and the live daemon. A strategy cannot tell which host it is in, by construction.
The contract
The whole architecture reduces to one function: decide(MarketState) → TargetWeights. The backtester feeds it history; the paper daemon feeds it today with simulated fills; the live daemon feeds it today with IBKR fills. There is no separate backtest mode inside a strategy. The classic backtest-versus-live divergence bug is designed out rather than tested for.
| Host | MarketState source | Fills |
|---|---|---|
| Backtest | Historical snapshots, replayed | Simulated |
| Paper | Live snapshot | Simulated, the same fill model |
| Live | Live snapshot | IBKR |
This is the interface as it exists in Plutus.ClassLibrary today, comments trimmed.
public interface IStrategy { // The module code — the StrategyModules row this class is registered as string Id { get; } // SHA-256 of the canonical config document. A backtest manifest names exactly this. string ConfigHash { get; } // The instruments this strategy can hold. The assembler loads bars for exactly these. IReadOnlyCollection<string> Universe { get; } // Macro series this strategy reads, so the assembler can supply them (or record their absence). IReadOnlyCollection<string> MacroSeries { get; } TargetWeights Decide(MarketState state); }
Notice what is missing. No Task, no CancellationToken, no service provider, no clock, no I/O. A strategy that cannot fetch anything cannot look ahead, cannot be slow, and cannot be nondeterministic. The implementation plan calls this the single most load-bearing decision in the document, and the contract test enforces it: same MarketState in, same TargetWeights out, every time.
Why synchronous and pure
The engine runs the decide phase as Parallel.ForEachAsync over accounts. A strategy that can await something can observe nondeterministic ordering, and the bit-identical reproduction contract dies with it. So all I/O lives in the host, before and after. The concrete trap the spec names: coin-flip's randomness must be a pure function of seed, period and symbol, never drawn from a shared generator, or parallel execution silently changes results between runs. The shipped class hashes exactly those three values.
Purity is also what makes the frozen-baseline gate in CI mean anything. A code change must reproduce the baseline backtest before it deploys, and that is only a meaningful check if nothing in the decision path can vary on its own.
Four properties the contracts encode, stated so they can be objected to: Decide is synchronous and pure; absence is explicit and carries a reason; provenance is part of the state, not logging; and strategies return weights, not orders.
The world as one record
MarketState is the world as one cycle saw it: immutable, every component as-of stamped. The structural no-lookahead guarantee lives in InstrumentState, whose history ends at its latest bar because the assembler never hands over a bar after it. One of the twelve contract tests probes the assembler out of order to try to make it do exactly that.
public sealed record MarketState( DateTimeOffset AsOf, DateOnly TradingDay, // the day whose open the decision will trade at CycleWindow Window, // Morning | Hourly MarketSession Session, // Open | Closed | EarlyClose, from the calendar IReadOnlyDictionary<string, InstrumentState> Instruments, PortfolioState Portfolio, Maybe<MacroState> Macro, Maybe<RegimeState> Regime, Maybe<ProbabilityVector> Probability, Maybe<NewsState> News, StateProvenance Provenance); // fresh, carried forward, or absent — and why
Four of those components are wrapped in Maybe<T>, which the source calls the single most important small type in the system. A null cannot carry the difference between "the dollar index was unavailable" and "the dollar index was flat", and that difference is the difference between a held position and a lost one.
public readonly record struct Maybe<T> { public bool HasValue { get; } public T? Value { get; } public string? AbsenceReason { get; } public static Maybe<T> Present(T value) => ... public static Maybe<T> Absent(string reason) => new(false, default, string.IsNullOrWhiteSpace(reason) ? throw new ArgumentException("Absence needs a reason.") : reason); }
A strategy that throws on an absent component is wrong; a strategy that treats absent as zero is worse. StateProvenance carries the same reason on the state itself, so a replay can prove what the daemon knew and the tear sheet can stamp what a strategy never saw. The RegimeState record makes the point-in-time rule explicit: EffectiveDate is when the regime became true, DetectedUtc is when anyone could have known, and a backtest acts on the second, never the first.
Weights, not orders
A strategy never sees a broker, a dollar balance or a fill. It says what it wants to hold as fractions of equity; sizing and order generation live downstream, which is what lets the same code run in three hosts.
public sealed record TargetWeights( IReadOnlyDictionary<string, decimal> ByInstrument, decimal CashWeight, string RationaleSummary, // one sentence, journaled IReadOnlyList<string> ReasonChainRefs) { // Rounding slack, not a policy: seven weights at six places can overshoot 1 by a few millionths. private const decimal Epsilon = 0.0001m; // v1 is long-only: every weight in [0, 1], and they sum to at most 1; cash is the remainder. }
The factory throws on a weight outside [0, 1] or a sum past 1. dollar-mag7 renormalizes its basket by rounding down at six places, so a basket with a missing instrument can never sum past one and the dust becomes cash.
Why strategies are a separate assembly
Plutus.Strategies has exactly one project reference: Plutus.ClassLibrary. It cannot see Plutus.DataAccess, Plutus.RestClient, the blob store or the backtester. That is not tidiness; it is the purity rule enforced by the compiler. A strategy that wanted to query a table or call FMP would first have to add a reference, and the reference would be visible in review.
Inside the assembly, StrategyBase<TConfig> implements the interface once: it takes an id and a config record, computes the config hash, and leaves Universe and Decide abstract. The three shipped strategies, spy-hold, coin-flip and dollar-mag7, are each a few dozen lines on top of it. StrategyRegistry maps module codes to factories and refuses unknown codes loudly; the StrategyModules table says what is allowed to run, the registry says what the code can build, and a code in one but not the other is the lifecycle ladder's problem.
The design's larger idea is "config, not code". A small set of engine types executes an unlimited set of strategy rows: rotation for graded-universe weight allocation, threshold-rules for config predicates over named signals, overlay for scaling a host account's gross exposure, event-tactical for capped event-driven entries with mandatory exits, research-thesis for thesis-driven entries with corroboration gates, and control for the baselines. Each of those becomes a base class in this assembly, beside StrategyBase, and never a runtime plugin. A row in EngineTypes cannot add behavior; the daemon asserts code-to-table agreement at startup and fails loudly on drift. New strategies are authored by writing a config document against an engine that already exists, and code-level modules remain the escape hatch for logic config cannot express.
Today only control and one rotation-family strategy have code behind them. The other engine types are named in the vocabulary and specified in the catalog, and they get a base class when the first strategy that needs one is written.
What it looked like when it ran
Gate 2 was spy-hold over 1993 to today matching SPY total return, with a rerun bit-identical. The first attempt failed: the benchmark series was truncated at 5,000 rows and the trial number leaked into the tear sheet. Both were fixed; neither tolerance moved. The second attempt passed at a CAGR difference of −0.02 percentage points.
The more telling run was dollar-mag7 with the dollar series absent, as it is until the GMI ingest lands. The strategy did not throw and did not guess. It held its long basket, wrote "dollar series absent" into its rationale, and the tear sheet carried the stamps SURVIVOR-UNIVERSE and ABSENT:macro beside a 30.95 percent CAGR. Confident garbage, and the record says so. Gate 2 in detail.
- Await anything
- Read a clock
- Touch a database or a service
- See a bar after the latest one
- Draw from a shared random generator
- Place an order
The strategy assembly knows nothing about where its inputs come from. The hosts know everything.