# Order failures and partial fills

> One multi-leg order per spread makes a half-filled spread structurally impossible; what the agent does when a submission is rejected or the account ends up unbalanced anyway.

![image](https://zmscvxdouuytwoutqtfa.supabase.co/storage/v1/object/public/docs-media/bfef459f-8ffe-4600-8fc5-c5f2d2973b38.jpg)
## One order, both legs

Every entry and every exit is a **single multi-leg (`mleg`) order** with both
legs inside it — never two separate orders, never a naked leg. Alpaca fills the
package against one net limit price, which is what makes the classic failure —
one leg fills, the other does not, leaving unhedged exposure — structurally
unavailable here. Alpaca's own documentation puts it as reducing "the chance of
partial fills that could distort the intended strategy". Options orders are
day-only, so an unfilled order simply expires at the close and the next cycle
re-evaluates from scratch.

## When a submission is rejected

`submit_mleg_order` wraps any API error as one `OrderSubmissionError` and **does
not retry** — a silent retry could double a position. The cycle catches it and
persists a `trades` row with `status = 'submission_failed'` and the broker's
message, emits an `order_failed` (or `exit_failed`) event, and prints the error.
The rejection is a first-class row in the decision log and shows in the
dashboard with the same weight as a fill.

Several checks stop an order before it is even built, each logging its reason
instead of sending: no fillable net-credit limit exists; being marketable would
concede more than half the measured credit; or the sized quantity came out at
zero.

## No silent stacking

Because the loop runs every few minutes, a resting order is committed risk the
position count cannot see. So if an entry order is already resting on the
account, **R10 rejects every new entry that cycle** — no stacking of unfilled
entries (this was a real day-one incident, since fixed). An order whose legs
cannot be read is treated as opening risk, not ignored. On the exit side, a
triggered close whose spread already has a working closing order is skipped, not
duplicated.

## If the account still ends up unbalanced

Each cycle pairs the account's open option legs back into spreads. Anything that
does not pair — a lone short leg (naked exposure), an unpaired long, a
non-option position — is surfaced as an **anomaly**: it is written to the log,
raised as a `position_anomaly` event, and **blocks all new entries until it is
resolved**. There is no automatic unwind. The agent's response to an unexpected
account state is to stop adding risk and make the problem visible, not to trade
its way out of it — clearing it is an operator action.
