# 05 · Order Execution — Paper & Live

This layer takes an **`ApprovedIntent`**, turns it into one or more **`Order`s**,
and submits them to either the **paper simulator** or a **live broker adapter**.
It owns idempotency, the order lifecycle, protective (bracket/OCO) orders, and
reconciliation.

```
ApprovedIntent ──► Order Router (idempotency, sizing check, bracket build)
                        │  Order(clientOrderId)
          mode=paper ┌──┴──┐ mode=live
                     ▼     ▼
             Paper Sim   Broker Adapter (Alpaca/IBKR/Binance/Kite/AngelOne)
                     │     │
                     └──┬──┘  Fill
                        ▼
              Ledger + Position + P&L  ──►  notify back (doc 04)
```

---

## 1. The Broker interface (build every venue to this)

One interface, many venues. The router only knows this contract; swapping
brokers or adding one never touches the router.

```ts
interface Broker {
  readonly id: string;              // "paper" | "alpaca" | "binance" | "kite" | ...
  readonly mode: "paper" | "live";

  submit(order: Order): Promise<BrokerAck>;         // must honour clientOrderId
  cancel(brokerOrderId: string): Promise<void>;
  getOrder(brokerOrderId: string): Promise<Order>;
  getPositions(): Promise<Position[]>;
  getAccount(): Promise<AccountSnapshot>;           // equity, buying power, cash
  /** stream or poll fills/order updates back into the OMS */
  onUpdate(cb: (u: OrderUpdate) => void): void;
}

type BrokerAck = { brokerOrderId: string; status: Order["status"] };
```

**Adapters translate**, they don't decide:
- our `type/side/tif` → the venue's enum,
- our `symbol` → the venue's symbol/instrument token,
- our `clientOrderId` → the venue's client-id field,
- the venue's fills/updates → our `OrderUpdate`.

---

## 2. Order Router (`oms/router.ts`)

```
route(approvedIntent):
  # 1. Idempotency — derive deterministic client order id (doc 01 §4)
  coid = hash(intent.signalId + intent.account + intent.side + round(intent.qty) + intent.barTs)
  if orders.exists(coid): return orders.get(coid)      // no double-fill, ever

  # 2. Final pre-trade risk check (doc 06) — caps can change between alert & fill
  guard = risk.check(intent)
  if not guard.ok: reject(intent, guard.reason); notify(); return

  # 3. Select venue
  broker = intent.mode=='paper' ? paperSim : brokers[account.brokerId]

  # 4. Build parent + protective children (bracket)
  parent = Order{...intent, clientOrderId:coid, status:'new'}
  persist(parent)                                       // UNIQUE(clientOrderId)
  ack = broker.submit(parent)
  parent.brokerOrderId = ack.brokerOrderId; parent.status = ack.status; persist(parent)

  if intent.bracket:
     buildBracket(parent, intent.bracket)               // TP + SL as OCO (see §4)
  audit('order.submitted', parent)
  return parent
```

**Lifecycle** (persisted, event-sourced): `new → submitted → partially_filled →
filled` (or `cancelled / rejected / expired`). Every `OrderUpdate` from the
broker advances the state machine and appends to the audit log. On restart,
rebuild state from the event log and **reconcile** against the broker (§6).

---

## 3. Paper-trading simulator (`oms/paper.ts`)

Ship this **first**. It implements `Broker` with `mode:"paper"` and fills
against the **same live market-data feed** the engine uses — so paper results
approximate live behaviour.

Fill model:
- **Market order:** fill at next tick / next bar open, apply **slippage**
  (`k·ATR` or a spread estimate) and **commission** from a fee model.
- **Limit order:** fill when the feed trades through `limitPrice`
  (`low ≤ limit ≤ high` on the bar), price = `limitPrice`.
- **Stop order:** activates when price crosses `stopPrice`, then behaves as
  market/limit.
- Maintain a virtual `AccountSnapshot` (cash, equity, buying power) and
  `Position[]`; update on every fill; emit `OrderUpdate` exactly like a real
  venue so downstream code is identical in paper and live.

