Choose an execution mode¶
Choose the runtime before interpreting orders or account values. A valid strategy describes a rule; the selected runtime must also supply its data, instrument terms, capital and execution behavior. Synthetic examples establish mechanics rather than historical trading performance.
What each runtime supplies¶
| Runtime | Result | Supported scope | Important limits |
|---|---|---|---|
Local event backtest (atom run, Engine) |
Simulated orders, fills, positions and cash | Single, broadcast, book(), forecasts, clocks and supported composites |
Next-bar-open fills by default; unit multipliers; explicit data/cost/margin configuration |
| Vectorized research | Signal-derived actions and position value | One ordered symbol and aligned entry/exit masks | Next-close default; no full cash, risk, settlement or live lifecycle |
| Triggered research | Order intents on selected rows | Host-supplied predicate and trigger callback | Intents are not fills; skipped rows do not receive full event-engine supervision |
| Injected synchronous paper/live | Host adapter orders and supplied feed | Host-supported dispatch and instrument contracts | Atom bundles no broker adapter; actual fills and deployment recovery need integration acceptance |
| Async live integration | Sync/async on_tick, observed fills and optional risk policy |
Explicit client/portfolio, live contract specifications and lifecycle runner | Host owns universe/cadence/recovery; this path does not automatically reproduce book()/forecast orchestration |
The bundled CLI does not run --mode paper or --mode live; hosts use the Python
integration APIs. Use the local backtest for an immediately runnable experience.
See research experiments for fast-mode accepted inputs and
rejected execution settings.
For example, a daily rule requests BUY 10 after Monday's close of 100. With local event defaults and Tuesday's open of 103, its simulated cost is 1,030 before commission, subject to admission. Vectorized next-close pricing can instead use Tuesday's close, and a triggered intent supplies no fill price. A real broker may partially fill or reject it. The same signal does not imply the same money outcome across these modes.
Protection follows confirmed positions¶
In the event simulator, long(1, stop=.05, hold=5) freezes the supplied
settings when submitted. Its stop and holding countdown start only when the
entry fills. If the decision close is 100 and the fill is 110, the long stop
is 104.5; a short filled at 110 has a stop at 115.5. Entry slippage belongs in
that price basis; commission does not. TradeScript's entry_price expression
still means the entry decision's close. Absolute stop_price= and
target_price= values remain the supplied snapshots, even across a gap.
Engine backtests and paper runs using Atom's simulator execute price protection on the eligible breach bar, including the range after an opening entry fill. For a long stop, an open at or below the stop fills at that open; otherwise a low reaching the stop fills at the stop. For a short, an open at or above the stop fills at the open; otherwise a high reaching it fills at the stop. Targets require one tick of trade-through at the open too: long targets fill at an open at least one tick above the target, or at the target when the high trades one tick beyond it. Shorts mirror this below their target. An exact opening touch or sub-tick move rests until the range trades through by a full tick. Ordinary signal entries/exits and time exits retain next-open execution. Injected live clients are unaffected.
For example, one long fills at 110 and arms stop 104.5. A fill-bar low of 106
survives. The next bar (111, 114, 103, 108) exits at 104.5, leaving cash 994.5
from 1,000 and realised P&L −5.5, before costs. A gap bar (101, 105, 99, 103)
instead exits at 101, leaving 991 and P&L −9. The confirmed close removes that
owner's protection, so the following bar cannot repeat the exit. Slippage is
applied once to a market/stop fill; fees are charged only on actual fills.
Executable OHLC must be finite, positive and satisfy low ≤ open/close ≤ high;
malformed or missing data raises ATOM-FILL-DATA before an affected fill.
An opening-eligible target precedes later intrabar stops. When competing
protections are reached only intrabar, choose the adverse eligible exit;
between a crossed fixed stop and trail, the tighter level is encountered first.
For two longs at 100 with stop 95 and target 105, (100, 106, 94, 102) sells two
at 95: cash 990 from 1,000, realised P&L −10 before costs. The short mirror buys
two at 105 with the same result. For one long with tick 0.01, (105, 106, 94, 100)
also rests at the opening target, then sells at stop 95 for loss 5 and cash 995,
recording ambiguity. Open 105.005 gives the same result. Previously these opens
incorrectly filled the target for a profit. With (105.01, 106, 94, 100), the
target fills at open 105.01 for profit 5.01 and cash 1,005.01, before costs;
there is no ambiguity. A short from 100 with stop 105 and target 95 mirrors this:
open 95 rests, then high 106 and low 94 lead to stop 105 and loss 5 with ambiguity;
open 94.99 fills the target for profit 5.01 before the later stop.
Inspect wiring.order_client.stop_registry.ambiguities after wiring =
engine.start(...). Both ordinary and independent-batch competing intrabar hits
append a structured protection_ambiguity record: symbol, logical bar,
owner (symbol/strategy/group/generation), levels keyed by trigger kind and
selected (stop_fired, target_hit or trail_hit). The history retains up to
the client's max_log_history records (default 100,000), including across
checkpoint resume. Ordinary protection also forwards the record
to the configured runlog sink.
Trails test the prior anchor, then ratchet only after survival. One long at 100
with trail=.05 survives (100, 110, 97, 108) using stop 95 and carries 104.5.
On (108, 112, 103, 106) it exits at 104.5 for cash 1,004.5 and profit 4.5 before
costs. The new high 112 cannot tighten that bar's exit first.
Rejections and partial confirmations¶
A rejected simulator protective exit retains protection and releases the failed attempt's reservation. The next eligible bar selects among the retained stop, target and trail levels again. ACK alone never retires protection or changes accounting; only confirmed fills do. This applies to ordinary positions and independent purchases, including shorts. Other symbols' bars cannot fill the exit.
For example, long(2, stop=.05) filled at 100 leaves cash 800 from 1,000 with
zero costs. Rejecting its exit at 95 leaves both units protected, cash 800 and
realised P&L zero. (100, 101, 99, 100) does not retry the fill; a subsequent
(100, 106, 94, 100) sells two at 95 for cash 990 and P&L −10. The retry is
selected afresh on each bar, with no extra fill on the rejection bar.
With long(1, stop=.05, target=.05) at 100, reject the stop on
(100, 104, 94, 100). A following (110, 112, 90, 100) exits at the open 110
via the target, giving cash 1,010 and P&L +10. A low of 108 gives the same
result. A following (100, 106, 94, 100) instead takes the adverse stop at 95
and records ambiguity. Previously a frozen retry could miss the target.
For long(1, trail=.05) at 100, reject on (100, 106, 94, 100).
(100, 110, 96, 108) survives and ratchets the trail to 104.5;
(108, 112, 100, 106) exits there for P&L +4.5. Partial remainders follow
the same selection rules; the filled part contributes only its actual P&L.
With long(3, stop=.05) at 100, confirming one exit at 95 leaves cash 795,
P&L −5 and two units still protected. Their in-flight reservation ends before
the next observed bar's protection selection. A gap to 90 sells only those
two: cash 975 and P&L −25. At 0.10 commission per filled unit, fees total 0.60
and final cash is 974.40. The simulator's cumulative-confirmation seam applies
only the newly confirmed quantity; replaying the same confirmation does nothing.
Once that attempt ends, its old confirmation cannot execute the quantity again.
An error after accounting is reported without repeating the committed fill.
It does not supply a new liquidity model. An exit exceeding confirmed owner
quantity raises ATOM-PROTECTION-STATE before cash or inventory changes.
Checkpoints save each pending protective order's identity and confirmed quantity, so multiple exits for one owner retain separate reservations. Their combined remaining quantity cannot exceed that owner's holding. Canceling an exit releases only its reservation and clears its batch's protective exit reason; it leaves accepted protection and other pending exits intact. Sizing counts only the unfilled remainder of each order.
A late confirmation after restart matches the saved order identity and applies
only the new quantity. Completed/canceled attempts retain bounded identity
receipts, so a repeated notification has no second accounting effect. Unknown
or contradictory identities raise ATOM-PROTECTION-STATE. Ambiguity history,
total commission and rejection counts continue across full and differential
checkpoints. Missing or contradictory reservation evidence refuses before
installation; rerun the source bars if the saved checkpoint lacks that evidence.
Simulator entries fill once; partial entry confirmations and same-order VWAP
re-anchoring are not supported. Express clients and AsyncLiveEngine retain
their existing behavior.
Replay the six worked examples¶
This complete Python strategy submits once, at the first decision close:
from atom.library.orders import long
def on_tick(bar, params, state):
if not state.get("entered"):
long(1, stop=.05)
state["entered"] = True
Use starting capital 1,000, the bars described above, and zero commissions except
for partial, which charges 0.10 per filled share. Change the quantity in long
to the table's value. For ambiguity, add target=.05; for trail replace
stop=.05 with trail=.05. The rejection and partial examples use controlled
simulator confirmations; the strategy does not request a liquidity model.
| Example | Quantity | Exit prices | Gross P&L | Final cash |
|---|---|---|---|---|
| normal | 1 | 104.5 | -5.5 | 994.5 |
| gap | 1 | 101 | -9 | 991 |
| ambiguity | 2 | 95 | -10 | 990 |
| trail | 1 | 104.5 | 4.5 | 1004.5 |
| reject | 2 | 95 | -10 | 990 |
| partial | 3 | 95 then 90 | -25 | 974.4 |
Normal uses B0 (99,102,98,100), B1 (110,113,106,112),
B2 (111,114,103,108), B3 (120,123,118,121). Gap replaces B2 with
(101,105,99,103). Each other example starts with decision bar
(100,100,100,100) before its described fill bars. All end flat. The entry
decision close remains 100 even when the fill is 110; TradeScript entry_price
keeps that decision-close meaning, while percentage protection anchors to 110.
For a fixed price distance, use stop_amount= and target_amount=:
from atom.library.orders import long
def on_tick():
# Worked input: the decision's ATR(14) result is 2.5 price units.
atr_at_decision = 2.5
long(1, stop_amount=2 * atr_at_decision, target_amount=7)
At a decision close of 100, this freezes amounts 5 and 7. A later fill at 110
arms stop 105 and target 117, even if ATR is now 20. stop_amount=5 is the
equivalent points form. short(1, stop_amount=5, target_amount=7) instead arms
115 and 103. With initial cash 1,000 and no costs, cash is 890 long or 1,110
short. Supply the observed ATR result in a real strategy; this example fixes
it to make the arithmetic reproducible. entry() and position.long/short()
accept the same amount keywords. Dynamic position.set_stop/target() still
take fractions. Risk sizing with risk_pct= still requires fractional stop=.
Choose one fraction, amount or absolute level per kind; omitted protection stays
omitted. For an absolute snapshot, long(1, stop_price=95, target_price=125)
keeps levels 95/125 after the fill at 110. Expressions passed as absolute prices
are evaluated when submitted too. TradeScript stop: 2 ATR and target: 7 points
now use the amount behavior; their old close-derived long levels were 95/107
in the distance example. Use stop_at: close - 5 and target_at: close + 7
when those decision snapshots are intended. No compatibility flag is needed.
Each symbol/strategy/group allocation owns its protection. If strategy A owns
one long unit with stop 95 and B owns one short unit, net inventory is zero,
but A still has its one-unit stop. A protective exit carries A's tags and
reserves A's quantity; a subsequent close() in A's same decision submits
no duplicate exit. B's holding and protection remain independent.
Use close() for this coordinated exit. Raw client.buy() and client.sell()
submit the requested quantity independently; they do not express a target of flat.
A confirmed partial reduction keeps the accepted levels, trail anchor and
remaining hold count for the remainder. An unprotected addition keeps them too.
An addition explicitly supplying stop= replaces that stop after its fill,
using the allocation's confirmed volume-weighted entry price: one unit at 110
plus two at 111 with stop=.05 gives 105.133333. Omitted fields stay unchanged;
an explicitly supplied trail or hold replaces that setting. Closing or reversing
retires the old position's levels and timer. An unprotected reopening inherits
nothing. Submitting a close alone does not cancel protection.
Independent TradeScript purchases (even if holding) use this same protection
lifecycle. Each purchase is its own entry group: its stop, target, trail and
hold belong to its confirmed position, even alongside an ordinary entry on the
same symbol. With one long unit filled at 100 and another two at 120, 5% stops
are 95 and 114. A bar opening at 116 and reaching low 110 sells only the two
newer units at 114 for 228; from initial cash 1,000, cash becomes 888 and the
older unit remains protected at 95. If that bar gaps open at 110 instead, they
sell for 220 and cash becomes 880. Previously this exit waited for the next
open: a subsequent open of 108 produced cash 876. Breach-bar pricing replaces
that delayed protective fill without changing ordinary signal-exit timing.
Entry hold= accepts a positive whole number, including 2.0. Ordinary and
batch entries reject 1.5, True, strings, NaN and infinity with
ATOM-PROTECTION-STATE. Distances and absolute levels must be finite positive
numbers, and derived levels must remain positive at the confirmed fill.
For example, long(1, stop=.05, hold=float("inf")) admits no entry exposure.
An unrelated queued buy of one at 110 still completes, leaving cash 890 from
1,000 before the essential error. Omit hold for no time limit.
If a valid five-point long stop instead fills at 4, activation fails because
the derived stop is −1. The buy and an unrelated buy at 110 remain filled:
cash is 886, both positions remain, and no partial stop/target/hold is installed.
A short five-point target at fill 4 fails likewise (cash 894 with that unrelated
buy); a short five-point stop is valid at 9. The error never undoes real fills.
For a batch with hold=2, the first observation of its confirmed fill consumes
one bar; the next observation of that symbol requests its close. Repeated
callbacks for the same bar and bars for other symbols do not consume another
batch bar. Confirmed closure retires the count; a later occurrence starts anew.
position.set_stop(.05), set_target(.10) and set_trail(.02) modify the
confirmed position in the running strategy/group, including composite legs.
They require an open owner. Direct registry stop, target, trail and time-stop
registration has the same requirement; a missing owner raises
ATOM-PROTECTION-STATE. For a pending entry, put protection on long() or
short() instead of registering it immediately after client.buy().
External hosts must reconcile each owner's confirmed quantity and average entry
price before using these setters, whether or not they expose a position tracker.
Direct registry reads, price checks and clearing use the same running owner as
registration. A host without a position tracker uses its confirmed owner state
when sizing close(): A long one and B short one still lets A close one unit.
Resume a backtest or paper run with its Engine checkpoint to retain each strategy/group's accepted protection. For example, if A holds one unit bought at 100 with a 5% stop and B holds one bought at 200, A resumes with its saved level of 95. If A is short from 100, its saved stop is 105. An opposing B position that makes the symbol net flat does not change either level. The checkpoint must contain each owner's inventory, confirmed entry price and protection; restarting a strategy declaration alone does not recreate them. Standalone protection rehydration and direct order-queue restore are removed in 5.0.0. Use Engine checkpoint resume for continuation, or start a fresh run when the required checkpoint state is unavailable.
The explicit check_stop(.05) action uses the same owner's confirmed entry
price, including on tracker-free hosts. It returns False when the running
strategy/group is flat; a held position missing its owner raises
ATOM-PROTECTION-STATE.
Invalid ordinary or batch entry settings reject that submission. With deferred fills,
Atom drains unrelated queued orders before raising ATOM-PROTECTION-STATE,
even if the queue is empty. A failure preparing protection after a fill keeps
that fill and its accounting; a failed replacement preserves previously accepted
levels. Immediate single-order and composite submissions raise before returning;
deferred fills raise after the queue drains.
Checkpoints retain exact owners, confirmed quantities and average entry prices,
generations, levels, timers and pending intents. Tracker allocations must also
agree with the retained portfolio inventory; an empty tracker cannot replace
an existing holding.
Batch checkpoints now retain protection with the same owner state as ordinary
positions. A held batch checkpoint whose protection exists only in the old
purchase ledger is refused with ATOM-PROTECTION-STATE; Atom does not reconstruct
its levels or countdown. Use a complete new checkpoint or rerun the backtest from
its source bars. Flat batch checkpoints retain their existing behavior.
Every held allocation must have a matching owner with the same side and quantity,
even if no stop was requested, and every owner must match a held allocation.
Missing or contradictory ownership rejects the
whole restore before installation, including old unprotected checkpoints holding
one unit and named positions whose owner record was omitted. Complete owner state
lets that restored one-unit long submit SELL 1 through close(). Empty holdings
need no owner records. Owner records also require the confirmed entry price;
start a fresh run when it or ownership is missing, rather than guessing either.
For example, A long one and B short one require two matching inventory entries
and two owners, even though the symbol total is zero. A host exposing only that
total rejects any checkpoint carrying protection or owner state with
ATOM-PROTECTION-STATE, even when all owner records are missing and the total
is zero. With both entries and owners restored, A's close()
sells one and B's close() buys one. An empty protection payload is a quiet
no-op. Any decoding, differential expansion or validation failure in a checkpoint
holding a position or carrying protection/owner state raises
ATOM-PROTECTION-STATE. This includes mismatched differential frame counts, a
non-mapping portfolio, an invalid queued quantity, a queue saved as a mapping
instead of a list, or a zero saved fill price, even if owner records are missing.
The original error remains chained as the cause. Refusal happens before changing
queues, portfolio, strategy state or pending resume data; no strategy ticks,
new orders or fills follow. Flat checkpoints without protection or owner state
retain the previous warning and continuation behavior where applicable.
Live clients without an Atom stop registry keep their existing resume behavior;
Atom skips owner and protection decoding on those clients. A simulator checkpoint
with queued orders warns and startup continues with the live state, including
older checkpoints with named owners that lack the confirmed entry-price field.
Simulator price protection and pending-exit recovery use the rules above.
TradeScript remains //@version=1.
Simulator limit and stop-limit orders¶
For client.buy("AAA", 1, limit=100), a bar opening above 100 must trade at least
one tick below 100 to fill at the limit. With the default equity tick 0.01,
low 100 does not fill; low 99.99 does. A sell limit needs high ≥ limit + tick.
Opening at least one tick through either limit fills at that open. A sell limit
105 on (105, 105, 104, 105) rests, as does open/high 105.005 with tick 0.01;
open 105.01 fills at 105.01 before costs. There is one policy and no touch-fill
compatibility flag. Explicit Order.tick_size takes precedence
over the existing asset-class defaults; composite legs use their parent's terms.
Unknown or invalid tick metadata when matching a limit raises ATOM-FILL-DATA.
Slippage cannot worsen a LIMIT or STOP_LIMIT fill past its limit. A buy limit at 100 with 1% slippage fills at 100 when reached intrabar. A favourable open of 99.5 also caps at 100; an open of 98 fills at 98.98. A sell mirrors the cap. Fees use the actual fill price and quantity; a resting order pays no fee.
client.buy("AAA", 1, stop=105, limit=103) activates on (102, 106, 101, 104)
but cannot reuse that bar's low: OHLC does not say whether 101 preceded 105.
The activation persists, and (104, 105, 102, 104.5) fills at 103 next bar.
If that later bar instead opens at 102, the activated buy fills at 102, not 103.
If the activation bar opens at 105, its subsequent range may fill the limit
on that same bar. The one-tick rule and slippage cap apply in both cases.
Unsupported queued order types raise ATOM-ORDER-TYPE instead of filling as
market orders. These rules apply to Atom's event simulator and simulated paper
client; broker/Express and AsyncLive execution are unchanged.
Instrument units and capabilities¶
| Instrument | Columnar accounting | Live reducing-order requirements |
|---|---|---|
| Equity | Shares, contract_size=1 |
Symbol is sufficient; group and strategy ownership are preserved |
| FX | Native base units, unit multiplier | Explicit base/quote currencies; Forex author lots are converted to units before native execution |
| Crypto spot/perpetual | Native units, unit multiplier | Explicit base/quote assets; settlement asset for the selected contract where applicable |
| Option/future | Only unit-multiplier contracts | Underlying/expiry plus option strike/right; explicit contract_size=1 |
| Multiplier 50/100 or another non-unit value | Rejected by local simulator and live capability checks | Requires a different accounting implementation; naming the contract does not enable it |
The default Order contract and the local CLI demo are cash equities. A ticker
such as ES, EUR_USD or BTCUSDT does not change that contract. When the local
engine is explicitly started with market="forex", "crypto", "crypto_spot",
"crypto_perp", "futures" or "options", every fill must carry the corresponding
instrument type. An implicit equity order in those markets is rejected with
explicit instrument metadata required for market=...; it formerly could fill
as equity. The existing Order validation still requires the product's terms.
For example, an FX order uses explicit identity even if its symbol is AAPL:
from atom.models import Order
order = Order(symbol="AAPL", side="BUY", qty=1, order_type="MARKET",
instrument_type="FX", base_currency="EUR", quote_currency="USD")
An omitted market retains the documented cash-equity default; it is not product autodetection. The standalone TradeScript demo explicitly selects that equity contract. To model FX or crypto, the host must supply metadata-bearing orders and the appropriate supported runtime; renaming an equity demo is insufficient. No TradeScript syntax or new error code is introduced by this validation.
Unit support describes representation and tested accounting, not full exchange semantics. Borrow, financing, expiry delivery, margin, FX conversion and venue rules still require the corresponding models and host integration. In particular, a normal US option with multiplier 100 is not modeled by pretending it has multiplier 1. Do not multiply quantity and lose the original contract identity.
Supply live contract terms¶
An integration constructs AsyncLiveEngine(..., instrument_specs={...}) with
authoritative terms keyed by symbol. These terms are copied and validated through
the same Order rules used for execution. They are not inferred from symbol text.
Equities retain their symbol-only default. A held non-equity position must have
its terms available before activation; incomplete or contradictory terms raise
before the runner connects or subscribes.
This complete configuration check uses a restored FX position. It makes no broker connection and does not treat restoration as an executed fill:
from atom.engine.async_live import AsyncLiveEngine
from atom.models import Portfolio
portfolio = Portfolio(base_currency="USD", cash=1000)
portfolio.add_position("EURUSD", 2, 1.10, instrument_type="FX", currency="USD")
def on_tick(bar, params, state):
pass
engine = AsyncLiveEngine(
on_tick,
portfolio=portfolio,
instrument_specs={
"EURUSD": {
"instrument_type": "FX",
"base_currency": "EUR",
"quote_currency": "USD",
"contract_size": 1,
}
},
)
engine.validate_live_activation()
A flatten request for the two-unit long creates a SELL 2 with the same currencies, strategy and group; a two-unit short creates BUY 2. The broker must acknowledge and report fills before completion. Terms learned from a successful identity-bearing fill can support subsequent exits in that process. After restart, the host must restore the portfolio and supply specifications again. A configured contract cannot silently change between fills, partials or duplicate notifications.
Unknown specification fields are errors. Supported fields are instrument type, underlying, strike, expiry, right, contract size, tick/pip size, base/quote currency, base/quote asset, settlement asset and leverage. For options/futures pass a valid expiry date. Price and quantity remain order-specific. Live strategy requests through the injected client are validated before adapter submission; raw adapter references remain the host's responsibility.
If an unexpected real fill has an unsupported multiplier or contradicts configured terms, Atom halts before modifying its portfolio and retains the fill as pending in the ledger. Reconcile the real account and restore correct accounting before resolving that record. Never mark it applied merely to restart. Durable runners require the configured ledger, broker/account identity and reconciliation policy; ephemeral ledgers are only for tests/replay. See risk checks for exit timeouts and reconcile-before-retry behavior.
Reproducible local time¶
Event backtests timestamp order lifecycle events with their observed bar clock. Each thread or async task has its own clock, and a nested run restores the caller's previous clock even after failure. Orders outside a configured run use wall time unless the caller explicitly sets a clock. This controls order stamps; it does not reset arbitrary strategy globals or make external data deterministic.
Commission models receive this execution time as well, including next-open fills. A historical fill in 2024 does not receive today's fee-schedule date. Custom models should be deterministic functions of their supplied order/fill; admission may evaluate a candidate before the final fill.
Account for execution costs¶
An undeclared custom commission model returns an amount in account currency,
preserving the existing contract. A model can opt into currency="QUOTE" for
the order's quote units or an explicit currency code such as "INR". Built-in
basis-point fees use quote currency; Zerodha models declare INR and IB declares
USD. The simulator converts once before admission, debit and reported totals.
For example, ten FX units at JPY 100 produce JPY 1,000 turnover. At 100 basis
points the fee is JPY 10, or USD 0.10 with JPY/USD 0.01. A USD 1,000 cash account
then has cash 989.90 and equity 999.90 at unchanged prices. A custom model
returning 1 without a currency declaration still charges USD 1 in that account.
Supply the required FX before foreign fees/fills; missing conversion is not a
zero fee. The reference schedules are models, not a guarantee of current tariffs.
A configured commission/slippage exception or non-finite result invalidates the
execution result even when ordinary strategy errors are configured to continue.
The same rule applies inside optional lifecycle hooks: a nested essential
execution failure cannot be turned into a successful run by a hook's warning
handler. Already observed fills remain observed; abort and discard that failed
backtest result rather than interpreting the exception as a rollback of trades.
Use commission_model=None for intentionally frictionless simulation. Do not
use a failed cost provider as a frictionless opt-out.
If an essential failure occurs in on_breach, the selected risk response is
attempted before that error propagates. In a local next-open simulation this
submits an unwind; it cannot invent a fill after the last observed bar. An
aborted run may therefore retain an open position and a checkpointed pending
exit. Discard the failed performance result and inspect outstanding work before
any restart. Async integrations likewise require observed fills, not merely an
exit acknowledgment, to establish a closed position.
Settle only known expiry values¶
The local unit-contract model values an expiring option from its supplied underlying quote and strike; a future uses its available mark. Every due contract's reference must be known before any is settled. An unquoted underlying does not make a short call worthless: the run raises a settlement error with the existing positions and cash intact. Supply the reference data and rerun. Atomic rollback also protects against a failed financial write.
This is a last-observed-price simulation, not official exchange settlement, exercise, physical delivery or live brokerage verification. Non-unit contracts remain unsupported. These corrections can change old backtest results and need re-execution; no TradeScript grammar/version change is required.