top of page

Autonomous Trading Platform

An end-to-end autonomous trading system: strategy research and validation, governance and risk controls, and live execution, engineered with the same operational discipline as production fintech infrastructure.

System Overview 

Four layers with soft boundaries.

The stated architecture is contracts (Pydantic) → storage (SQLAlchemy system-of-record plus versioned Parquet) → application → interfaces, with each layer only allowed to depend on the one before it. That rule bends in practice: across the application/services/ files from M through Z, 17 of 37 query ORM models directly via select()/session.query()/session.commit(), with no repository layer in between — the dominant pattern in that slice, not an exception to it. Three contracts files import from governance/ and storage/, and one storage model imports back from execution/. The layering still does most of the real work, but it isn't strict, and these are documented deviations rather than hidden ones."

One rule engine, not 17 ad-hoc checks. 

contracts/validators/core.py defines a small, typed framework. Rule[T] and ValidationContext are frozen dataclasses, Violation carries an ERROR/WARNING severity, and run_rules() applies a list of rules and collects every failure instead of stopping at the first. If a rule itself throws, the framework catches it and converts it into its own ERROR violation rather than crashing the whole validation pass, so one broken check just reports itself as failed instead of taking everything else down with it. Seventeen validator modules build on this single framework, one per contract type, so the rules for any given object are a countable list, not logic buried in scattered conditionals

​​​​​​​​

Contracts are typed for correctness, not blanket immutability.

contracts/common/types.py is a 26-line file that quietly eliminates two entire bug classes: UTCDateTime rejects any naive datetime at parse time and normalizes everything to UTC, and Money/Quantity are both Decimal, not float, so financial values can't silently lose precision. Immutability is applied selectively rather than everywhere: only one Pydantic model in the whole package (DividendEvent) is frozen, while a separate set of 63 internal dataclasses splits roughly evenly between frozen and mutable. This is a deliberate per-type choice, not an accidental one.

The frontend is a working app. Every one of its 6 mounted pages calls real backend endpoints (24 distinct routes) through TanStack Query and a real JWT-authenticated Axios client, and the mock data file is fully superseded — nothing in the codebase imports it anymore. A fully-built strategy comparison page even exists but isn't wired into any route yet. But the shell all of this runs on is a generic SaaS-style scaffold that predates the platform's current shape, which is why the next step is restructuring around governance, safety gates, and the research pipeline, not just wiring up more pages.

One core, several purpose-built adapters, not five copies of the trading logic. 

OrderExecutionService, OrderStateMachineService, and PortfolioConstructionService are the same instances whether an order originates from live trading, paper trading, a backtest, shadow mode, or a chaos-replay run. Live and paper both route through AlpacaBrokerClient, differentiated only by account configuration. Backtests substitute a SimulatedBrokerClient through a separate execution path built for bar-by-bar simulation. Shadow mode runs the full pipeline and intercepts only the final broker-submit call. Replay injects synthetic failures directly into system-of-record tables to test how the same core reacts. The environments aren't unified behind one formal interface yet, but the effect already holds: a passing test against one environment is real evidence about the other four, because none of them run a separate reimplementation of the trading logic itself.

system_overview_01_layering_import.png
system_overview_02_rule_engine.png

TODO NEED FRONTEND SCREENSHOTS

Safety

Four independent gates instead of one flag. 

LiveTradingGateService replaces a single "is live trading on" boolean with four sequential gates: environment/config, account allowlist, armed/disarmed runtime state, and kill switch. Each gate raises its own typed exception, so a blocked trade is diagnosable at a glance. A failure at any gate short-circuits the rest, so an unauthorized account never even reaches the kill-switch check. The same fail-closed pattern extends to the API layer, where a consistent RBAC and error-handling standard covers 14 route files. One known gap remains: `portfolio_routes.py` has no RBAC on 12 endpoints, already flagged.


 

Order lifecycle transitions live in one table, not scattered conditionals. 

