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.
| Type | What it computes |
|---|---|
sma | Simple moving average of the source over N bars |
ema | Exponential moving average — recent bars weigh more |
rsi | Relative strength index, 0–100 — how one-sided recent gains versus losses have been |
zscore | How many standard deviations the current value sits from its rolling mean |
atr | Average true range — the typical bar-to-bar movement, a volatility yardstick |
stddev | Rolling standard deviation of the source |
rolling_max / rolling_min | Highest / lowest value over the window (e.g. the 252-day high) |
lag | The value of a column N bars ago |
vwap_proxy | A volume-weighted average price approximation |
custom | Your own formula over existing columns, e.g. close / sma_100 |
external | A 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).
| Expression | Reads 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 > 55 | Price above its 20-day average and RSI above 55 |
pct_change(close, 5) > 0.03 | Price rose more than 3% over the last 5 bars |
close > close[20] * 1.10 | Price at least 10% above where it was 20 bars ago |
month == 5 | Only bars in May — seasonal rules are just calendar columns |
day_of_week == 0 | Mondays only |
close < entry_price * 0.95 | Exit rule: price is 5% below what the position paid |
days_held >= 10 & close < sma_20 | Exit 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 usesposition_size_pctpercent of current equity (1–100). Simple and predictable.risk_pct— each trade risksrisk_per_trade_pctpercent 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.