Skip to content

Protection And Recovery Examples

Read execution and protection rules first. These worked examples continue the same confirmed-position lifecycle.

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 in the fill rules, 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. External hosts with confirmed owner inventory can rebuild fixed percentages using host recovery.

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.

An unreadable existing checkpoint is different: Atom cannot establish whether it contains held positions. In 5.0.0, a read/decode failure aborts startup with ATOM-PROTECTION-STATE before strategy ticks, including an unreadable or incomplete differential frame chain, rather than warning and trading from a clean state. For example, a truncated JSON file cannot reset the saved tick counter and submit the initial entry again. Repair or recover the artifact; omit resume_from only when deliberately starting a fresh run. An absent backend still starts fresh, and an empty saved payload retains its compatibility behavior.

Continuation compares UTC instants, not clock labels. A checkpoint at 13:00+05:30 has processed 07:30 UTC: replaying 07:30 is skipped, while 08:30 is new. Naive timestamps use the existing UTC convention; they are not exchange-local times. Use aware input clocks whenever possible. The order's recorded fill time still retains the supplied bar timestamp.

The financial-date cursor is separate from the last observed bar. With no held or queued exposure, a safely skipped January 2 gap does not consume its funding day: after a January 4 observation, continuous and resumed runs charge January 1, 2 and 3. Older checkpoints ending at a gap without this cursor refuse; use an earlier checkpoint or replay the source. A committed rejection callback may fail after accrual but before decision history advances, so the financial cursor can lead that history. Resumed input cannot precede the completed financial date.

Async protection latches the flatten guard immediately after a submit failure, before attempting another owner's exit. Later owners are still attempted in that pass, but their callbacks cannot open risk. Accepted/uncertain exit IDs remain reserved until the host reconciles them; a guard is not proof of a fill. Observation and retry clocks compare UTC instants, including mixed zoned and unzoned inputs. A direct caller of the low-level stop helper must provide the same host-owned guard; only the engine integration owns strategy admission.

New checkpoints retain the risk halt, reason and outstanding flatten request. Resuming does not reset a halt or authorize saved increases: queued and newly requested increases remain rejected, while owned reductions can execute. A saved flatten request still submits its owned unwind when the resumed strategy has no risk rules configured: removing the current supervisor does not cancel an existing risk decision. It submits on the next processed observation and, in replay, fills no earlier than the following bar's open. For example, one share bought at 100 and then flattened at 110 leaves cash 1,010 from initial 1,000 before fees. Submission alone is not an authoritative live fill. In local replay the flatten request stays active until the unwind has actually filled and the book is flat. New-risk orders are rejected before queuing, and previously queued increases are rejected even if their instrument has no quote on the current bar. Owned reductions still use next-open fills; duplicate reductions cannot reverse the position. A missing quote keeps the existing unwind pending rather than submitting another copy. Once flat, a non-halted strategy may make new decisions again; flatten is not a permanent halt. An existing take-profit limit or stop cannot postpone the replay unwind: Atom cancels that conditional reduction and requests a market exit instead. New conditional reductions during active flatten refuse rather than parking another limit. A saved sell limit at 200 for a share bought at 100 therefore does not wait for 200 when risk requests flatten; the market unwind fills at the next available open, for example 110. Pending grouped closes reserve every leg, so the same unwind cannot also submit a separate exit for the second leg. A newly triggered supervisor halt does not abandon an already-established flatten. An essential rejection callback failure still aborts the run, but the final checkpoint keeps the outstanding flatten and only active orders. Fix the callback before resuming; a rejected order is not restored as pending work. An already-submitted saved exit follows the ordinary pending-order recovery rules; this does not create an additional broker or OMS recovery mechanism. A malformed saved risk record refuses before state installation even for a flat book. Start an intentional fresh run only after reconciling its exposure; do not edit a checkpoint to bypass a halt. Older snapshots cannot reconstruct a halt they never recorded, and historical diagnostic breach events are not restored. Flat composite legs carrying realized P&L remain in the snapshot, so a closed group with profit 10 still reports 10 after full or differential continuation. If a saved halt or flatten request is active, any other checkpoint decoding or validation failure also refuses recovery, even when holdings are flat. A damaged bar cursor is not permission to discard the saved risk decision and trade fresh.

Host recovery after restart