OrderStateMachineService declares every legal transition as data in one table. Terminal states (filled, canceled, rejected, expired) have no outgoing transitions, so a filled order can never be resurrected; the table itself enforces it. Any transition not in the table raises an error immediately rather than being silently ignored, which matters because broker events can arrive late, out of order, or duplicated.


 

A kill switch that survives a restart. 

The kill switch needed to survive more than the current process: a restart, a deploy, a crash. KillSwitchService reads and writes a single authoritative row in Postgres rather than holding an in-memory flag, using one singleton ID so there's exactly one source of truth platform-wide. Every enable/disable call commits immediately, and every check reads straight from the database. `test_kill_switch_persistence.py` (14 tests) confirms this by constructing a fresh service instance and verifying it re-reads the persisted state instead of defaulting to off.

03_killswitch.png
safety_03_environment_policy.png

Two-layer enforcement against live trading. 

EnvironmentSafetyPolicy checks NO_LIVE_TRADING, then ENABLE_LIVE_TRADING, then whether live-trading modules were even compiled into the build: three independent ways to refuse live execution at the config/build layer. A second, runtime layer inside the replay/debug tooling refuses to run if the environment is LIVE at all, and separately re-checks the persisted settings snapshot. A stale in-memory value can't let a debug run silently execute against live state.

Shadow mode validates the real decision path. 

Shadow mode lets the platform compute and fully evaluate a trade decision, including every pre-trade risk check, without submitting it to the broker. The order still passes through the entire order-generation and risk-check pipeline; only the final broker-submission call is skipped. Shadow mode tests real conditions rather than a separate, simplified simulation that could drift from production behavior.

safety_04_shadow_mode.png
safety_05_pretrade_risk.png

Pre-trade risk checks project the resulting portfolio. 

PreTradeRiskService answers one question before any order reaches a broker: would the resulting portfolio still be within every risk limit? It projects gross exposure, per-symbol exposure, daily notional, and concentration caps, then raises a typed exception on the first breach. The exposure math correctly distinguishes a position-reducing sell from one that adds risk. For sector concentration, where metadata isn't always available, the service supports three configurable policies for unknown sectors: reject, warn and allow, or route into a limit-checked "unknown" bucket.

The database enforces its own promotion rules. 

Postgres CHECK constraints require at least 30 days tested and a nonzero trade count for any strategy transition into live status. Application code already enforces these thresholds. But if that logic has a bug, gets bypassed by a raw write, or a future migration reintroduces old behavior, the database itself refuses to store a row that violates the constraint.

safety_06_db_check_constraints.png
safety_07_sector_test.png

Safety tests separate the math from the enforcement. 

Safety tests follow a consistent layered pattern: test the underlying math in isolation, test enforcement separately, then test the audit and metric side effects, then add regression tests. Separating math from enforcement is deliberate: if a real order gets incorrectly blocked, you want to know immediately whether the exposure number was wrong or the policy threshold was wrong. `test_sector_concentration.py` (33 tests across 9 classes) is the clearest example of the full pattern in one file.

One shared function prevents double-counted fills. 

Brokers report cumulative filled quantity, not incremental fills, and the platform learns about fills through two channels, polling and streaming, that can both report the same state. One function shared by both paths computes the delta between current and previous filled quantity and only records a new fill when that delta is positive. A negative delta is treated as stale, logged as critical, and ignored rather than rewinding state.

safety_09_incremental_fill.png

Research / Strategy Generation / Experiment Systems

A strict survivor funnel: cheap, intermediate, walk-forward, Monte Carlo. 

Each stage only receives the prior stage's survivors. "Cheap" and "intermediate" aren't separate stage types. They're two configurations of the same simulation stage: one with a short window and loose thresholds, one with the full window and real thresholds. The ordering is deliberate, cheapest and most-discriminating test first, so a badly broken candidate dies early instead of consuming expensive compute it was never going to survive.

Four generators, one shared parameter registry.

