Adjustments and triggers
Adjustments change a position while it is open: roll a leg, add a hedge, drop a leg, or resize. Each adjustment pairs one named trigger with one action, and the engine checks it every minute against every open position. This reference states each action, the accessors a trigger can read, and the typed no-action reasons; the trigger itself is written in the Signal language.
What an adjustment is
An adjustment is a named trigger plus one action. The trigger is a Signal that returns a truthy value (Float64 1.0/0.0); the action is the change applied when the trigger reads 1.0. Each open position carries the strategy's full adjustment set. How the trigger is evaluated depends on what it reads. A trigger that references position() or leg(slug) is evaluated once per open position per minute against that position's own legs, so the same trigger can read 1.0 on one position and 0.0 on another held at the same minute. A trigger that reads only book() is evaluated once per minute for the whole account, and that single value is applied to every open position, so a book-level trigger that fires runs its action on all open positions at once. Either way, when the value reads 1.0 on a position, the trigger's one action runs on that position.
Trigger names must be unique within a strategy. The name is how a fired action, a rejection, and a no-action are attributed back to the trigger in the results.
// Trigger: roll when the short put's delta breaches 0.40
leg("short_put").delta > 0.40The trigger reads live state on the position under evaluation. Here leg("short_put").delta is the current delta of the held short put at this minute, and the comparison returns 1.0 on the minutes where it sits above 0.40.
The four actions
Each adjustment carries exactly one action. Use the exact action name.
replace_legadd_legremove_legadjustment_full_close.scale_lotsreplace_leg and add_leg resolve their contract at request time, the same way primary legs do (see selecting contracts). The replacement or added contract comes from an option.contract(...) selection evaluated at the adjustment minute, so the same threshold and tie-break rules apply.
Both replace_leg and add_leg operate only on standard-multiplier legs (100 shares per contract). A leg carrying a non-standard multiplier after a split raises multiplier_mismatch and the adjustment is a no-action.
Adjustment fills use the backtest's fill mode. A roll closes the old contract and opens the new one at that mode's price; a scale up or down fills the added or removed lots the same way. See fills, marks and slippage.
scale_lots parameters
Set exactly one of the three. Setting zero or more than one is rejected at configuration time.
delta_lotstarget_lots≥ 0target_lots=0 fully closes the position with reason adjustment_full_close.delta_pct≥ -1.0-1.0 is the floor; for a full close use target_lots=0.A resolved lot count that lands below zero, or equal to the current count, is a no-action (adjustment_no_change). A scale up runs through the margin gate before it commits (see margin and auto-decomposition); if the account cannot support the larger position the action is rejected with margin_insufficient.
Triggers are level-based
Triggers are evaluated on level, not on edge. There is no latch and no debounce. A trigger that stays true fires every minute the condition holds, on every open position. So an action must be self-limiting or idempotent under repeated firing.
This is the same re-firing footgun as entry and exit signals. Write the action so firing removes the condition: a remove_leg that empties the position ends its eligibility, a replace_leg that rolls the leg out of the trigger region reads 0.0 on the next minute, and a scale_lots toward a fixed target_lots is a no-action once reached. An action that leaves the condition true keeps firing. See entry, exit, and timing for the same pattern on the entry side.
To control firing frequency, gate the trigger yourself. Combine the condition with a time-of-day or state check in the trigger Signal rather than relying on a built-in cooldown, because there is none.
A slug that is not present on the position resolves to null, which makes the trigger falsy, so no action runs and the engine records signal_null for that position and minute. A leg removed by an earlier adjustment stops driving any trigger that reads it.
What a trigger can read
An adjustment trigger runs in the adjustment context, which admits three accessors: book(), position(), and leg(slug). book() returns account-level state (net liquidation value, maintenance margin, excess liquidity, and the rest). position() returns state for the position under evaluation. leg(slug) returns state for the named held leg.
// Trigger: defend margin when account excess liquidity drops below $5,000
book().excess_liquidity < 5000leg(slug) reads the live held leg on the position being evaluated, including its current greeks, quote prices, strike, dte, and multiplier at this minute. It also carries the entry snapshot captured when the leg opened, so a trigger can compare current state against the state at open. That snapshot is preserved across a roll: replace_leg keeps the slug, so a leg that has been rolled still reports the original open values. For the full accessor-context table and the columns each one exposes, see signals inside a backtest.
Slug rules
A slug names a role in the position, not a specific contract. That is what makes a roll possible: replace_leg swaps the contract underneath a slug while the slug stays put, so any Signal referencing that slug, including a rolling window on it, continues across the roll without resetting.
replace_legandremove_legmust reference a slug that already exists on the position, either an original leg or one an earlieradd_legcreated.- An
add_legslug must not collide with a slug already on the position.
When an adjustment cannot run, it is a typed no-action: the position is left unchanged and the reason is recorded against the trigger name. The reasons:
leg_not_found_in_positionreplace_leg or remove_leg referenced a slug not on the position.duplicate_slug_in_positionadd_leg slug collided with an existing slug.multiplier_mismatchreplace_leg targeted a non-standard-multiplier leg, or add_leg was attempted on a position whose existing legs carry a non-standard multiplier.adjustment_no_changescale_lots that lands on the current count or below zero).contract_selection_failedforward_quotes_emptymargin_insufficientLimitations
Two behaviors are not modeled.
- Latch and debounce trigger semantics. Triggers are level-based only. There is no built-in state that suppresses re-firing after an action, and no built-in cooldown between fires. Gate the trigger yourself.
- Computed leg slugs. A
leg(slug)slug must be a literal. Building a slug from an expression is not available. Reference each leg by its written name.
See platform status for the full status ledger.