An external host can call recover_fixed_protection(lifecycle, holdings, spec) from atom.engine.host_recovery to rebuild missing fixed-percent stops and targets. Pass the lifecycle used by the host's stop registry and its confirmed inventory, one ConfirmedHolding per (symbol, strategy_id, group_id). Positive quantity means long, negative means short, and zero confirms flat. Never substitute the symbol's net position for separate owners. Both tags default to ""; they must be strings, and the symbol must be a nonempty string. Quantities must be finite real numbers, excluding booleans. An open holding requires a finite positive average entry price/VWAP confirmed by the host.

Use the compiled strategy's PROTECTIVE_EXIT_SPEC. This version-1 mapping has only version, stop_pct, target_pct and non_rehydratable keys. Percentages are fractions: .05 means 5%, not 0.05%. Omitted or None percentages mean no level of that kind; supplied values must be finite positive numbers, excluding booleans and strings. non_rehydratable defaults to an empty list of strings. For a strategy with a 5% stop and 10% target:

from atom.engine.host_recovery import ConfirmedHolding, recover_fixed_protection
from atom.engine.order_lifecycle import ProtectionLifecycle

lifecycle = ProtectionLifecycle()  # Use the host registry's lifecycle in an integration.
holdings = [
    ConfirmedHolding("AAA", 2, 100, strategy_id="A"),
    ConfirmedHolding("AAA", -2, 200, strategy_id="B"),
]
spec = {"version": 1, "stop_pct": .05, "target_pct": .10, "non_rehydratable": []}
recovered = recover_fixed_protection(lifecycle, holdings, spec)
assert len(recovered) == 2
assert recover_fixed_protection(lifecycle, holdings, spec) == []

A's stop is 100 - 100*.05 = 95 and target is 100 + 100*.10 = 110. B's short stop is 200 + 200*.05 = 210 and target is 200 - 200*.10 = 180. The returned owner keys follow input order and include only owners whose missing levels were set by this call. An identical second call leaves the checkpoint unchanged. Existing levels and established entry prices stay unchanged: a late addition to A updates its quantity, keeps stop 95 and entry 100, and uses 100 for a missing target. A side flip retires the old owner and uses the new side's confirmed entry; zero retires the owner and returns no recovered key. Owners omitted from the inventory are untouched; omission does not mean confirmed flat.

spec=None, or a valid spec with no percentages and an empty non_rehydratable, returns [] without reconciling inventory. Otherwise all holdings are validated, including duplicate owner rejection, before a complete dry-run validates every derived level. Any refusal raises ProtectionStateError from atom.engine.order_lifecycle with ATOM-PROTECTION-STATE, leaving the real lifecycle unchanged. Correct invalid inventory/spec data before retrying; a percentage that produces a nonpositive or nonfinite level is also refused.

Trailing anchors, ATR/points snapshots, timers and stop_at/target_at expressions need persisted entry-time state. If any supplied holding is open and the compiled spec lists any non_rehydratable kind, recovery refuses the whole call with retained protection requires persisted entry-time state. Restore that state through the host's recovery procedure; do not remove the markers to force recovery. With no open supplied holding, these markers are ignored.

The host must serialize recovery with fills and other lifecycle changes. It owns confirmed inventory, pending exit IDs and durable recovery; this helper submits no orders and is not complete checkpoint continuation or proof of production-host adoption. rehydrate_protective_exits stays removed. This differently named, owner-keyed replacement has no alias under any removed restore name. Hosts moving a production adapter from Atom 4.x also read Engine host migration.

Persist a local backtest

For local backtest checkpoints, LocalJSON(path) supports full save/restore on the supported wheel platforms, including Windows. Reads are bounded to 64 MiB and require a regular, nonsymlink file. Differential JSON sidecars additionally require directory-relative file operations; unsupported platforms fail with a message to use SQLite(path) instead. SQLite supports both modes without those POSIX directory requirements. No checkpoint format migration is needed.

For example, an Engine configured with persistence=LocalJSON("run.json") writes its normal full checkpoint; pass that backend as resume_from on the next run. For checkpoint_mode="diff" on Windows, use a SQLite backend from the start. Keep checkpoint directories trusted and private; these local stores are not a multi-writer service or proof of production recovery. Earlier Windows full JSON loads could fail with a missing O_DIRECTORY attribute despite successful saves.

An unknown pending-fill failure, incomplete calendar accrual or essential on_end failure does not overwrite the last good checkpoint with its partial state. If none was previously written, no final checkpoint is created. on_end still runs once. Known simulator failures after a committed rejection keep their existing recoverable checkpoint behavior, including a pending risk flatten; not every exception implies corrupt accounting. This does not reconcile external side effects.