Generating strategy candidates is parameter-space sampling over a typed registry, not machine learning(in current version). Grid search takes the full Cartesian product: complete but combinatorially explosive (10 params times 5 values equals 9.7M candidates), so large ranges get collapsed to a min/default/max set. Random sampling draws with replacement, seeded, default 50 samples. The "evolutionary" generator has no fitness function or selection; it mutates each member randomly each generation, exploring neighborhoods rather than climbing toward better regions. The composite path assembles strategy skeletons from a hand-curated table of 13 indicators, producing a documented pool of 854 skeletons checked against the live component registry before acceptance.

Walk-forward tests a fixed config across rolling time windows. 

A fixed, un-retrained strategy configuration has to keep clearing the bar as both the training window and the following unseen window roll forward through time. The committed config produces 9 folds. A fold passes only if the config clears filters on both windows, and test simulation is skipped entirely after a train failure. The 6-of-9 pass requirement is a deliberate tightening, not a framework default. Like the other stages, this one fails open: if every fold's train run returns nothing, the unfiltered survivor set passes through rather than being eliminated.

Config hashing dedupes within and across sessions.

Every accepted config gets a SHA-256 hash: a canonical, order-independent identity, not a security hash. It's used for in-session dedup and, separately, for cross-session dedup via a persistent cache that survives across processes and fails open (a broken cache file logs a warning and starts empty rather than crashing generation). One real gap: the persistent cache is a standalone facility the engine itself never calls; only the in-session set gets checked automatically.

research_02_generation_cache.png

Eight checks decide pass/fail; a separate score decides rank.

Thresholds are inclusive, and all checks are evaluated (not short-circuited), so a rejected candidate returns its full failure profile. The eight core checks cover Sharpe, drawdown, trade count, consistency, profit factor, win rate, return variance, and total return; only three are meaningfully active by default. Filtering and ranking are separate steps. Ranking only orders strategies that already passed every filter, so a high score never compensates for a failed gate.

research_04_scoring.png
research_06_monte_carlo.png

Monte Carlo varies the execution seed, not the time period.

It holds the strategy, data, and dates fixed and instead varies the random seed driving execution realism (rejection, partial fills, slippage), testing whether a strategy's edge depends on one favorable draw. Filtering happens per-run, not on aggregate means, because a mean Sharpe of 1.2 can hide 40% of runs going sharply negative. One confirmed gap: the pass-rate denominator counts only completed runs rather than the configured total, so missing simulations make survival artificially easier.
​​

A 9-candidate sweep can mean 630 simulations.

The committed sweep covers 9 configurations across all four stages and 5 symbols, small enough to inspect while still exercising the full pipeline. A full survivor can require up to 70 simulations (cheap plus intermediate plus up to 18 walk-forward runs plus 50 Monte Carlo trials). The larger default pools stay affordable only because the funnel guarantees most candidates never reach the expensive stages.

research_07_staged_yaml.png
research_08_deterministic_seed.png

A shared seed service keeps every strategy's randomness independent.

DeterministicSeedService hashes each unit's full logical identity (strategy, config, stage, fold, trial) into its seed, so two different strategies never reuse the same random stream. The key safety property is the identity-mismatch guard: if a checkpoint's ID matches but its full identity doesn't, the service raises an error instead of resuming with mixed lineage. Execution runs on a thread pool, with results reordered by a stable sort key so thread-completion order never affects reported results.

Research hands off to governance with no human gate, by design.

Only complete pipeline survivors get auto-inserted into governance as `approved_research`, which is safe because that state is non-executing and non-capital: eligible for review, not authorized to trade. Every later promotion still runs through governance's own machinery. The insertion is idempotent and never resets an existing row, so rerunning an experiment can't accidentally undo a prior demotion.

research_09_governance_08_autoinsert.png

Governance & Capital Allocation

A role-gated state machine, not a free-form status field. 

