Proposals & lifecycle

A proposal is an order suggestion: a chosen strike + limit, not yet sent to Lynx. You stay in control from draft to execution.

Status values

draft  ->  pending  ->  sent_to_lynx  ->  partially_executed  ->  executed
                                                                    +-> expired   (option expires worthless)
                                                                    +-> assigned  (option gets exercised)
                  +-> rejected
                  +-> discarded     (3 days no fill OR manually discarded)
StatusMeaning
draftCreated but not yet released for execution
pendingSubmitted, waiting to be sent to Lynx
sent_to_lynxHanded to the adapter, order is live
partially_executedPartial fill
executedFully executed (open position, premium collected)
rejectedRejected by broker
discardedManually discarded OR auto-cancelled at broker after 3 days w/o fill
expiredEnd state: short option expired worthless (win)
assignedEnd state: short option got exercised (shares moved)
Note: expired and assigned only apply to executed proposals whose position dissolved at expiry. A non-executed proposal cancelled at the broker after 3 days becomes discarded (not expired). This keeps the wheel statistics clean: executed -> expired = premium kept, executed -> assigned = stock change via wheel.

Actions

  • Submit (draft → pending) – computes expires_at using the same

rule as the desktop app:

  • Submit on a trading day before 14:00 ET -> that day counts as day 1.
  • Submit after 14:00 ET or on a weekend/holiday -> the next trading

day counts as day 1.

  • Cancel runs on the 3rd trading day after 16:00 ET (NYSE close).
  • The auto-cancel cleanup only runs while the NYSE is closed and

only when the user gateway is authenticated; if the gateway is down, the proposal stays open and the next tick will re-check.

  • Refresh-mid – fetches the current mid price and checks drift against

the original proposed mid (see "Smart-limit & mid-drift").

  • Send (pending → sent_to_lynx) – performs a final drift gate and hands

the order to the Lynx adapter.

  • Discard – moves the proposal to discarded (any time as long as not

executed).

Stage "Open" (sent_to_lynx | partially_executed)

  • Cancel – cancels the live order at the broker and moves the proposal

to discarded. On partial fills, the already executed contracts remain as a position; only the unfilled remainder is pulled. Endpoint: POST /api/proposals/{id}/cancel.

  • Replace – modifies the limit of the live order at the broker

(replace-order). The status stays sent_to_lynx/partially_executed; only limit_usd is overwritten. Endpoint: POST /api/proposals/{id}/replace.

#### Price columns "Limit" and "Bid"

The Open tab has two primary price columns, each with a small grey sub-info block below:

  • Limit – your limit price for the order.
  • Ask (grey) – current sell side at the broker (only when both sides > 0).
  • Spread (grey) – Ask − Bid. Wide spreads = low liquidity.
  • Bid – current buy side at the broker. That is the price at which a

sell-limit would fill immediately, which makes Bid the most relevant value for fill probability of an open STO order. To the right of the Bid value there is a source marker:

  • L – Live Bid (real buy side, best source, ~30 s cache).
  • S (yellow) – Snapshot (Refresh): no live bid; the last

scheduler-stored mid is shown as an approximation (~30 min old).

  • S0 (orange) – Snapshot (Initial): only the mid captured at

"Send to broker" time is available. Can be hours to days old.

  • - – no value. Check the gateway connection.
  • Mid (grey) – (Bid + Ask) / 2, fair midpoint of the spread.
  • Last (grey) – last actually traded price (activity indicator).

A "Prices explained" button in the table header opens an info modal covering every marker and field (touch-friendly, does not rely on hover).

Stage "Active" (executed)

  • Close – creates a BUY_TO_CLOSE draft on the same option. The draft

then runs through the normal pipeline (Proposals → To send → Open → ...). Endpoint: POST /api/proposals/{id}/close.

  • Roll – creates two drafts: a BUY_TO_CLOSE on the existing option

and a SELL_TO_OPEN on a new strike/expiry. Both stay as drafts and must each be confirmed and sent. Endpoint: POST /api/proposals/{id}/roll (body: close_limit_usd, new_strike, new_expiry, open_limit_usd, optional new_quantity).

Plan gating: Cancel/Replace/Close/Roll, like all write actions, are unlocked in the three paid tiers (Wheel Starter, Master, Premium).

