Signals inside a backtest (book, position, leg, contract)
Inside a backtest, a signal reads live engine state through four accessors: book(), position(), leg("slug"), and option.contract(...). Each returns a Table, not a Signal. You get a usable value by projecting one numeric column with field access, so book().excess_liquidity is a Signal and book() on its own is not. Which accessors are legal depends on the evaluation slot: an entry signal sees only book(), an adjustment trigger sees all three state accessors. Whatever you build, an entry, exit, or adjustment signal still has to resolve to a truthy Signal, and a contract-selection signal still has to resolve to one contract.
Tables, not Signals
book(), position(), leg("slug"), and option.contract(...) each return a Table: a row per trading minute with many numeric columns. Every column is Float64 and nullable.
Project one column with field access to get a Signal:
book().excess_liquidityposition().net_deltaleg("short_put").deltaoption.contract(dte=30, delta=0.30).mid
Field access is the only edge between the Table type and the Signal type. The projection is what feeds an entry, exit, or adjustment signal, because those slots need a Signal, never a Table.
Two field-access mistakes are rejected at compile time:
- Projecting a field off a Signal (not a Table). Only Table values support field access.
- Projecting a column a Table does not declare. The error is
Field '<name>' not declared on <Table>and lists the known fields.
Context gating: where each accessor is legal
An accessor is available only in the slots where its backing state exists. There is no position when an entry signal runs, so position() and leg() have nothing to read there.
book()position()leg("slug")book().book().LegTable)ContractTable, no entry_*)leg("slug") resolves another leg's run-start candidate contract.Using a state accessor outside its slot raises an error that names the offending accessors and the slot, for example ... uses backtest-state accessors (position()) that are not available in entry signals: no position exists when an entry signal is evaluated; only book() state is available.
leg("slug") requires a non-empty string-literal slug. A bare or computed argument raises leg() requires a non-empty string-literal slug, e.g. leg("short_put") at compile time. The runner then checks the slug against the position's declared strategy slugs at startup: a slug that no declared leg owns is a startup error, while a declared slug that is not held at a given minute reads as an all-null row (covered below).
The entry, exit, and adjustment slots themselves, and when each one is evaluated, belong to the Backtests section. This page owns the accessors.
option.contract(...), the ContractTable
option.contract(...) selects the single real option contract per minute that best matches your targets, and returns a ContractTable on the equity-option spine. This is contract selection: the program for a leg's contract ends on option.contract(...).
// Contract selection: the 30-DTE 30-delta call
option.contract(dte=30, delta=0.30)Project a column to read one attribute as a Signal, for example option.contract(dte=30, delta=0.30).mid.
Columns. The ContractTable exposes the shared option surface, 20 columns:
strikedtedte_threshold).delta, gamma, theta, vega, rhoivmid(bid + ask) / 2.bidaskunderlying_pricemoneynessln(K/forward); 0 is at-the-money-forward.multipliersizescaled_delta … scaled_rhosize.The greeks are the platform's computed greeks. Premiums here are quotes, not modeled fills; the fill a backtest books is set separately by the run's fill mode.
Parameters. Keyword-only after the required dte:
dtetype"call" / "put""call".deltadelta / moneyness / strike.moneynessln(K/forward) (centered on the implied forward).strikedte_threshold | dte_actual - dte | in days. Default5. Positive.delta_threshold | delta_actual - delta | . Default 0.05. Positive.moneyness_threshold | moneyness_actual - moneyness | . Default 0.05. Positive.strike_threshold | strike_actual - strike | . Default 1.0. Positive.size1 (one long contract per lot).Selection. Each minute, the accessor picks the one real contract that minimizes the threshold-normalized weighted sum of absolute distances to your targets, tie-breaking on ascending (expiration, strike, right). A minute with no contract inside every threshold yields an all-null row.
leg("slug"), the LegTable
leg("slug") reads the live per-minute state of one held leg, addressed by its strategy slug. Inside an adjustment trigger it returns a LegTable.
// Adjustment trigger: roll when the short put's delta breaches 0.40
leg("short_put").delta > 0.40The LegTable carries the same 20 option-surface columns as the ContractTable above, plus 13 entry-snapshot columns captured when the position opened and preserved across rolls:
entry_strike, entry_dte, entry_delta, entry_gamma, entry_theta, entry_vega, entry_rho, entry_iv, entry_mid, entry_bid, entry_ask, entry_underlying_price, entry_moneyness.
There is no entry_multiplier, entry_size, or entry_scaled_*.
The slug tracks the role, not the contract. After a roll (replace_leg) the same slug serves the replacement contract, so a rolling window over leg("short_put").delta continues across the roll rather than resetting. A slug that is declared but not currently held reads as an all-null row, so a trigger built on it evaluates to null and fires no action.
The entry_* columns exist only on a live held leg. Referencing one when selecting contracts is a compile-time error: a run-start candidate has no entry snapshot, so entry_* columns require a live leg.
position(), the PositionTable
position() returns a PositionTable: the structure under evaluation, aggregated across its legs. It is available only in adjustment triggers, where it evaluates once per open position per minute.
unrealized_pnlrealized_pnlinitial_marginmaintenance_marginmarket_valuenet_delta, net_gamma, net_theta, net_vega, net_rholotsleg_countdteentry_net_delta … entry_net_rhoNet greeks use null-propagating aggregation. If any one contributing leg value is null, the sum is null, not a partial total. Window a net greek to smooth it:
// Adjustment trigger: 15-minute average net delta of the open position above 0.50
rolling_mean(position().net_delta, 15) > 0.50rolling_mean(x, 15) on position() rows spans 15 minutes, because position() produces one row per trading minute. Sample to daily first if you mean days. See the execution model for observation-count windows.
book(), the BookTable
book() returns a BookTable: whole-account state, one row per trading minute for the whole run. It is the only state accessor an entry or exit signal can read, and it is also available in adjustment triggers.
// Adjustment trigger: fire when account excess liquidity drops below $5,000
book().excess_liquidity < 5000Values are dollars except where noted:
cashnlvinitial_marginmaintenance_marginexcess_liquiditymargin_utilization_pctunrealized_pnlrealized_pnlposition_countbook() evaluates once per minute for the whole run, regardless of how many positions are open.
Cross-leg references
One leg's contract selection can reference another leg's resolved contract with leg("slug"). Here the reference resolves to the named leg's run-start candidate (a ContractTable), so you can place one leg relative to another:
// Contract selection: long put 5 strikes below the short put
option.contract(dte=30, type="put", strike=leg("short_put").strike - 5)These references resolve once before the run, in dependency order across the entry legs. Only delta, moneyness, strike, and size may carry a cross-leg expression. dte, type, and the thresholds must be constants, because a per-minute threshold would change which contracts are eligible, not only how they are ranked. A leg cannot reference its own contract, and a reference to a slug that no entry leg declares is rejected before the run.
A referenced leg's candidate exposes the shared 20-column surface only, never the entry_* columns, because a candidate has no entry snapshot.
The slug in leg("slug") must be a string literal. Computed or non-literal slugs are not supported.
Use the canonical leg slugs short_put, long_put, short_call, long_call when writing multi-leg selections.
The per-slot return contract
Each slot has one return shape:
- Entry, exit, and adjustment signals resolve to a truthy Signal. The engine acts at a minute where the value is
1.0, and treats0.0,null, andNaNas no action. Evaluation is level-based per minute, not edge-triggered: the engine reads the value each minute rather than firing on a transition. This is why a comparison likeleg("short_put").delta > 0.40works directly. Comparisons returnFloat641.0/0.0(see operators and functions). - Contract selection resolves to one contract per minute via
option.contract(...).
The strategy semantics behind these slots, when an entry or exit signal is checked, how adjustment triggers fire actions, and how contract selection drives which contract a leg holds, live in the Backtests section.