approved_research → approved_for_paper_trading → approved_for_live_trading, with legal backward moves to demote or retire; every transition is checked against an explicit table. Authority scales with consequence: research needs researcher, system-risk, or admin approval; live trading is admin-only. Before any promotion, the service evaluates every criterion individually and records each threshold, actual value, and pass/fail, so the decision is explainable after the fact.

Capital reallocation is priority-ordered, then proportional.

Disabled strategies get zero. Manually overridden strategies keep their fixed allocation and are never silently replaced. Everything else splits the remaining pool proportionally to a blended live/backtest quality score. Automatic changes are written as explicit override records, so an automated reallocation is attributable exactly like a manual one.

A five-rung ladder recovers one step at a time. 

NORMAL, WARNING, PROBATION, SUSPENDED, and BREACHED map to an allocation scalar of 1.00, 0.50, 0.25, 0.00, and 0.00. Escalation can jump immediately; recovery steps down one rung at a time, gated by a hysteresis band and a cooldown that lengthens with severity. Recovering from BREACHED specifically requires operator acknowledgment. Every other rung recovers automatically once conditions clear.

Missing rules and unpinned evidence both fail closed.

A missing promotion rule raises a configuration error instead of silently skipping the check. Separately, capital-bearing transitions require a pinned evidence run. The service will not fall back to "whatever the latest run happens to be," because a passing run at evaluation time could be superseded by a failing one before the promotion executes.

governance_04_advisory_lock.png

Two independent guards stop reallocation runs from racing.

A transaction-scoped Postgres advisory lock is the primary guard; a row-based "already running" check is the fallback for non-Postgres environments. A separate run-ID mechanism protects against the same run being processed twice on retry, and a hysteresis threshold suppresses writing a new override when a proposed change is too small to matter.

Corrections create a new event; they don't rewrite history. Every governance decision runs through one shared audit service recording the actor, previous and next state, the criteria evaluated, and rule versions. Correcting a mistaken entry creates a new amendment event and links the original to it, the only field ever mutated on the original row. Decision evidence stays immutable; only its supersession metadata changes.

governance_05_audit_supersede.png
governance_06_drawdown_scaling.png

Three separate drawdown defenses at three scopes. 

DrawdownScalingService operates per-strategy, scaling position size continuously. PortfolioDrawdownGovernanceService operates on total account equity, because a portfolio can be in real trouble even when every individual strategy looks fine. It escalates through four actions: warn, pause new trading, pause rebalancing, activate kill switch. A confirmed breach fails closed, but missing infrastructure context fails open deliberately, so a monitoring gap doesn't shut down the whole platform.

Admission into governance grants no capital.

Research survivors are auto-inserted as approved_research, the direct handoff point from the experiment stack. That state marks candidacy, not approval — it carries no capital and no execution authority. Every step toward paper or live trading still has to clear the full governance machinery: a legal transition, the correct role, a configured promotion rule, passing criteria, and a pinned evidence run.

Portfolio Intelligence

Section A: The Math Layer

Black-Litterman starts from equilibrium, not a guess. Given a covariance matrix, benchmark weights, risk aversion, and an operator's views on expected returns, the service answers a narrow question: what portfolio would a rational investor hold? Rather than asking a researcher to specify raw expected returns directly (historically the weakest input in mean-variance optimization), it starts from an implied equilibrium prior and lets the operator nudge it with views they actually have confidence in.

The research-only boundary is enforced in code, not just docs. Most safety controls in this platform draw their boundary through docstrings or downstream checks. This one is enforced directly in the code path: a call raises an error unless the caller's context is on an explicit allowlist and not simultaneously on a blocklist that includes live trading and production rebalancing. A caller can't invoke the service from a live context even by accident.

A hand-rolled solver, not a QP library. The optimizer solves the classic problem, maximizing return for a given risk tolerance under a full-investment constraint, with a from-scratch solver: projected gradient descent, projected back onto the feasible simplex via KKT bisection. Turnover is discouraged via an L1 penalty, and sector caps are enforced by scaling down any sector that exceeds its limit. A convex-backend flag can route the same problem through CVXPY instead — the hand-rolled solver is the default, not the only option.

