Skip to content

fix(uta,cli,ibkr): live-testing rounds 6+7 — alpaca bracket legs + IBKR acceptance run#335

Merged
luokerenx4 merged 6 commits into
masterfrom
UTA-issue
Jun 13, 2026
Merged

fix(uta,cli,ibkr): live-testing rounds 6+7 — alpaca bracket legs + IBKR acceptance run#335
luokerenx4 merged 6 commits into
masterfrom
UTA-issue

Conversation

@luokerenx4

@luokerenx4 luokerenx4 commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Two catalog rounds on one branch (docs/uta-live-testing.md rounds 6–7).

Round 6 — Alpaca (market open):

  • CLI gateway strict flags: unknown flags were silently stripped — a typo'd --quantity staged a quantity-less LMT order that committed clean. Now z.strictObject → 400 naming the bad keys.
  • Stage-time per-orderType validation in stagePlaceOrder (LMT needs lmtPrice, qty XOR cashQty, "" = absent, etc.).
  • Bracket TP/SL leg tracking: PlaceOrderResult.legs → TradingGit tracks legs as first-class pending orders (the held SL leg never appears in venue listings — place-time is the only recovery point). OCO cancel verified live.
  • Sync-commit log rows attribute per-update symbols.

Round 7 — IBKR TWS paper (first acceptance run):

  • TPSL refusal gate: placeOrder(_tpsl) silently ignored protective legs (okx naked-entry species) — refuses loudly until native bracket lands (ANG-103).
  • getOpenOrders wired (bridge primitive existed, never exposed).
  • By-conId quotes: TWS 321 on bare conId → enrich via reqContractDetails + cache; reqMarketDataType(3) + delayed tick types (66-73) for paper entitlements.
  • Account-cache delta semantics: TWS pushes deltas between accountDownloadEnd markers; swap-on-end cache showed filled sells as still-held for minutes, dropped zero-qty (closed) updates, duplicated rows on churn → upsert-by-conId into live cache + pending buffer.
  • decodeContractProto: empty if (cp.secType !== undefined) body — dropped assignment, every protobuf portfolio row had secType ''.
  • dayTradesRemaining no longer fabricates 0 when TWS omits the tag.

Test plan

  • tsc --noEmit clean (root + uta-protocol + ibkr)
  • pnpm test passes (1920, +25 regression specs across both rounds)
  • Alpaca live: S2–S6 incl. bracket legs + OCO cancel; flat at baseline
  • IBKR live: S2/S4 (same-id modify)/S6 (PreSubmitted stop)/S8 (restart + TWS reconnect)/S9/S12; flat at baseline (700×100, AAPL×10)

Boundary touch

Trading: uta-protocol wire types (additive), TradingGit scans, AlpacaBroker, IbkrBroker + request-bridge + @traderalice/ibkr decoder. No migrations, no auth.

🤖 Generated with Claude Code

… order validation, bracket leg tracking

Round 6 of the live-testing catalog (docs/uta-live-testing.md), first
US-market-open run on the alpaca paper account. Three bugs:

1. CLI gateway silently stripped unknown flags: a typo'd --quantity /
   --limitPrice staged a quantity-less, price-less LMT order that
   committed clean. Gateway now parses with z.strictObject (unknown
   flags 400 loudly, naming the keys).

2. No stage-time required-field validation: the per-orderType rules
   lived only in tool description prose. UnifiedTradingAccount now
   refuses at stage time (LMT needs lmtPrice, STP needs auxPrice,
   totalQuantity XOR cashQty, cashQty MKT-only, TRAIL aux/percent
   exclusivity; "" treated as absent).

3. Bracket TP/SL legs untracked from birth: Alpaca brackets placed the
   legs on the venue, but only the parent id entered the ledger — order
   list, sync poller and cancel were all blind to them, and the held SL
   leg never appears in the venue's open-orders listing so no later
   diff could recover it. PlaceOrderResult.legs now carries child ids;
   TradingGit tracks legs as first-class pending orders (pending scan,
   known-ids set for the observation pass, sync resolution); compaction
   surfaces leg ids to the agent.

Also: sync-commit log rows attribute per-update symbols (was 'unknown').

Live-verified on alpaca paper: S2 lifecycle, S3 hanger, S4 modify
(replaceOrder mints a new id — tracked, no ghost), S5 bracket incl.
OCO cancel (one leg cancelled → venue kills both → synced), S6
standalone stop. Account left flat at baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openalice-demo Ready Ready Preview, Comment Jun 12, 2026 5:56pm

Request Review

…ing, conId quotes, account-cache deltas, secType decode

First live-testing-catalog acceptance run against IBKR TWS paper
(docs/uta-live-testing.md round 7). Five findings:

1. placeOrder silently ignored its tpsl param (underscore-prefixed) —
   the ledger would record protection TWS never received. Now refuses
   loudly until the native parent/child bracket lands (ANG-103).

2. getOpenOrders unwired despite bridge.requestOpenOrders existing —
   wired (this-clientId scope; manual TWS-UI order observation needs
   reqAllOpenOrders + permId identity, deferred to Linear).

3. By-conId getQuote hit TWS error 321 — reqMktData refuses to resolve
   a bare conId even though the wire field is sent. Enrich once via
   reqContractDetails, cached per conId. Also: reqMarketDataType(3)
   declared at init + delayed tick types (66-73) mapped in the snapshot
   collector (paper accounts have no live entitlement).

4. Account-cache delta semantics: TWS pushes position deltas BETWEEN
   accountDownloadEnd markers; the swap-on-end cache left a filled sell
   showing as still-held for minutes, dropped zero-quantity (closed)
   updates entirely, and duplicated rows on repeated updates. Now
   upsert-by-conId into both the live cache and the pending rebuild
   buffer; account values apply immediately too.

5. decodeContractProto had an empty `if (cp.secType !== undefined)`
   body — dropped assignment; every protobuf portfolio row reached the
   UTA layer with secType ''.

Also: dayTradesRemaining omitted when TWS doesn't report the tag
(was fabricating 0; -1 = unlimited passes through).

Live-verified on TWS paper: S2 lifecycle (fill @292.50 via execution
data), S4 modify (same-id semantics — inverse of Alpaca), S6 standalone
stop (PreSubmitted tracked, not mis-terminaled), S8 restart survival
incl. TWS reconnect, S9 partial close, S12 staging undo. Account left
flat at baseline. ANG-101 mechanism confirmed live (broker-layer
HKD+USD blind sum) — diagnosis recorded, FX fix out of scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@luokerenx4 luokerenx4 changed the title fix(uta,cli): round-6 alpaca live findings — strict flags, order validation, bracket leg tracking fix(uta,cli,ibkr): live-testing rounds 6+7 — alpaca bracket legs + IBKR acceptance run Jun 12, 2026
…r routing

Round-7 follow-up, chased through the market-data entitlement session:

1. _expandAliceIdIfNeeded overlay copied Contract NUMERIC defaults: the
   HTTP route wraps bodies with Object.assign(new Contract(), body), and
   while string defaults ('') were skipped, conId=0 was copied over the
   expanded conId — the broker received an all-empty contract and TWS
   rejected with 321. The by-conId quote path was dead in production
   while direct broker calls worked (why the probe and prod disagreed).
   Numeric Contract fields carry no signal at 0 / UNSET sentinels — skip
   them like string defaults.

2. request-bridge error routing swallowed every code >= 2000 as
   "informational". The farm-status band is 2100-2200; the blanket also
   ate the 10xxx REAL errors (10089 needs-subscription, 10197 competing
   live session), so pending market-data requests died as context-free
   timeouts. Agents now get the venue's actionable message.

Live-verified: by-conId AAPL quote through the production CLI now
surfaces "IBKR error 10089: ... requires additional subscription" with
the full venue text (entitlement is account-side: live-account US stock
market data subscription + paper sharing — sharing toggle alone gave
FX live ticks but not stocks).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ns, FX families

Design session outcome (with Ame): venue search returns two species —
LEAVES (tradeable contracts) and HUBS (directories: a bond issuer, an FX
currency family, an option chain behind a stock). The old identity layer
only modeled leaves; FX/BOND search rows either mis-resolved (symbol
nativeKey hardcoded STK → "EUR" the stock) or died as unaddressable
noise. Rather than excluding asset classes one by one, the hub/leaf
structure is now first-class and extensible:

- nativeKey grammar (IBKR): conId = canonical leaf for EVERYTHING
  tradeable (bonds included — uniqueness doctrine intact); issuer:eXXX =
  bond-issuer directory (addressable, loudly NOT tradeable); bare symbol
  stays an STK convenience. resolveNativeKey refuses directory keys with
  an actionable message instead of silently assuming STK.
- expandContract (protocol + IBroker optional + UTA + route + SDK + tool
  + CLI `contract expand`): issuer hub → individual bonds; underlying →
  option parameter grid (expirations × strikes via reqSecDefOptParams);
  underlying + expiry/right/strike window → concrete option contracts;
  secType=FUT → futures months. Every leaf out carries its own conId
  aliceId. No silent truncation (total + hint).
- Search: CASH family rows auto-expand inline into concrete pairs
  (".USD" pattern suffix narrows); BOND issuer rows surface as
  expandable hubs with the issuer name.
- Fixes en route: getContractDetails SMART/USD defaults poisoned conId
  and non-STK queries (EUR.USD@IDEALPRO → error 200) — defaults now
  STK-only; getQuote applied defaults BEFORE conId enrichment (same
  poison, production-only); bondContractDetails callback was unrouted
  (bond chains returned empty); compactContract drops meaningless
  strike '0'.