Live protection requires an adopting host

Protected entry requires an owned stop registry before any buy or sell. The async engine forwards the inner host's synchronous intent-admission method; that Boolean acknowledgment is not a confirmed fill. Before consuming ticks it requires the versioned awaited protection interface. An ordinary buy/sell adapter is not enough to qualify it.

Stop/target/trail and time-stop obligations run before decisions, including while an MTF decision failure is latched. Tick dispatch is serialized. An exit reservation is durably acknowledged before submission. Uncertain retries use the same ID; an accepted or partially filled request is not submitted again. Every triggered owner is reserved in that same durable snapshot before the first submission, so one lost acknowledgment cannot erase another owner's exit. If one async submit raises, the remaining owners are still attempted in that pass. The driver then raises ATOM-LIVE-PROTECTION, retaining every durable ID for host reconciliation; it does not assume the raising call was rejected. Each obligation permits at most three submissions, at most one per observation of its instrument. Replaying that timestamp can reconcile receipts but cannot submit again. Cancellation changes the ID, not the remaining retry budget. Exhaustion raises ATOM-LIVE-PROTECTION, retains unresolved exposure and blocks new exposure; the host must reconcile it rather than reset the budget. After a definitive cancellation, only confirmed remaining inventory can be retried under a new ID. For a ten-unit position, four confirmed exits followed by cancellation leave six to request, not another ten. The host restores both the registry and the pending state before restart, and reconciles real fills before the next tick. Replaying one observation does not decrement a time stop or advance a trailing anchor twice. Driver timeouts retain unresolved IDs. Risk flatten first reconciles completed protection, then closes only remaining inventory. Unresolved protection still blocks a competing flatten submission. The pending snapshot retains ID, quantity, attempts and last-attempt timestamp; incomplete old reservations refuse instead of inventing a fresh retry budget. describe lists owned_protection_v1 for compiled stops or level exits.

A synchronous external stop adapter must declare protection_sync_version=1; unqualified callables and coroutine functions refuse before they are called. Every triggered owner gets at most one attempt per observation, including combined price/time stops. Other breached owners are attempted before unresolved outcomes refuse and latch flatten; no new callback or competing unwind is authorized. Sync flatten blocks strategy orders, cancellation, modification and option intents. Both flatten paths attempt every held owner before raising a submission failure. Uncertain submissions remain reserved; a later pass cannot resend an accepted or unknown order merely because another owner failed. Strategy client methods are guarded by default, including bracket/OCO and unknown host commands. Cached nested clients and handles returned by unknown commands stay guarded; .client does not expose raw transport and no .engine escape is exposed. Known acknowledgments remain data. This is an integration guard, not a sandbox against trusted Python reflection. Known read contracts remain available, including quotes, balances, tracker state and the closed-trade ledger used by traded_today() and loss-streak checks. Completion needs flat inventory and known-empty host queues; unknown state refuses. Normal sizing and protected-entry hooks remain available outside flatten. See sync refusal and recovery. Mock persistence and receipts verify shared behavior only; production storage, Express adoption, broker orders and OMS reconciliation remain external work. Missing closes cannot authorize execution or erase stop observations; see missing observations for continuation/refusal rules.

Synchronous flatten failures

Synchronous risk flatten shares the stop pass's per-bar owner reservation. If ten owned units hit stop 95 at low 90 while flatten is requested, only one SELL 10 may be submitted. Other owners, including anonymous same-symbol groups, retain their own reservations. Only an explicit False response, confirming no acceptance, permits a later retry, even while halted, never twice at one timestamp. Three such refused passes stop with ATOM-LIVE-FLATTEN and retain the request. Any submit exception (including a lost connection), terminal status, awaitable response or accepted exit without confirmed closure stops immediately for host reconciliation, not another submission. False proves nonacceptance; a lost acknowledgment does not, so giving both the same retry policy would be unsafe. A partial confirmation cannot authorize reselling the whole position; one owner's failure cannot authorize repeating another's accepted exit. Only a confirmed flat book with no known pending orders clears the request. While the request is set, strategy submissions, including cancel, modify, option intents and direct submit_protection_exit() calls, are blocked in both sync and async strategy clients. Only host reconciliation may change those outstanding orders during flatten. A flat position table alone cannot cancel a known queued entry. An ordinary halt without flatten still permits cancellation, not a strategy modification that could increase exposure; the adopting host owns safe adjustments. Use the awaited host protocol for uncertain broker outcomes and delayed fills; this bounded synchronous path is not an OMS.