dry_run defaults to True, and one constraint only works on the other path. A run is persisted for inspection but explicitly does not write live allocation changes, so no output can go live without an operator deliberately flipping that flag. The optimizer also supports factor-neutralization, constraining exposure to a risk factor toward zero, but the hand-rolled solver never enforces it. It's implemented only on the CVXPY path.

portfolio_intel_04_dry_run_default.png
portfolio_intel_03_factor_neutralization.png
portfolio_intel_06_overfitting.png
portfolio_intel_07_regime_fingerprint.png

Section B: Candidate Intelligence

Ranking prioritizes; it does not promote. 

CandidateRankingService converts research evidence into one ranked list via a weighted score across seven components. Alongside the score, it produces a deployability gate at a 0.55 threshold and a human-readable explanation. Its own docstring states the boundary plainly: "This service does NOT promote strategies or enable trading. It is a prioritisation tool for research review."

Overfitting and robustness are different questions, not the same one twice. 

OverfittingEstimationService looks backward at signs a strategy was shaped by its own training data. RobustnessPredictionService asks a forward-looking question instead, but layers a hard-floor fragility check on top: five catastrophic-signal floors that can override the weighted average entirely, so one genuine failure mode can't get averaged away by five reassuring numbers.

A cosine-similarity fingerprint flags hidden regime overlap. 

Each strategy gets a 20-element fingerprint across five regime dimensions: trend, volatility, liquidity, mean-reversion, risk. Comparing fingerprints by cosine similarity produces a portfolio-level diversification score. A low score flags strategies that tend to succeed and fail together across the same regimes, a risk a plain return-correlation check can miss entirely.

From-scratch clustering surfaces near-duplicate strategies. 

StrategyClusteringService groups similar strategies with hand-rolled single-linkage clustering; no external library import appears in the file. A ranked list can contain a dozen near-duplicate variants of the same idea, and clustering surfaces that redundancy before a researcher mistakes many similar strategies scoring well for many independent sources of edge.

This layer has no REST endpoint, by design. Unlike governance and safety, nothing in Portfolio Intelligence is exposed via API. It's driven entirely through CLI commands and scripts: an internal research toolkit a researcher runs deliberately, not something the platform calls automatically.

Data & Storage Layer

Immutable, versioned Parquet, never overwritten in place. 

Before writing, the system checks whether a version already exists and raises an error if so. Every downstream cached simulation and audit record points at a specific dataset version, so if that version could be silently rewritten, the platform's "same inputs, same result" guarantee collapses. Two independent SHA-256 checksums protect against different failure modes: one checks whether the data matches, the other whether the file was corrupted on disk. The roughly 33.6K committed Parquet files in the repo are the expected consequence; old versions are never deleted, only superseded.

The tradeable universe is versioned like market data. 

A universe that silently drifts makes every backtest against it systematically optimistic. The lifecycle moves through four states, candidate, proposed, active, retired, tested against a real database, not mocked. Before activation, a churn check catches an unusually large day-over-day membership change, which more often signals a broken data feed than a real market event. Rollback builds a brand-new proposed version rather than pointing the active flag backward, so every version, once created, means exactly one thing forever.

Two feature-pipeline guards ask two different questions. 

One asks a performance question: has this already been computed, so recomputation can be skipped. The other asks a correctness question: does an available dataset actually satisfy a strategy's required features, including price basis (raw versus adjusted, where using the wrong one silently corrupts every return calculation). A dataset can pass one check while failing the other, which is exactly why they stay separate.

Corporate actions, survivorship, and ticker lifecycle solve related but separate problems. 

Corporate-action adjustment happens at processing time, not ingestion; raw bars are never mutated. Of eight modeled action types, only splits and cash dividends currently have implemented logic(more action types planned in further updates). Survivorship runs in two layers: a hard pre-flight gate, and a softer validation service for fold-level walk-forward checks. Ticker lifecycle is a flat event log of renames, delistings, and mergers, walked as-of a given date with explicit cycle protection.