> Because paper and live share the `Broker` interface and the engine is pure,
> **paper mode is the integration test for the entire pipeline.** A signal that
> paper-fills correctly needs no code change to go live — only a config flag and
> real keys.

---

## 4. Bracket / OCO / protective orders

A trade is entry **plus** protection. Build them together so a position is never
naked:

- **Bracket:** parent entry + a **take-profit limit** + a **stop-loss stop**,
  where TP and SL are **OCO** (One-Cancels-Other): filling one cancels the other.
- If the venue supports native brackets (Alpaca `order_class:"bracket"`, IBKR
  bracket orders, Kite GTT/CO) — use it. Otherwise **synthesise OCO** in the
  OMS: on TP fill, cancel SL (and vice-versa); on partial fills, resize the
  survivor.
- **Shipped — Binance spot OCO.** `BinanceBroker.placeBracket()` posts an
  `order/oco` (take-profit LIMIT + STOP-LIMIT) on the **opposite** side once the
  entry **fills** — spot is long-only, so a long exits via a SELL OCO; the
  stop-limit is nudged `BINANCE_OCO_STOP_LIMIT_BPS` (default 10 bps) past the
  trigger so it fills, and the list carries a `-oco` client id for idempotency. A
  still-resting LIMIT entry (status `NEW`) gets no OCO yet — you don't hold the
  asset — so it's deferred to a fill handler.
- **Trailing stop:** if requested, move the SL by `k·ATR` or a fixed offset as
  price advances (Supertrend/Chandelier exit are good trailing rules).

---

## 5. Live broker adapters (reference set)

Implement the `Broker` interface per venue. All six below are **shipped** in
[`server/broker.js`](../server/broker.js) — each an injectable-transport adapter
held to the shared [broker conformance suite](../server/test/broker-conformance.test.js):

| Adapter | Market | API notes |
|---------|--------|-----------|
| **Alpaca** ✅ (`AlpacaBroker`) | US equities/crypto | REST; `client_order_id`, native `bracket`/`oco`; **paper endpoint built in** (`paper-api.alpaca.markets`); 422-replay → idempotent success. |
| **Binance** ✅ (`BinanceBroker`) | Crypto spot | REST; `newClientOrderId`; **Spot Testnet** default; HMAC-SHA256 `timestamp/recvWindow` signing; -2010 dup → idempotent. |
| **Zerodha Kite** ✅ (`KiteBroker`) | India (NSE/BSE) | Kite Connect REST; `token api_key:access_token` auth; **form-encoded** body; `tag` carries the client-order-id reference; daily token refresh (no sandbox host). |
| **Angel One SmartAPI** ✅ (`AngelOneBroker`) | India | REST + JSON; JWT bearer + `X-PrivateKey`/client headers; `ordertag` reference; `symboltoken` per instrument; TOTP login upstream. |
| **Interactive Brokers** ✅ (`IBKRBroker`) | Global multi-asset | Client Portal Web API; session held by the local gateway; trades by `conid`; `cOID` idempotency; auto-confirms order-precaution prompts via `/iserver/reply`; `DU…` account → paper. |

