Skip to content

Engine host migration from Atom 4.x to 5.x

This note is for a downstream host such as strategy-engine-v2 that already runs Atom. Strategy authors should keep using protection and recovery and execution. Atom 5 does not restore 4.x compatibility shims; the host adapter and the Atom pin move together.

Worked numbers below use two owners on AAA: A long 2 at 100 and B short 2 at 200, with a 5% stop and 10% target. 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.

Replace rehydrate_protective_exits

atom.engine.stop_registry.rehydrate_protective_exits is removed. There is no alias. The host passes confirmed inventory into atom.engine.host_recovery.recover_fixed_protection and must not infer owners from client internals. See atom/engine/host_recovery.py.

Removed 4.x call (do not restore this import):

from atom.engine.stop_registry import rehydrate_protective_exits
rehydrate_protective_exits(client, spec)

5.x replacement. Use the host registry's lifecycle. Pass every confirmed (symbol, strategy_id, group_id) holding; a symbol net of zero is not a substitute. On ProtectionStateError, refuse activation instead of starting the strategy with partial protection:

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

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": []}
try:
    recovered = recover_fixed_protection(lifecycle, holdings, spec)
except ProtectionStateError:
    raise  # Refuse activation; do not start with partial or inferred protection.

A second identical call returns [] and leaves levels 95/110 and 210/180 unchanged. Trailing anchors, ATR/points snapshots, timers and stop_at/target_at need persisted entry-time state; an open holding with a non-empty non_rehydratable list refuses the whole call.

Owner-keyed protection

Each (symbol, strategy_id, group_id) owns its quantity, entry price and levels. Direct register_stop / register_target require a confirmed owner: atom/engine/stop_registry.py calls resolve_owner(..., required=True), which raises ProtectionStateError with no confirmed open position for owner when that allocation is missing or flat (atom/engine/order_lifecycle.py). Reconcile the host's confirmed quantity and average entry through ProtectionLifecycle.reconcile(..., entry_price=) before those registrations. A and B on the same symbol stay independent even when the net position is zero. Do not register from a pending order or from symbol net inventory.

Live protection contract

Synchronous hosts that drive drive_stop_registry must set protection_sync_version = 1 on the client (atom/engine/stop_dispatch.py). The declaration alone is not proof of a callback-isolation guard and is not permission to discard awaitables. After a sync protective submit, Atom still requires confirmed closure or an exact owned reservation; otherwise it raises ATOM-LIVE-PROTECTION: protective exit lacks confirmed closure or an owned reservation.

Awaited AsyncLiveEngine hosts declare protection_exit_version = 1 and implement persist_protection_state, submit_protection_exit and protection_exit_status. Protected entry additionally needs protection_entry_version = 1 and synchronous _submit_protected_entry(symbol, delta, protection) -> bool. The names, payload rules and restore order are in docs/internal/live-protection-contract.md and the live protection examples.

TradeScript 5.x admission

TradeScript remains //@version=1. Condition strategies need both entry: and exit: headers; omitting either is TS-SECTION-REQUIRED (tradescript/validate/checks.py). Protection-only scripts may leave exit: empty. hold: portfolios do not use those trading blocks.

A declared base timeframe is admitted only when the bar carries matching bar.timeframe metadata (atom/library/readiness.py). Missing metadata is TS-TF-COMPATIBILITY; Atom does not resample.

Metadata discovery and host admission are different. tradescript/strategy_config.py reads recognised module-level names (symbol, symbols, universe, SCHEDULE, SIZING_VERSION, PROTECTIVE_EXIT_SPEC, OPTION_INTENT_VERSION) only as literals via ast.literal_eval. Non-literal assignments are ignored there (non-literal <name> ignored) and do not fail admission. That path does not inspect SIZE_SPEC.

The recorded Engine 26bbf17 rejection is runner/lifecycle_manager.py::_validate_assignment, which raises Top-level assignments must use literal values; move dynamic initialization into on_start for a nonliteral unless it is one of the frozen compiler forms that validator already allowlists (sizing = explicit_budget(...) / vol_target(SIZE_SPEC, ...) / units_book(), constraints = [...], and a fixed __builtins__ = dict(...) keyword set). Atom's compiler (tradescript/lower/emit_py.py) still emits those constructors, and it also emits readiness wrapping such as on_tick = _with_readiness(on_tick, READINESS_SPEC, ...) (same wrap for book / manage / shared_budget_signal). A host that requires every top-level assignment, or every metadata name, to stay literal rejects that output. Extend _validate_assignment to accept the current compiler forms instead of forcing metadata to stay literal.

Pin Atom by commit SHA

Pin Atom to an immutable 40-character commit SHA. Do not resolve a branch name such as main at deploy time. The Engine's 4.12 pin at 2b49c83 is the migration window: move the pin in the same change as the adapter that imports recover_fixed_protection.

tests/acceptance/test_engine_import_surface.py is the Atom-side contract for that host. A failing pair means change the Engine pin or adapter in the same release; do not loosen the test.

What a host upgrade must adopt

These Atom 5 contracts are intentional. They are the Engine 26bbf17 failure categories against 5.2.0, listed so a host can plan the work; they are not Atom defects and this package does not reintroduce 4.x behaviour.

  • Call recover_fixed_protection with host-confirmed holdings; do not import rehydrate_protective_exits.
  • Reconcile each owner's quantity and entry price before register_stop / register_target.
  • Declare protection_sync_version = 1 on synchronous stop hosts, and still confirm closure or own a reservation after a protective submit.
  • Adopt the awaited protection names above when the host is async.
  • Require entry: and exit: on stored condition strategies.
  • Supply bar.timeframe on admitted bars.
  • Adopt Engine _validate_assignment for compiler __builtins__, sizing constructors and readiness wrapping; do not require every top-level assignment to be a literal.
  • Pin Atom by SHA, never by a floating branch.