Skip to main content
When an agent calls backtest_run (or the REST API receives POST /api/runs with kind: "backtest"), Ithaca loads the referenced StrategySpec, fetches an aligned price panel, and runs a multi-asset accounting kernel. The result is a deterministic equity curve, a stats block, and a risk attestation — all reproducible to the bit.

Pipeline

1

Load spec

The engine loads the immutable StrategySpec by id + version. The spec is never mutated.
2

Fetch price panel

An aligned multi-asset price panel is fetched from the configured data source (e.g. Polygon). The panel is a single numpy array of shape (T, N, K) — time × names × fields (open, high, low, close, volume). All names are aligned to the same trading calendar; missing bars are forward-filled then flagged.
3

Run accounting kernel

The kernel computes signal → weights → returns with no lookahead: weights computed at time t-1 are applied to returns at time t. This is the single most important invariant in the engine.
4

Compute stats

Performance and risk statistics are computed from the equity curve.
5

Emit artifacts

The 8-file artifact bundle is written and content-hashed. See Artifacts.

No-lookback invariant

weights[t-1] are applied to return[t]. The kernel never reads a bar at time t to decide the weight at time t. Violating this would be a critical bug and is covered by the test suite.
This means a strategy can only act on information that was available at the decision time. Earnings announced after the close on day t cannot influence the weight applied to day t’s return.

Return values

A successful backtest_run returns:
FieldTypeDescription
equityCurvenumber[]Daily equity values, starting at 1.0.
statsobjectPerformance + risk statistics (below).
finalWeightsobjectLast-bar per-name weights, keyed by symbol.
riskAttestationobjectConfirmation that risk guardrails held.

stats block

FieldTypeDescription
sharpefloatAnnualized Sharpe ratio (rf = 0).
sortinofloatAnnualized Sortino ratio.
cagrfloatCompound annual growth rate.
totalReturnfloatTotal return over the window.
volfloatAnnualized volatility.
maxDDfloatMaximum drawdown (positive number).
winRatefloatFraction of days with positive return.
turnoverfloatAverage daily turnover.
var95float95% Value-at-Risk (daily).
cvar95float95% Conditional VaR (daily).

riskAttestation

{
  "max_gross_held": true,
  "max_net_held": true,
  "max_name_pct_held": true,
  "violations": []
}
If any guardrail was breached during the run, violations lists the bars and the breach. A non-empty violations array does not fail the run, but it is surfaced to the human reviewer.

Job lifecycle

A backtest run moves through a state machine:
draft → queued → running → succeeded
                         → failed
                         → timeout
                         → cancelled
| State | Meaning | |---|---|---| | draft | Spec is attached; run not yet enqueued. | | queued | Run is in the work queue waiting for a worker lease. | | running | A worker has leased the run and is executing the kernel. | | succeeded | Kernel completed; artifacts written. | | failed | Kernel raised an error. Error captured in the trace. | | timeout | Run exceeded its tier’s wall-clock budget. | | cancelled | A human or the system cancelled the run. | Transitions are emitted as run_transition trace events and streamed over SSE. See Run endpoints for the REST API and Trace events for the event contract.

Determinism

A backtest is reproducible if and only if these three things are held constant:
  1. seed — the RNG seed from the spec (default 42).
  2. Pure numpy — the kernel uses only deterministic numpy operations. No random.random(), no GPU non-determinism, no floating-point reordering across runs.
  3. Fixed window — the same price panel (same symbols, same calendar, same bar range).
When all three hold, two runs produce identical result hashes. The artifact bundle includes an environment_lock.json that pins the numpy version and panel hash so a future re-run can be verified.
Want to verify a run? Re-run the same spec and compare the summary.json result hash. If they match, the run is bit-identical.