Atomic transactions for state changes; independent sessions for audit trails.

One shared database session gives multi-table operations, like a rebalance touching positions, cash, and allocations, atomic commit-or-rollback semantics. Governance, audit, and diagnostic tables deliberately sit outside that boundary and manage their own session scope. That separation is deliberate: an audit record should commit on its own terms, independent of whatever business transaction triggered it.

data_storage_02_unit_of_work.png
data_storage_04_airflow_dag.png

Four ingestion schedules, each matched to its data's cadence.

A daily backfill catches historical gaps, a 5-minute DAG keeps market data fresh during trading hours, a daily corporate-actions DAG reflects that splits and dividends don't happen intraday, and a weekday-only trading DAG encodes the market calendar directly in its schedule.

The golden-path orchestrator is a manual validation tool, not a fifth schedule.

It chains backfill, corporate actions, features, and experiments as one durably tracked run. Its only caller anywhere in the codebase is a manually-invoked soak-testing tool; the real DAGs run independently on their own schedules.

data_storage_06_golden_path.png

Testing Suite

A schema-drift tripwire enforces a rule the docs alone can't. 

A written rule isn't enforced by anything on its own. One test diffs 28 contract/ORM pairs field-by-field on every run; a second, integration-tier test runs a real migration against live Postgres and diffs the model against what the database actually has. Together they catch both contract-versus-code disagreement and code-versus-database disagreement, including the kind of failure that would surface a hand-edited or silently failed migration.


 

Near-zero skip and xfail debt across 4,080 tests. 

Near-zero skip and xfail debt across 4,080 tests. A direct grep turns up exactly one xfail marker and no unconditional skips — passing tests are passing because the code works, not because failures were quietly waived.

A real Postgres tier in CI, a fast SQLite tier locally. 

CI boots a real Postgres container for integration-marked tests, including the schema-drift suite's database half, so that check actually executes rather than being skipped. Locally, the suite defaults to in-memory SQLite for speed. The schema-drift suite is what makes that split safe: without it, a green SQLite run wouldn't prove the schema was correct against the database it runs on in production.

testing_02_safety_test_pattern.png

Ten dedicated safety test files, not one catch-all.

That granularity reflects the code itself: this is the platform's highest-consequence path, so a failing test name pointing at exactly one gate matters more here than elsewhere. The repeated pattern across files: verify the math, verify the gate enforces it, verify enforcement is logged, then add a dedicated regression test for the specific bug the gate was built to prevent.

Fixtures write through real code, not mocks.

Eight fixture modules layer from single-stage state up through full multi-cycle chains. The trading-cycle fixture writes a real dataset and seeds real database rows, patching only the one genuinely unpredictable seam — what the strategy itself decides to trade — before handing control to the actual production code. A repository bug would surface here, since the fixture writes through the real repository rather than asserting against a mock.

testing_03_fixture_seed.png
testing_06_risk_parameter_wiring.png

A/B replays prove settings actually change behavior.

One test file proves risk settings change real trading-cycle output, not just that they get saved to the database, by running the same deterministic replay twice and changing one setting between runs. The most pointed case sets the portfolio-level drawdown limit so loose it can never fire and the strategy-level limit so tight it fires immediately, then confirms orders actually get blocked, proving the two gates are independent.

Observability 

261 instruments across a 1,932-line metrics module. 

A platform trading autonomously has no operator watching every tick, so the only way to catch a problem early is if the code emits numbers at every meaningful state transition. Histograms slightly outnumber counters. A codebase leaning this heavily on histograms is built to answer "what does this normally look like," the harder, more useful question when the failure mode is gradual drift rather than a clean outage.

TODO ADD GRAFANA SCREENSHOT HERE 

One shared function stamps every trace automatically. 

