ADR 018 — Canonical execution aggregate
- Status: Accepted-in-direction (2026-08-03) via the ARP review: the implemented OrderIntentService + domain_orders path stays the persisted truth path; the bots frozen model is promoted as wire/contract shape only, inside the Trading & Ledger context on the alphaswarm#212 outbox-unification track; no service extraction. Supersedes the Proposed (2026-06-19) E5/Wave-2 gating. See alphaswarm_internal
docs/architecture/agent-first-research-platform/02-context-map-and-ownership.md§6 (binding disposition table) and10-adrs.md. - Authors: Platform team
- Related: Enhancement Guide §6/E5, ADR 008, ADR 015; Hard Rule 1 (
Symbol/vt_symbol)
Context
Two order models coexist:
alphaswarm_bots | monolith alphaswarm | |
|---|---|---|
| Order | NewOrder / OrderRef (schemas/trading.py, msgspec.Struct(frozen=True)) | DomainOrder dataclass (core/domain/orders.py) + Pydantic OrderIntent (trading/order_intent.py) |
| State | Guarded OrderFSM (execution/lifecycle.py, _VALID_FORWARD, idempotent) | Unguarded apply_report() fold (trading/execution/order_state.py) |
| Fill | Fill (mandatory exec_id) | implicit |
| Portfolio | implicit PositionProjection / PnLProjection | core/domain/positions.py |
| Execution policy | ExecutionLayerSpec + ExecutionAlgorithm ABC | — |
The result: OrderIntent → ExecutionOrder → Fill is split across two stacks with
two grammars; there is no single canonical aggregate, no explicit Portfolio
aggregate (it is an emergent projection), and no ExecutionPolicy aggregate
(the closest artifact is a config object, ExecutionLayerSpec). This is the
memo's "single canonical aggregate" gap.
Decision
Adopt one canonical execution aggregate, promoting the stronger model and demoting the weaker fold.
ExecutionOrder(canonical). Promote thealphaswarm_botsfrozen, guarded, idempotent model (OrderFSM+msgspecstructs, post-Wave-1exec_ididempotency and injected clock) as the canonical venue-routable order. Replace the monolithtrading/execution/order_state.pyfold with the guardedOrderFSM.OrderIntentas pre-trade intent. Keep the monolith PydanticOrderIntentas the desire to trade (sizing mode + rationale) that a risk-approvedExecutionOrderis derived from — the LEANInsight → PortfolioTarget → Orderseparation.- Explicit
Portfolioaggregate. Wrap the existingPositionProjection/PnLProjectionin aPortfolioaggregate with anapply_fillcommand that carries design-by-contract invariants (Enhancement E4): positions reconcile to fills, no money created/destroyed, exposure within configured limits. - Explicit
ExecutionPolicyvalue object. WrapExecutionLayerSpec+ExecutionAlgorithmselection (TWAP/VWAP/POV/IS/Iceberg) as a first-class policy with a deterministicplan(intent, state) → ExecutionPlancontract. - One definition, two stacks. Extract the canonical types into a shared
boundary (
alphaswarm_coreor a dedicatedexecutionpackage) so both the bots runtime and the monolith import one definition; deprecate the monolith fold behind a shim.
Consequences
Positive
- One vocabulary from signal to fill; the guarded, idempotent FSM becomes the
only order state machine;
PortfolioandExecutionPolicybecome testable aggregates with invariants rather than emergent behavior. - Unblocks E4 (contracts on
Portfolio.apply_fill) and E1/E2 (a single FSM to model and dedup).
Negative / risks
- Touches both stacks; must proceed one axis at a time (unify types before unifying runtimes) and behind deprecation shims, per ADR 015.
msgspec(bots hot path) vs. Pydantic (monolith) interop at the boundary needs a thin adapter; keep the hot-path typemsgspecand expose a Pydantic view for API/persistence.
Explicitly rejected
- Maintaining two parallel order models indefinitely.
- Promoting the unguarded monolith fold (it lacks transition safety).
Rollout order
- Extract canonical
ExecutionOrder+OrderFSMto the shared boundary (types only). - Introduce
PortfolioandExecutionPolicyaggregates over existing projections/specs. - Replace the monolith fold with the guarded FSM behind a shim; deprecate.