Quickstart: your first backtest
By the end of this page you have a finished backtest result on screen: a short-premium SPY strategy run over a fixed date range, with an equity curve, a trade ledger, and a headline summary you can read. One path, every step succeeds.
What you'll build
A single short put on SPY, 30 days to expiration, near 0.30 delta, held one lot short and rolled through a fixed window of history. This is the first rung of the strategy ladder every later page builds on: short put, then put credit spread, then iron condor. You choose the fill assumption before you run, so the result is never a number without the settings that produced it.
Before you start
This tutorial fixes every choice so the run is reproducible. The defaults below shape the number you read at the end.
This walkthrough runs SPY over a fixed date range, and you choose the fill mode. The equity curve marks at mid between trades. The backtest engine opens its first fill at 09:35 and places its last trade at 15:55 on a normal session (12:55 on a 13:00 half-day). Those engine session bounds are 5-minute offsets from the open and close, and they are separate from the 09:31-to-close spine a signal reads through an accessor. Sharpe, Sortino, CAGR, and annualized volatility scale by 252 periods.
Step 1: Open a new backtest
Open a new backtest to get a blank builder.
Under the interface, a backtest is four things: the legs (the option contracts you hold), an entry signal, an exit signal, and a sizing and fill configuration. Every step below fills in one of those pieces. Nothing runs until all four are set.
Step 2: Pick the symbol and date range
Select SPY as the symbol. Enter a start date and an end date for the run.
The end date is inclusive of the full final trading session. A bare end date means the run covers that day through its close, not up to the morning of it. For what symbols and history are available, see Data coverage.
Step 3: Define the strategy (legs)
Add one leg: a short put. In the position designer, set Days to expiration to 30, set Option type to Put, and select the contract by delta near 0.30.
Put deltas are signed negative, and the engine matches on the signed value, so a 0.30-delta put is selected at a delta of -0.30, not +0.30. Keep the sign negative anywhere you pick a put by delta. Choose exactly one selection dimension per leg: delta, log-moneyness, or strike. There is no default, so set one.
Set the contract's size to short one contract per lot. Size is the signed per-lot count on the selection, negative for short, positive for long. The selection decides which put you hold, and its size decides which direction and how many per lot.
Prefer code? The contract selection is written in the Signal language and returns the held contract, size included:
// Short put contract: 30 DTE, about 0.30 delta, one short per lot (size -1)
// Put deltas are signed negative, so a 0.30-delta put is delta=-0.30
option.contract(dte=30, type="put", delta=-0.30, size=-1)The Days to expiration field is the dte argument and Option type is type. For how a leg's contract expression fits a backtest, see signals inside a backtest.
Step 4: Set entry and exit
Set one entry signal and one exit signal. Both are truthy Signals. The engine reads them level-based every minute with no edge detection: while an entry Signal holds truthy, the engine attempts an entry every eligible minute up to the position limit, and while an exit Signal holds truthy it closes the position.
For this run, enter only when 30-DTE IV rank is elevated:
// Entry signal: enter only when 1-year IV rank is elevated
// 252 counts 252 DAILY observations because iv_daily is sampled to daily first
iv_daily = option.iv(30, sampling=sampling(period="day", time="close"))
rolling_rank(iv_daily, 252) > 0.50Exit when it falls back:
// Exit signal: close when 1-year IV rank drops below 0.30
iv_daily = option.iv(30, sampling=sampling(period="day", time="close"))
rolling_rank(iv_daily, 252) < 0.30The 252 window counts 252 daily observations, not 252 minutes, because iv_daily is sampled to daily before the rank runs. For an unconditional strategy, the entry Signal is always true. To gate on time of day, use time.minute_of_day().
Step 5: Size the position
Set starting capital to a value above zero, for example 100000. Then choose one sizing mode and its value. There is no default mode, so you supply both.
The five modes are capital_pct_nlv, capital_dollars, margin_pct_nlv, margin_dollars, and fixed_lots. For a deterministic first run, choose fixed_lots with a value of 1: one lot of the short put per entry. The maximum number of concurrent open positions defaults to 1, so the run holds one position at a time.
Step 6: Choose the fill assumption
Choose the fill mode. This is required; there is no default.
- The Backtest.ai fill model prices each fill where the calibrated model expects it to land between the mid and the touch. This is how paper and live trading price, so it tracks live behavior most closely.
- mid prices every fill at the quoted mid. You pay no spread, an optimistic bound.
- bid_ask buys at the ask and sells at the bid. You pay the full quoted spread on every fill, a pessimistic bound.
In every mode, the equity curve marks at mid between trades, and any forced liquidation crosses the spread regardless of the mode you pick.
The fill mode you choose here is the single largest driver of how conservative the result is. The Backtest.ai fill model prices where an order fills between the midpoint and the touch, matching how paper and live trading price. mid reports fills with no spread cost, the optimistic bound, which flatters a premium-selling strategy; bid_ask charges the full spread at the touch, the pessimistic bound. See how fills, marks, and slippage work.
Step 7: Run it
Launch the run. Results stream in batches, flushed at calendar-month boundaries during the run, with a terminal summary at the end: the equity curve, the margin curve, the per-trade ledger, and a running summary all fill in as the run progresses.
You know it is working when the equity curve begins drawing from the left and the trade ledger adds rows as positions close.
Step 8: Read the result
Read the headline summary panel. It carries:
- Total return % and CAGR (compound annual growth rate, annualized with 252 periods).
- Max drawdown %.
- Sharpe and Sortino, annualized with the same 252 periods.
- Win rate, trade count, and average P&L per trade.
- Close-reason counters, including the soft-trim count and the forced-liquidation count, so you can tell whether the risk layer ever stepped in.
- Final NLV, the ending net liquidating value.
Every run carries a benchmark, an S&P 500 total-return line by default, so a positive return has context to read against. The Backtests section covers the benchmark comparison in depth.
You have a finished result when the summary shows a trade count above zero and a final NLV, and the equity curve is fully drawn across your date range.
What you assumed
Every number above rests on defaults you set or accepted. Before you trust the result, know what shaped it:
- Fill mode. The Backtest.ai fill model prices between the mid and the touch; mid fills at the quoted mid with no spread cost; bid_ask crosses the full spread. The equity curve marks at mid between trades, and forced closes always cross the spread.
- Engine session window. First fill 09:35, last trade 15:55 on a normal session.
- 252-period annualization. Sharpe, Sortino, CAGR, and annualized volatility all scale by 252.
- Adjusted underlying prices.
bar.*reads are split- and dividend-adjusted by default; see stock price adjustments.
For the full accounting of what a backtest does and does not model, read backtest assumptions and realism.
Next steps
- Iterate. Change the entry threshold, the delta, or the DTE and re-run. The entry, exit, and timing and position sizing and capital pages go deeper on each control.
- Save it. Give the run a name so it persists at a clean, unhalted end. A saved run can be extended to a later end date without recomputing the range you already ran.
- Go further. Build the short put into a put credit spread, then an iron condor, following the backtest lifecycle. For the exact meaning of each summary figure, see the metrics glossary.