> The `tag`/`ordertag` fields are idempotency **references** (Kite and Angel One
> don't dedupe on them), so the router's durable store is the real one-fill
> guarantee (§2). Alpaca `client_order_id`, Binance `newClientOrderId` and IBKR
> `cOID` are enforced venue-side as well.
>
> **Token bootstrap ships** in [`server/auth.js`](../server/auth.js): Kite's
> daily login-URL → `request_token` → `access_token` exchange (SHA-256
> checksum); Angel One's `loginByPassword` with a **TOTP computed on the fly**
> (RFC 6238) + JWT `refresh`; and the IBKR Client Portal Gateway session
> (`status` / `reauthenticate` / `tickle`). The adapters above consume the token
> these flows return.

Each adapter must:
1. **Map symbols** via an instrument master (cache & refresh daily).
2. **Sign & authenticate** correctly (API key/secret, TOTP, session tokens);
   read secrets from the vault, never the repo (doc 06).
3. **Round** qty/price to the venue's **lot size / tick size / step size** — a
   sub-tick price is an instant rejection.
4. **Handle partial fills, rejections, and rate limits** — surface them as
   `OrderUpdate`s; back off on 429s.
5. **Stream or poll** order/fill updates back into the OMS.

> Treat every venue quirk (min notional, market-hours, margin, settlement) as an
> adapter concern. The router and strategy layers stay venue-agnostic.

---

## 6. Reconciliation & recovery

Networks drop; processes restart. **The broker is the source of truth for
money.**

- On startup and on a schedule, call `getOrders`/`getPositions`/`getAccount` and
  **reconcile** against the local ledger; fix drift; alert on unexplained
  positions. *(Shipped: [`server/portfolio.js`](../server/portfolio.js) is a
  multi-account ledger — signed average-cost books per account, a cross-venue
  aggregate, and `reconcile(account, snapshot)` that diffs the ledger against a
  broker's positions and returns the drift. Fed by the router's `onFill` hook;
  read at `GET /portfolio` · `/accounts` · `/reconcile`.)*
- If `submit` times out, **do not blindly resubmit** — query by `clientOrderId`
  first (that's why every order carries one). Resubmit only if the venue has no
  record.
- Persist every state transition (event-sourced) so the OMS can be rebuilt
  deterministically after a crash.

> **Shipped.** [`server/store.js`](../server/store.js) implements this behind one
> synchronous `Store` interface with three drop-in backends: `MemoryStore`
> (ephemeral), `FileStore` (append-only JSONL event log, replayed on boot), and
> `PgStore` (an in-memory read cache with **write-behind** to Postgres via an
> injectable `pg` client; `init()` creates the schema, `hydrate()` replays on
> boot, `flush()` drains the queue on shutdown). Order idempotency — one
> `clientOrderId` ⇒ one order — holds **across a restart** in both durable
> backends. A live broker stays the source of truth for real positions.

---

## 7. Position & P&L ledger (`ledger/`)

- **Positions:** net qty, average entry, realised & unrealised P&L (mark to the
  live feed), fees.
- **Trades/blotter:** every fill, immutable.
- **Audit trail:** append-only `signal → alert → decision → order → fill` with
  ids linking each step — required for compliance and for debugging "why did it
  buy that?".
- Feeds `/positions`, `/orders` chat commands (doc 04) and the web dashboard.

---

## 8. "Claude task" prompts

> **Claude task — Router + paper simulator**
> Create `oms/router.ts` (idempotent `route()` per §2, deriving `clientOrderId`,
> calling the risk gate, building brackets) and `oms/paper.ts` implementing
> `Broker` with the fill model in §3 against the live feed, plus the position/
> P&L ledger. **Acceptance:** replaying the same `ApprovedIntent` twice creates
> **one** order; a market buy paper-fills with slippage+fees and updates equity;
> a bracket's TP fill cancels its SL.

> **Claude task — Alpaca adapter (first live-shaped venue)**
> Create `brokers/alpaca.ts` implementing `Broker` against Alpaca's **paper**
> endpoint: submit with `client_order_id`, native bracket orders, WS order
> updates → `OrderUpdate`, symbol/tick rounding, reconciliation on startup.
> **Acceptance:** a bracket order places on Alpaca paper, fills stream back into
> the ledger, and `getPositions` reconciles with local state. Keys come from the
> vault; `EXECUTION_MODE=live` is required and refused without an ack (doc 06).

> **Claude task — Additional adapters (as needed)**
> Implement `brokers/binance.ts` (Testnet), `brokers/kite.ts`, `brokers/ibkr.ts`,
> `brokers/angelone.ts` to the same interface with venue-correct signing, symbol
> maps, and lot/tick rounding. **Acceptance:** each passes the shared Broker
> conformance test-suite (submit/cancel/get/positions/updates) against its
> sandbox/testnet.

Guardrails that wrap all of this:
[`06-risk-and-safety.md`](06-risk-and-safety.md).