In a system running concurrent cycles, a raw trace is close to useless unless it can be filtered to exactly one run, and that can't depend on every call site remembering to attach the right IDs by hand. A shared runtime context lives in a context variable and merges onto whatever's already bound, so a nested call inherits its caller's identity automatically. Correctness of one seam guarantees consistency everywhere.

Allocation config is snapshotted once so a live edit can't leak into a backtest. 

A multi-year backtest re-reading policy from the database on every bar has no way of knowing if an operator edited that policy mid-run. The fix snapshots the config once before the loop starts. The second database read simply doesn't exist, so the guarantee is structural, not incidental.

A chaos-testing matrix injects known-bad conditions on purpose. 

Verifying the platform handles a broker mismatch or a governance trigger correctly is hard to test safely against real infrastructure. `platform_replay` covers nine failure kinds across six injection targets, with every incident written into real database tables rather than mocked.
 

observability_03_correlation_links.png

Clickable trace links are wired into the real API, not sitting unused.

A small utility builds ready-to-click Grafana links from a correlation ID. It's called per row by the operations service, and the resulting links are serialized out through the real API. A job-run response contains a clickable link straight into the trace and logs for that run.
 

The replay tool calls the real production functions, unmodified.

Backtesting infrastructure quietly stops meaning anything when the test path and production path drift apart. This orchestrator imports and calls the real ingestion, feature, and trading-cycle functions straight from their production modules, so there's no parallel code path to drift.
 

observability_05_replay_orchestrator.png

Backtesting & Simulation Engine

Three cash buckets, because settled and unsettled aren't the same money. 

A backtest that treats cash as one number will happily let a strategy spend proceeds from a sale it hasn't actually received yet. The engine tracks settled, unsettled, and reserved cash separately, with buying power as a hard gate: an order exceeding it is rejected and logged, not silently under-filled. Settlement is bar-indexed rather than wall-clock, so runs at any bar cadence settle after the same number of bars.

Latency is modeled by deferring execution, not adjusting price. 

Dividends are applied the moment the ex-date bar is reached, credited directly into settled cash. Order latency pushes execution to a future bar index rather than approximating a delay in the fill price. With nonzero latency, an order fills at the execution bar's open, not the signal bar's close, because it genuinely didn't exist yet when the signal fired.

Sim and live share the slicing math but not the routing. 

The simulated and live execution engines both construct instances of the identical TWAP/VWAP slicer classes. The honest caveat: on the live path, the slice schedule gets folded into a single order's metadata rather than routed as real child orders. The simulator genuinely schedules and fills each slice independently. The math is shared; the routing isn't.

Slippage assumptions are calibrated from real paper-trading fills. 

The calibration service pulls realized fill-quality metrics from paper trading and derives an impact coefficient as an explicit, reproducible formula. A minimum of 30 fill samples is required before a calibration counts as reliable; below that, it falls back to safe defaults rather than letting a handful of fills swing the model.

Byte-identical replay is a real CI gate, not a loose check.

Every probabilistic decision, including rejection, partial fills, and fill probability, runs through a single seeded RNG reset at the start of every run. The deterministic-replay suite runs the same seeded simulation twice and asserts exact equality across equity, cash, positions, and fill counts, paired with a companion test proving different seeds do eventually diverge.

backtesting_03_deterministic_replay_test.png

Two Year Backtest Results Breakdown & Visualization

Results and Breakdown

A 522-trading-day replay (Jan 2023 – Dec 2024) across a 100-symbol universe, starting from $1,000,000, run tick-by-tick through the platform's real production trading-cycle function. Every one of the 522 ticks succeeded, producing 29,654 orders with matching fills, not a simulated approximation. Rebalancing runs weekly, with a 12% portfolio drawdown governance limit.

This is a backtest/replay demonstration, not live client performance. No live capital was ever at risk. Figures are gross and pre-tax; taxes and fees aren't modeled.

