Raxx · internal docs

internal · gated

ADR 0144 — Wheel Structure Engine: own tables + user-driven invariant

Status: Proposed Date: 2026-07-20 UTC Deciders: software-architect, operator (Kristerpher) Scope: Raptor (backend_v2/), Antlers (frontend/raxx-next/); extends existing options strategy surface


Context

A Wheel options-income strategy (cash-secured put → assignment → covered call → repeat) requires a multi-phase lifecycle tracker that does not fit cleanly into any existing data model. The existing strategies table and lcc_strategy_config extension cover single-phase rule storage and long-dated CC income cycles respectively. The Wheel adds a CSP acquisition phase before shares are held and requires explicit state transitions between phases across potentially multiple legs and roll events.

A competing design newsletter delivered this capability as a "what to trade this week" recommendation feed. Raxx's strategic position prohibits that posture (see project_strategic_position, feedback_no_forward_looking_framing). The question this ADR answers is not whether to build a Wheel — it is how to scope and bound it so the architecture enforces the user-driven constraint at the data and API layer, not just the UI layer.

Three meaningful choices required decisions:

  1. Whether to bolt the Wheel onto existing tables (strategies, lcc_strategy_config) or give it its own table family.
  2. Whether the Wheel's CC phase should reuse lcc_strategy_config rows directly.
  3. Where the structuring math should live and what form it should take.

Decision

The Wheel Structure Engine is implemented as a new set of tables (wheel_positions, wheel_csp_legs, wheel_cc_legs, wheel_roll_ladder) within the existing Raptor (backend_v2/) Tier 2 Python service. No new standalone service is introduced. Structuring math is a pure-function service module (wheel_math.py). The CC phase owns its lifecycle data independently from lcc_strategy_config with a nullable FK (lcc_strategy_config_id) reserved for future integration.

The architectural invariant — the user supplies the underlying symbol; the engine structures around that input; no ticker is proposed to the user by the system — is enforced at three layers:

  1. API layer: POST /api/wheel/positions requires underlying_symbol from the client. No endpoint accepts a symbol-suggestion request or returns a ranked list of symbols.
  2. Data layer: No column in any wheel_* table stores a "suggested" or "screened" symbol. All symbol columns are user-supplied inputs or OCC option symbols derived from them.
  3. Flag layer: FLAG_WHEEL_STRUCTURE_ENGINE = false returns 404 on all Wheel routes (hide-don't-gray). A kill switch at the feature-flag level prevents any Wheel surface from being accidentally exposed.

The roll-candidates endpoint (/api/wheel/positions/{id}/roll-candidates) is gated on options-roll analytics (#3995) shipping. It presents candidate strikes from the live chain for a position the user already has open — it does not suggest new positions to open.


Language choice rationale

Service: Wheel Structure Engine

The Wheel Structure Engine is not a new standalone service. It is a feature addition to Raptor, the existing Tier 2 Python service (backend_v2/).

Language tier: Tier 2 — Python (existing service; no new service boundary)

Rationale for this classification: The Wheel is a user-facing structuring and lifecycle-tracking feature. Its computation profile (Decimal arithmetic, DB writes, options chain API calls) is identical to the existing LCC and IC features in Raptor. None of the Tier 1 criteria (C-1 through C-6) in docs/architecture/language-tier-policy.md apply: this is not an auth hot path, not latency-critical (p99 budget is the same as the rest of Raptor), and does not handle cryptographic key material. No Tier 1 promotion is anticipated.

API contract portability (Tier 2 only): The Wheel REST API uses standard JSON request/response shapes compatible with OpenAPI 3.1. A future Tier 1 port (if ever warranted) would require no redesign of the contract. Portability risk: none identified.


Consequences

Positive

Negative / risks

Neutral


Alternatives considered

Alternative A: Extend strategies + lcc_strategy_config for the Wheel

Model the Wheel as a special strategy.type = 'wheel' row, with a new wheel_phase column on lcc_strategy_config for the CSP phase.

Rejected because: lcc_strategy_config assumes shares are already held. The CSP phase — where no shares are held yet — has no representation in the LCC model. Forcing it in would add nullable columns with complex conditional logic and make the position_state_enum ambiguous (is NO_POSITION a pre-CSP state or a pre-LCC state?). Separate tables are cleaner and have no migration cost at this stage.

Alternative B: Reuse lcc_strategy_config for the CC phase only; new table for CSP

Model the CSP phase in a new wheel_csp_legs table, then on assignment create an lcc_strategy_config row and hand off CC lifecycle tracking to the LCC engine.

Rejected because: the LCC engine's position_state machine is tightly coupled to the LCC roll-alert and uncovered-shares-guard features. Injecting Wheel-assigned positions into that machine creates cross-feature coupling that makes both harder to reason about and test. The nullable lcc_strategy_config_id FK on wheel_cc_legs preserves optionality for a future integration without forcing it now.

Alternative C: Dedicated "Wheel service" (new Raptor sub-process or sidecar)

Spin the Wheel off as a separately deployable service.

Rejected because: no Tier 1 criterion is met. The Wheel's computation is Decimal arithmetic and DB writes — the same profile as every other Raptor route. A new service would add operational overhead (new dyno, new DB connection pool, new health checks, new CF Access policy) with no benefit. Raptor's existing blueprint pattern handles new route families without a service split.


Security / GDPR checklist


Revisit when