Standalone sync stop exits follow the same ownership discipline. An accepted SELL 10 without confirmed closure or an exact host-owned pending reservation stops with ATOM-LIVE-PROTECTION and retains the obligation. It is not resent on later bars, even if price still breaches. Rejection also refuses explicitly rather than silently dropping protection after price recovers. An exact pending reservation blocks competing reductions until the host reconciles actual fills. Zero closeable quantity is not proof of closure: without that exact reservation or confirmed flat inventory, the run refuses before any strategy callback. Use the host's qualified recovery path before restarting an unresolved owner.

Suppose three owned positions on A breach together: long 10, another long 4 and short 3. The synchronous stop pass attempts SELL 10, SELL 4 and BUY 3 once each, even if the first response is uncertain. A simultaneous time stop cannot submit the same owner twice. Untriggered owners are not sent. If fills/reservations remain unconfirmed, obligations survive. Atom's Engine immediately latches flatten at the first failed owner, before another owner's exit can invoke a fill notification. Remaining protective exits still proceed; that notification may observe a real fill but cannot submit, cancel, modify or open an option intent through the strategy client. Ordinary strategy ticks do not resume. Already observed fills are real; this is not an atomic rollback. For example, if SELL 10 is unresolved and SELL 4 fills at 100, the four units close, but its callback cannot buy a new unit while the ten-unit obligation remains. From cash 600, cash stays 1,000, not 900 after an unintended re-entry. If an owner's inventory lookup fails before submission, the same guard latches and the other independently triggered exits still proceed. That owner's quantity and protection remain unresolved; a failed lookup is not evidence of flatness.

External hosts calling the shared stop helper keep the existing call form; there is no public on_failure callback. The immediate latch above belongs to Atom's Engine. A synchronous host must independently block strategy mutations from fill callbacks during unresolved protection; catching the final error is too late. Use only a host qualified for that isolation or the awaited recovery protocol. This compatibility repair does not establish production-host or broker acceptance.

Risk unwinds retain the held product terms in both sync and async paths. A EUR/USD position closes as FX with EUR base and USD quote currency, not default equity. Directional adapters must accept those terms; unresolved non-equity identity refuses rather than guessing an order from the symbol string.

Live clients without an Atom stop registry refuse checkpoints carrying held positions, protection, retained risk decisions or unrestorable simulated order state with ATOM-PROTECTION-STATE, before strategy ticks or state installation. For example, a checkpoint with cash 900, one held unit and a queued buy cannot warn and restart at cash 1,000 to submit the initial entry again. Recover with a host that can restore and reconcile the complete book and its obligations, or deliberately start fresh only after reconciling exposure externally. Flat checkpoints without those obligations retain ordinary restore behavior. Simulator price protection and pending-exit recovery use the rules above.

Resume An Async Host Without Duplicating An Exit

Switching from a synchronous host to an async host does not resolve an outstanding exit. Suppose an owner holds ten units and its stop submits SELL 10. If the host accepts the order but cannot confirm closure or an exact owned reservation, Atom retains the ten-unit obligation and its activation error. A lost acknowledgment is also uncertain: the broker may already have accepted the order. Restoring that owner into an async host now refuses before a new protection ID, persistence call or strategy decision. Earlier async activation could submit a second SELL 10.

An explicit zero-fill rejection is different from uncertain acceptance: it confirms that attempt did not fill. The synchronous stop path nevertheless retains its failure and requires host reconciliation; changing runtimes does not authorize an automatic retry. For an already-admitted async exit, an accepted or partial receipt keeps its existing ID without another submission. An unknown outcome may retry only that same durable ID through the qualified idempotent host protocol; a confirmed rejection or cancellation can authorize a new ID for the confirmed remainder, subject to the existing observation and three-attempt limits.

Restore the async host's pending exit IDs together with its owned registry and qualified exit protocol. For example, ten units still held plus one saved pending exit cannot activate on a client that omitted its registry: Atom refuses before consuming ticks or running an entry callback, keeps the saved ID and inventory, and latches flatten when a breach state is supplied. It does not send a new BUY or a second exit. An empty pending mapping on an otherwise unprotected client remains a no-op; malformed saved exit state refuses. Do not clear the IDs or the activation error to force startup. StrategyEngine/OMS must establish the actual order outcome and reconcile the owner before reactivation. These shared guards do not supply production recovery, broker routing or a reconciliation service.

TradeScript remains //@version=1.