Strategy envelope
A strategy envelope is the unit of sharing for a lemon strategy: a small,
versioned JSON document that wraps a strategy together with everything needed to
re-run it — a name, the strategy itself (as lemon source text or a lowered
spec tree), the engine config, and the universe window. It is pure data:
an envelope never contains market data, only series names and a universe,
so it is safe to store, publish, and re-run in another browser.
Format
{ "format": 1, // envelope version (this build: 1) "name": "Cheap quality rotation", "description": "…", // optional "author": "…", // optional
// Exactly ONE of the following two: "source": "rank(pe) < 20", // lemon text, or "spec": { "op": "…", … }, // a lowered Expr tree
"config": { "fee_ratio": 0.001, "delist_after": 10 }, // optional engine knobs "universe": { // names + window only, never data "from": 20180101, "to": 20251231, "symbols_hint": "sp500", // a named universe the runner resolves "symbols": ["AAPL", "MSFT"] // …or an explicit list }, "engine_version": "yuzu-core 0.x" // optional reproducibility pin}format— must equal the envelope version this build understands (currently1). Bumped only on a breaking change to the envelope shape, never for a new engine op.sourcexorspec— provide exactly one.sourceis human-writable lemon;specis the loweredExprtree (schema/lemon-spec.schema.json). Both lower to the same tree the engine evaluates.config— the engineBacktestConfigknobs (fees, slippage, delist, benchmark, bootstrap, …). Interpreted by the engine/runner; opaque to the language layer.universe— the date window and the symbol universe (a hint like"sp500"the runner resolves, or an explicitsymbolslist). No prices or fundamentals — the runner supplies data locally (e.g. a BYO-key fetch, seefmp-data-source.md).
The machine-readable schema is
schema/strategy-envelope.schema.json.
Validation
lemon check strategy.json # one or more files (or stdin)lemon check reports actionable errors and exits non-zero on any problem, so a
registry or the web app can reject malformed submissions before storing them. It
checks, in order:
- the document is valid JSON with no unknown envelope keys;
formatmatches this build;nameis non-empty;- exactly one of
spec/sourceis present; sourceparses as lemon orspecdeserializes into a validExpr(unknown ops, missing required fields, and wrong shapes are all caught);config, if present, is an object.
The same check is available as a library entry point,
lemon::envelope::check(doc) -> Result<Checked, Vec<String>>, returning the
resolved Expr tree on success — and in the browser via the lemon-wasm
check_envelope(doc) export, which returns
{"ok":true,"name","spec"} or {"ok":false,"errors":[…]} (never throws), so a
web app / registry can validate submissions client-side.
Reproducibility
An envelope reproduces bit-for-bit only when three things match:
- the same engine version — pin it in
engine_version; op semantics are fixed by golden tests, but a version bump can add or change behavior; - the same data panels — same series, same symbols, same date window, same adjustment. The envelope names the universe; it does not carry the data, so two runners must load the same panels to agree (this is the crux of the “run it yourself” model — see the data-licensing note in issue #30);
- the same
config— fees, slippage, delist rules, benchmark, and bootstrap seed all affect the equity curve.
The strategy tree itself round-trips losslessly: source → Expr (via lemon check / lemon::parse) is deterministic, and a spec envelope stores the tree
directly.