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, rejectedForbidden 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_atuseszoneinfo("America/New_York")and is DST-safe.
Three gates protect against false cancels:
- Market-closed gate: cleanup only runs while the NYSE is closed
(otherwise skipped).
- 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.
- 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.