Live-verified on TWS paper: EUR.USD search → conId aliceId → first
production IBKR quote (bid/ask 1.1573@IDEALPRO); IBM issuer hub → 65
bonds, each conId-addressable; AAPL grid (26 expirations × 123 strikes)
→ 5 concrete Jul-2026 calls → order lifecycle smoke (deep LMT placed,
tracked with conId aliceId, cancelled clean). Option quotes refuse
loudly with the venue's entitlement message (10091). Catalog gains S13
(hub/leaf identity) + checklist items.

Known polish gaps (daylight work): bond leaves' coupon/maturity labels
come through empty (decoder field investigation); Quote.last fabricates
'0' when the venue reports none (type change touches all brokers).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…295), mixed-book valuation (#314/ANG-101), dead-connection gate (#294), symbol-first display (#208)

Triage pass over open UTA issues, live-verified on the running TWS paper
account (mixed HKD+USD book — exactly the configuration the reporters
describe):

#295 — updateAccountValue ignored the currency argument; multi-currency
tag families (CashBalance, NetLiquidationByCurrency, ExchangeRate, …)
arrive once per currency + a consolidated BASE line, and last-write-wins
left a random currency in the plain key. Values are now stored under
key:currency composites; BASE owns the plain key and a per-currency
line can never overwrite it.

#314 + ANG-101 — IbkrBroker.getAccount blind-summed per-position
unrealizedPnL and marketValue across currencies (live numbers: HKD
-4767.62 + USD +368.80 reported as USD -4398.82; netLiq inflated
$39.6k by counting 46.4k HKD as USD). TWS hands us per-currency
ExchangeRate tags — position math is now rate-converted; mixed books
prefer TWS's own consolidated NetLiquidation tag (authority + FX
correctness) with rate-converted reconstruction as fallback; same-
currency books keep the fresher position-derived reconstruction.
Live: netLiq 1,085,714 → 1,046,098 (TWS says 1,046,101), uPnL -4,398
→ -305.71. #314's duplicate-position complaint was already fixed by
tonight's upsert-by-conId.

#294 — a silently-dead Gateway socket kept health green because the
IBKR account surface is cache-backed (no query ever touches the
socket). Added: 45s reqCurrentTime heartbeat that marks the connection
dead on timeout; connectionClosed marks dead immediately; cache reads
and order paths loud-refuse while dead (orders could previously be
accepted and never transmit); init() force-tears-down a half-open
socket so the recovery loop's re-init actually reconnects.

#208 — orders/positions tables rendered the internal aliceId before
the symbol. Symbol first, aliceId moved to the tooltip.

1934 tests green (+10 regression specs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ot-loop crash

The community "option PnL direction is flipped" report, reproduced live
via the four-combo matrix (long/short × call/put on AAPL paper) and
S14-ified in the catalog:

1. IBKR averageCost is PER CONTRACT (multiplier-baked: 103 for an
   option bought at 1.03) while marketPrice is per unit. TWS's own
   pass-through PnL was correct, but every surface that RECOMPUTES
   (mark − avg) × mult — simulator, any pnlOf consumer — produced
   ~100x-wrong, sign-inverted numbers: a losing short put showed
   +10,181. avgCost is normalized to per-unit at the bridge.

2. simulatePriceChange matched positions by bare symbol — option rows
   sharing the underlying's symbol got RE-MARKED WITH THE STOCK PRICE
   (observed live: simulatedPrice 275.98 on a 1.15 put, +23,874%
   "move"). Symbol-level changes now exclude derivative secTypes with a
   loud note; 'all' scales each row's own mark; the simulated math is
   multiplier-aware (was also dropping ×100 on contract values).

3. Round-6 regression, found the hard way: getPendingOrderIds read
   operations[j] for every RESULT row — sync commits store 1 op with N
   results, so the first multi-update sync commit (two option fills in
   one poller pass) crashed the UTA process on EVERY boot once
   persisted in the journal. operations[j] ?? operations[0], plus
   getOperationSymbol tolerates undefined. This is exactly the
   workspace-agent-equivalence class: an in-workspace AI placing two
   orders would have bricked its UTA the same way.

Live-verified: short call (+6.52 on 1.0223→0.9571) and short put
(−10.99 on 1.0296→1.1395) both sign-correct on every surface; simulator
excludes "AAPL OPT 260, AAPL OPT 320" loudly while moving the stock leg
−5%; closed positions vanish from the cache immediately (zero-qty
removal). Account left flat at baseline. Catalog gains S14.

Known gap noted: venue reject REASONS for late rejections (post-submit,
caught by sync) don't reach the ledger row — only the status does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant