Safety gates

Before a proposal reaches the Lynx adapter it passes through several protective layers. They prevent costly fat-finger errors and make sure only consistent orders go out.

1. Plan gate

Write endpoints (create / submit / send / discard a proposal, save presets, edit watchlists) are unlocked in all three paid tiers: Wheel Starter, Wheel Master and Wheel Premium. Legacy accounts in the locked free state get:

HTTP 403  { "error_code": "PLAN_REQUIRED", "data": { "required_plans": [...] } }

In the frontend the button is rendered semi-transparent and non-interactive by the <StrategyGate> wrapper.

2. State-machine gate

proposal_service.ALLOWED_TRANSITIONS describes the legal moves:

draft       ─► pending, discarded
pending     ─► sent_to_lynx, discarded, expired
sent_to_lynx ─► executed, partially_executed, rejected, expired
partially_executed ─► executed, rejected

Forbidden transitions raise IllegalProposalTransition → HTTP 409 with error_code: "ILLEGAL_TRANSITION".

3. Auto-cancel scheduler (3 trading days unfilled)

APScheduler invokes proposal_service.discard_unfilled_after_3d every 5 minutes (alias: expire_overdue). Proposals in sent_to_lynx or partially_executed whose expires_at has passed are cancelled at the broker and moved to discarded (status_reason='auto_3day_no_fill+...').

expires_at calculation (1:1 with the desktop app):

  • Submit on a trading day before 14:00 ET -> the submit day counts

as day 1.

  • Submit after 14:00 ET or on a weekend/holiday -> the next trading

day is day 1.

  • Cancel runs on the 3rd trading day after 16:00 ET (NYSE close).
  • expires_at uses zoneinfo("America/New_York") and is DST-safe.

Three gates protect against false cancels:

  1. Market-closed gate: cleanup only runs while the NYSE is closed

(otherwise skipped).

  1. Per-user gateway auth gate: if gateway_not_authenticated /

gateway_not_running, the user's candidates are skipped; the next tick re-tries. Prevents false-positive discards during gateway outages.

  1. Broker confirm: discard only if Lynx accepts the cancel OR the

order no longer appears in live orders. Otherwise the proposal stays open with status_reason='cleanup_failed:<reason>'.

4. CC lock gate

When you create a covered call, shares_locked_in_cc(symbol) checks how many shares are already locked by other open CCs (pending, sent_to_lynx, partially_executed). You cannot write more calls than the shares you own.

5. Mid-drift gate

Before send_to_lynx runs, the backend pulls the fresh mid and compares it to the original mid_initial_usd (threshold 2 % or 0.01 USD absolute). Drift > threshold → HTTP 409 with error_code: "MID_DRIFT_GATE". In the frontend the drift modal opens.

See "Smart-limit & mid-drift".

6. Risk-flag awareness

When you create a proposal from a candidate, the existing risk flags (earnings, ex-div, IV-extreme, low-liquidity) are stored in preflight_checks. The frontend renders them and the user acknowledges them via user_confirmed_warnings: true at submit.

7. User isolation

All reads / writes filter strictly by user_id. It is not possible to access another user's proposals, watchlists, or presets. Every HTTP route pulls the user from Depends(get_current_active_user).

Putting it together

        ┌───────────────────────────────────────────────────┐
Click → │ Plan-gate → State-machine → CC-lock → Mid-drift   │ → Lynx adapter
        └───────────────────────────────────────────────────┘
                          ↑               ↑
                      Auto-expire   User confirmation (modal)

Only when every gate passes does the order go out.