These charts use three different comparison lines, and they shouldn't be conflated:

  • The SPY line on the equity curve is a synthetic proxy (a correlated random-walk model, not real SPY prices), used because no external price feed is loaded into this artifact.

  • The 15/20/25% lines are internal business targets, not financial or regulatory benchmarks.

  • Real external references (actual SPY/QQQ/VTI) and same-universe baselines (equal-weight buy-and-hold on the same symbols) aren't available in this artifact.

Validation here is per-strategy, not per-run. Individual strategies discovered by the research pipeline did pass through walk-forward and Monte Carlo validation before promotion (see the Research Funnel chart), but that's a per-strategy filter, not a property of the 2-year run as a whole. The full backtest itself hasn't yet been re-run across different market regimes, cost assumptions, or as an out-of-sample holdout. That's the next phase.

Reading the headline number
+261.7% is a real, non-simulated result, but it's a single historical window, not a forecast. 2023–2024 was an unusually strong period for the mega-cap technology names in this universe, and a momentum-weighted strategy trading that basket during this stretch benefited from tailwinds that won't always be there. This run shows the pipeline (research, validation, governance, execution) works end-to-end and finds real edge. It isn't yet evidence of a sustainable, repeatable annual return; proving that requires testing across different market regimes, which hasn't been done yet.


The -61.5% max drawdown is a genuine characteristic of this run, not a rendering artifact, and it's tied to specific, adjustable choices rather than being an inherent property of the strategy. The backtest used a warn_only drawdown governance mode: breaches are logged but never pause trading, since a historical replay has no operator present to acknowledge and resume it. In a paper or live deployment, the platform's actual enforcement mode (pause on breach, operator sign-off to resume) would engage well before a drawdown reached this depth. Position-level volatility scaling, also available in the platform, is a direct lever for reducing this kind of swing going forward.
What's next


The recommended validation sequence, in priority order: a 2022 bear-market replay (the most adversarial test for a momentum-led strategy), a formal walk-forward test across rolling in-sample/out-of-sample windows, a higher-slippage stress test on execution assumptions, and a Monte Carlo bootstrap to quantify a confidence interval around the reported Sharpe ratio rather than a single point estimate.

Reference files attached detailing visuals and next steps backtesting roadmap:

- methodology assumptions 

​- chart explanations

​- robustness next steps

What's Next 

This backtest validates the platform end-to-end, but a full backtest run is still a controlled environment. The next phase is closing the gap toward real deployment, step by step:

 

Performance optimization.

This 2-year backtest took multiple days of wall-clock time to run(5 days straight even with multi-threaded simulation), with the monthly research pipeline (candidate generation, walk-forward, Monte Carlo validation) accounting for a large share of that cost. Profiling and speeding up that pipeline is the first priority: faster iteration means faster validation cycles for everything that follows.

 

Cloud deployment.

Move the platform off a local machine and onto cloud infrastructure, using the containerized services (Postgres, Airflow(partially integrated and planned expansion as well), observability stack) already in place, enabling continuous, scheduled operation instead of manually-triggered local runs.

 

Frontend restructuring.

A dashboard already exists and is wired in, but it was built around a SaaS-style app structure, the kind of generic, multi-tenant shell that doesn't map cleanly onto what the platform actually does today. This isn't a matter of connecting it; it's a matter of rebuilding it around the platform's real domain model (governance, safety gates, the research pipeline) instead of the generic scaffold it started from.

 

Paper trading.

Run the platform live against real-time market data in paper mode, validating that real-world timing, data latency, and execution assumptions hold up the way the backtest assumed they would. This is the first time the system runs against the present instead of history.

Live trading.

After a sustained, monitored paper-trading track record, move to live capital under the platform's existing safety architecture: the four-gate trading check, the persistent kill switch, and drawdown governance running in full enforcement mode rather than the backtest's warn_only setting.Alongside that core path: continuing the regime and robustness validation already scoped for the backtest results (bear-market replay, walk-forward, cost stress-testing).

bottom of page