Skip to main content
A StrategySpec is a declarative, JSON-serializable description of a trading strategy. Agents author specs; the Ithaca backtest engine executes them. No agent-authored code is ever run — the spec is matched against a frozen, closed schema and dispatched to trusted numpy kernels.

Design principles

The schema is closed: additionalProperties: false on every object. Unknown fields are rejected at validation time, not silently dropped. This prevents agents from smuggling arbitrary keys into the engine.
  • Declarative, not imperative. The agent describes what to trade, not how to compute it.
  • Closed schema. Every field is enumerated. No extension points, no freeform blobs.
  • Server-stamped identity. id, version, and tenant_id are assigned by the server. If an agent supplies them, they are ignored.
  • Deterministic. A seed field makes every backtest reproducible to the bit.

Top-level fields

FieldTypeRequiredDescription
spec_versionstringyesAlways "1.0". Bumped only on breaking schema changes.
namestringyesHuman-readable strategy name.
universeobjectyesTradable universe definition.
strategyobjectyesSignal logic selector + parameters.
constructionobjectyesPortfolio construction scheme.
riskobjectyesRisk guardrails.
executionobjectyesExecution assumptions.
resourcesobjectyesCompute tier reservation.
seedintegernoRNG seed. Defaults to 42.

Server-stamped fields (ignored if supplied)

These fields are never accepted from the agent. The server assigns them on persist:
FieldSourceNotes
idserver (UUID)Ignored if present in the request body.
versionserver (monotonic int)Increments per tenant + name.
tenant_idserver (from auth)Derived from the caller’s principal.
If an agent sends "id": "..." or "tenant_id": "...", the server silently discards them. This prevents an agent from impersonating another tenant or overwriting an existing strategy.

universe

FieldTypeRequiredDescription
symbolsstring[]yesTicker list, e.g. ["NVDA","AMD","INTC"].
sourcestringyesPrice panel source. Currently "polygon".

strategy

FieldTypeRequiredDescription
idenumyesOne of momentum, ma_cross, mean_reversion, rank_returns.
paramsobjectyesStrategy-specific parameters (see below).

Strategy id reference

Rolling-window momentum. params: lookback (days, int), skip (days, int, default 0).
Moving-average crossover. params: fast (days), slow (days).
Z-score reversion to rolling mean. params: lookback (days), entry_z (float), exit_z (float).
Cross-sectional return ranking. params: lookback (days), top_n (int).

construction

FieldTypeRequiredDescription
schemeenumyesequal_weight, rank_weight, or vol_weight.
long_onlybooleanyesIf true, weights are clipped at 0 (no shorts).
grossfloatyesTarget gross exposure, e.g. 1.0 for fully invested.

risk

FieldTypeRequiredDescription
max_grossfloatyesMaximum gross exposure.
max_netfloatyesMaximum net exposure.
max_name_pctfloatyesMaximum per-name weight (e.g. 0.10 = 10%).

execution

FieldTypeRequiredDescription
cost_bpsfloatyesRound-trip cost in basis points.
algoenumyesFill model. Currently "twap".
rebalanceenumyesRebalance frequency: daily, weekly, or monthly.

resources

FieldTypeRequiredDescription
tierenumyesCompute tier: "dev" or "prod".

Validation flow

1

Parse

The request body is parsed as JSON. Malformed JSON is rejected with 400.
2

Schema validate

The JSON is validated against the frozen StrategySpec schema. Unknown fields, type mismatches, and missing required fields return 422 with a field-level error list.
3

Strip server fields

id, version, and tenant_id are removed from the payload if present.
4

Semantic validate

Cross-field rules are checked (e.g. fast < slow for ma_cross, max_name_pct <= max_gross).
5

Stamp + persist

The server assigns id, version, tenant_id and writes the immutable spec row.

Complete example

{
  "spec_version": "1.0",
  "name": "Semis momentum 60/20",
  "universe": {
    "symbols": ["NVDA", "AMD", "INTC", "TSM", "QCOM"],
    "source": "polygon"
  },
  "strategy": {
    "id": "momentum",
    "params": { "lookback": 60, "skip": 5 }
  },
  "construction": {
    "scheme": "vol_weight",
    "long_only": true,
    "gross": 1.0
  },
  "risk": {
    "max_gross": 1.05,
    "max_net": 1.0,
    "max_name_pct": 0.25
  },
  "execution": {
    "cost_bps": 5.0,
    "algo": "twap",
    "rebalance": "weekly"
  },
  "resources": { "tier": "prod" },
  "seed": 42
}
The seed field is what makes a backtest reproducible. The same spec + the same price panel + the same seed always produces the same result hash. Change the seed and you get a different Monte Carlo path (where applicable).