Wheel Ledger (per-position receipt)

Next to every symbol in the Proposals, To send, Open, Active and Closed tables you'll see a small receipt icon. Clicking it opens the Wheel Ledger as a slide-in panel on the right — the full cycle overview for that position:

  • Header: symbol · bucket badge (PUT-L1 / PUT-L2 / CC / NAKED_CALL)

· cycle status (open / closed / assigned / expired).

  • Balance: gross premium − buyback − commission = net, plus

hold days.

  • Cycle timeline: all STO/BTC legs of the roll chain in

chronological order, color-coded (STO green, BTC red).

  • Month breakdown: where the net premium is booked in the

Performance tab (split by CALL / PUT).

  • Underlying: cycle window + ex-dividend markers in that window.
  • Journal note: the free-text note attached via the sticky-note

icon on the trade.

The ledger works from any leg of a roll chain — it always shows the full history starting at the root. See the dedicated help topic “Wheel Ledger (position receipt)” for the full spec.

Auto-sync with IBKR

A background scheduler syncs with the IBKR gateway every 5 minutes:

  1. Order adoption: Live orders submitted outside the web app (e.g. via

the Desktop app or Lynx client) are imported as proposals in status sent_to_lynx/partially_executed, so every open option order shows up in the Open tab automatically.

  1. Position sync + fill match: Positions are refreshed and open

proposals are matched against them (symbol/type/strike/expiry). When IBKR reports the position, the proposal flips to executed and appears in the Active tab.

  1. Position adoption: Active SHORT options (short_put/short_call)

that have no proposal yet - e.g. trades placed in the Desktop app or directly at the broker - are auto-created as executed proposals and show up in the Active tab too. status_reason carries adopted_position@<fill>.

The Open and Active tabs display a status line at the top ("Last update: Xs ago - Next: in Ys") plus a Now button for a manual trigger.

Endpoints (internal):

  • GET /api/proposals/sync/status - scheduler status for the UI.
  • POST /api/proposals/sync/run-now - manual trigger (adoption + sync +

fill match) for the current user.

Auto lifecycle (3-day cancel + expiry reconcile)

Two scheduler jobs keep the status clean:

  1. 3-day cancel of unfilled orders: if a proposal hasn't filled within

3 NYSE trading days (cut-off 19:00 UTC), the system calls Lynx cancel and sets the status to discarded (reason auto_3day_no_fill+broker_cancel). Nothing dangles at the broker.

  1. Expiry reconcile: for executed proposals whose expiry was

reached, the position-reconcile compares stock balances before/after the IBKR sync:

  • short put + balance increased by qty * 100 -> assigned
  • short call + balance decreased by qty * 100 -> assigned
  • else short option vanished -> expired (worthless)

US holidays for 2026 are hard-coded; the system skips them correctly.

CC lock protection

When you create a covered-call proposal the backend checks how many shares are already locked by other open CCs:

GET /api/proposals/locks/{symbol}

Returns e. g. {"symbol": "AAPL", "shares_locked": 200}. The engine prevents you from writing more calls than you own shares.

Naked-call protection on send

In addition, when you click Send in the confirm dialog, the backend explicitly verifies share coverage:

GET /api/proposals/{id}/coverage

Response:

{
  "symbol": "AAPL",
  "shares_owned": 300,
  "shares_locked_other": 100,
  "shares_required": 200,
  "shares_free": 200,
  "covered": true
}
  • shares_owned = long_stock quantity in the account.
  • shares_locked_other = shares already locked by other open CCs

(pending/sent/partial).

  • shares_required = quantity * 100.
  • shares_free = shares_owned - shares_locked_other.
  • covered = shares_free >= shares_required.

If covered=false both the UI (Send button disabled) and the backend (POST /send returns 409 CC_NOT_COVERED) refuse the order, so a naked call can never go out.

Plan gating

Write actions (create / submit / refresh-mid / send / discard) are unlocked in all three paid tiers (Wheel Starter, Master, Premium). Reads (list, detail, locks) are free.

Lynx adapter

A proposal is sent to the adapter using client_order_id = proposal-{id} (production: Lynx gateway; tests: in-memory adapter; default build: dummy adapter). The order id is written back to ibkr_order_id.