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)| Status | Meaning |
|---|---|
| draft | Created but not yet released for execution |
| pending | Submitted, waiting to be sent to Lynx |
| sent_to_lynx | Handed to the adapter, order is live |
| partially_executed | Partial fill |
| executed | Fully executed (open position, premium collected) |
| rejected | Rejected by broker |
| discarded | Manually discarded OR auto-cancelled at broker after 3 days w/o fill |
| expired | End state: short option expired worthless (win) |
| assigned | End state: short option got exercised (shares moved) |
Note:expiredandassignedonly apply to executed proposals whose position dissolved at expiry. A non-executed proposal cancelled at the broker after 3 days becomesdiscarded(notexpired). This keeps the wheel statistics clean:executed -> expired= premium kept,executed -> assigned= stock change via wheel.
Actions
- Submit (draft → pending) – computes
expires_atusing 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_CLOSEdraft 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_CLOSEon 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:
- 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.
- 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.
- 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:
- 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.
- Expiry reconcile: for
executedproposals 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}/coverageResponse:
{
"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.