The strategy spec

Everything the chat builds and the engine runs is one JSON object: the spec. The chat edits it, validation guards it, the ledger freezes it, and exports carry it. Once you can read a spec, nothing in this product is a black box.

The shape of a spec

{
  "name": "Golden cross with RSI filter",
  "symbols": ["AAPL", "MSFT", "SPY"],
  "indicators": [
    { "name": "sma_50",  "type": "sma", "source": "close", "length": 50 },
    { "name": "sma_200", "type": "sma", "source": "close", "length": 200 },
    { "name": "rsi_14",  "type": "rsi", "source": "close", "length": 14 }
  ],
  "entry_rule_long": "crosses_above(sma_50, sma_200) & rsi_14 > 50",
  "exit_rule": "close < sma_50",
  "backtest_timeframe": "1d",
  "position_size_mode": "notional_pct",
  "position_size_pct": 10,
  "max_positions": 5,
  "stop_loss_pct": 8,
  "take_profit_pct": 0,
  "max_holding_days": 0
}

Reading top to bottom: trade three symbols on daily bars; go long when the 50-day average crosses above the 200-day and the 14-day RSI is above 50; exit when price loses the 50-day average; put 10% of equity in each position, hold up to five at once, with a hard 8% stop. Zero means “disabled” for the stop, take-profit, and holding-period fields. Two fields not shown: entry_price_field picks which price of the triggering bar fills the entry (open, high, low, or the default close), and entry_rule_short defines a short side with the same grammar.

Indicators

Indicators are named columns computed before the rules run. The name_length convention (sma_50, rsi_14) is what your rules reference — and when a chat edit mentions a simple indicator that does not exist yet, it is added automatically.

TypeWhat it computes
smaSimple moving average of the source over N bars
emaExponential moving average — recent bars weigh more
rsiRelative strength index, 0–100 — how one-sided recent gains versus losses have been
zscoreHow many standard deviations the current value sits from its rolling mean
atrAverage true range — the typical bar-to-bar movement, a volatility yardstick
stddevRolling standard deviation of the source
rolling_max / rolling_minHighest / lowest value over the window (e.g. the 252-day high)
lagThe value of a column N bars ago
vwap_proxyA volume-weighted average price approximation
customYour own formula over existing columns, e.g. close / sma_100
externalA column borrowed from another symbol — SPY’s close as spy_close, for market-regime filters

Entry and exit rules

Rules are boolean expressions over the data columns: prices (open, high, low, close, volume), your indicators, and calendar columns (month 1–12, day_of_week where 0 is Monday, year). Combine comparisons with & (and), | (or), ~ (not), and parentheses — the words and / or work too. The allowed functions are abs, min, max, sma, ema, rsi, zscore, atr, lag, pct_change, crosses_above, and crosses_below. close[N] is shorthand for lag(close, N).

ExpressionReads as
crosses_above(sma_50, sma_200)The 50-day average crosses above the 200-day — the classic golden cross
close > sma_20 & rsi_14 > 55Price above its 20-day average and RSI above 55
pct_change(close, 5) > 0.03Price rose more than 3% over the last 5 bars
close > close[20] * 1.10Price at least 10% above where it was 20 bars ago
month == 5Only bars in May — seasonal rules are just calendar columns
day_of_week == 0Mondays only
close < entry_price * 0.95Exit rule: price is 5% below what the position paid
days_held >= 10 & close < sma_20Exit rule: held at least 10 days and price lost its 20-day average

Position context in exit rules

Exit rules see the open position as extra variables: entry_price, days_held, current_stop, initial_stop, r_value (the dollar distance from entry to the initial stop), shares, and position_side with the is_long / is_short shorthands. Entry rules cannot see any of these — there is no position yet.

Stops, take-profit, and time exits

  • stop_loss_pct(0–50, 0 disables) — a hard stop below the entry price (above it, for shorts). Checked against each bar’s low or high, so it can trigger mid-bar; the fill is simulated at the stop price plus slippage.
  • take_profit_pct (0–500, 0 disables) — the mirror image on the profit side.
  • max_holding_days(0–365, 0 disables) — closes the position at that bar’s close regardless of anything else.

For an open position the engine checks in a fixed order: stop-loss first, then take-profit, then the time exit, then your exit_rule — which is evaluated at the close of the bar. Each closed trade records which exit fired, and the results report breaks performance down by exit reason.

Position sizing

  • notional_pct — each position uses position_size_pct percent of current equity (1–100). Simple and predictable.
  • risk_pct — each trade risks risk_per_trade_pct percent of equity (0.05–10), and the share count is derived from the distance between entry and stop. A wider stop means a smaller position; every trade risks roughly the same dollars.

max_positions (1–20) caps how many positions are open at once, and the optional ranking_field names an indicator used to rank candidates when more entries qualify than there are open slots. One sizing smell worth knowing: if position size × max positions goes far past 100% of equity, the backtest is quietly assuming leverage — the chat will warn you when it sees this.

Where you see it in the lab

The Rules tab of the workspace shows the same strategy three ways: a plain-English translation of the rules (with an AI-polished version when available), the raw JSON of the spec as it was actually run, and a download button that saves it as strategy.json. The English version is meant to be complete enough to trade by hand, and the JSON is exactly what exports embed — there is no hidden version of your strategy anywhere.

Historical simulation for research and education. Not financial advice. Past performance does not predict future results.