Skip to content

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.