feat(broker-alpaca): keel-broker-alpaca — original Alpaca port adapter, conformance green - #382
Merged
Merged
Conversation
…r, conformance green Phase A of the keel-broker-alpaca PRD (#369): `packages/keel-broker-alpaca`, an original implementation of the `keel-broker-api` port against Alpaca's publicly documented Trading + Market Data REST APIs (no `alpaca-py`, no third-party adapter code — raw REST over an injected `requests` transport, the keel-broker-robinhood convention). Zero changes under `keel/`. FR-by-FR coverage: - FR-1 Package: workspace-pinned pyproject (`keel-core==0.9.3`, `keel-broker-api==0.9.3`), `py.typed`, registered under `keel.brokers` as `alpaca`, wired into the dev group (optional venue, like robinhood), mypy strict from birth, Dependabot + the pip-audit export exclusion. - FR-2 Venue identity: `venue = "alpaca"`, USD quotes, `asset_classes = {"equity"}` (the port's keel-side vocabulary). - FR-3 Orders: all four port kinds — `market_ioc_quote` maps to Alpaca's notional market order (`notional` + `type: market` + `time_in_force: day`, as the docs require), `market_ioc_base` to fractional `qty` market, `limit_gtc`/`stop_limit_gtc` to `gtc` limit/stop-limit. Fractional sizes render positionally (`format(d, "f")`), never scientific notation. `extended_hours: false` pinned on every body. - FR-4 Preview: Alpaca has no preview endpoint, so the preview is SYNTHESIZED — the keel-broker-robinhood precedent for venues without one (Robinhood's `synthesizes_preview=True` / `supports_native_preview=False` declaration, its "no endpoint that validates the order is a quote" reasoning). The book read is `GET /v2/stocks/{sym}/quotes/latest`; `best_bid`/`best_ask` ride in `Preview.detail` for the #332 warning and the #350 spread gate, and pricing uses only the crossed side (ask for buys, bid for sells). `ap`/`bp` = 0 is the documented "no active side" and lands in `Preview.errors`, never as a price. - FR-5 Market data: `15Min`/`1Hour`/`1Day` → FIFTEEN_MINUTE/ONE_HOUR/ONE_DAY, all other granularities refused with `ValueError` (the port's sanctioned refusal). Pagination follows `next_page_token` (bounded, like the sibling transports). The data tier is a DECLARED capability: constructor-validated `iex|sip`, sent on every data request — never the venue's silent `sip` default. - FR-6 Balances/positions: USD row with `available = min(buying_power, cash)` — on a cash account (`multiplier == 1`) the gap between the two is exactly the unsettled T+1 proceeds, surfaced honestly; long positions map `qty_available` → available, `qty` → total; short rows are skipped (long-only by construction). - FR-7 Fees: commission $0; sells modelled with the regulatory pass-throughs in `fees.py` as provenance-commented constants — SEC Section 31 $22.90/$1M (Alpaca's own regulatory-fees page; SEC advisory 2026-2 moves it to $20.60/$1M as of 2026-04-04 — recorded as a re-measurement point), FINRA TAF $0.000166/share capped $8.30 (FINRA Schedule A §4(b)(7)). The model feeds `Preview.est_fee` on sells; buys are honestly zero. - FR-8 Conformance: the shared suite (`keel_broker_api.conformance.suite.BrokerConformanceTests`) runs against the adapter via `tests/conformance/test_alpaca_conformance.py`, wired the same way the coinbase/robinhood/fake suites are (fixture-backed `FakeTransport`). Green. - FR-11 Rate limits/hosts: 429 retried honoring `Retry-After` when sent, exponential backoff otherwise, bounded attempt budget, then a typed `AlpacaAPIError`. Paper/live hosts come from an endpoint enum (`TRADING_HOSTS`) — there is no URL-shaped parameter for the trading host, so a paper configuration cannot reach the live host; tested structurally. Declared capability gaps (also in the package README): - Bracket/OCO and stop-market are NOT declared: the port's `OrderSpec` has no bracket concept and no stop-market kind, and this adapter does not invent venue-side order kinds the engine cannot ask for. `MarketOnOpen`/`MarketOnClose` likewise (available at the venue, unused, unexpressible in the port). - `supports_fee_summary` is false: the Trading API publishes no fee tiers, no fees-paid total, no volume window — a fabricated `FeeSummary` would read as coverage (the #197 lesson). - FR-10 corporate actions: bars are requested split-adjusted (`transport.BAR_ADJUSTMENT = "split"` so a cached series can state its policy); announcement consumption and dividend-purification recording are Phase B. - FR-9 session awareness is wired ahead of the rails: `is_market_open()` reads the venue's `/v2/clock` (no local calendar); extended hours are off by posture. Also fixes a pre-existing red on main introduced by #380: the README trademark sentence "not affiliated with, endorsed by, or sponsored by" trips `test_no_document_claims_a_review_has_occurred`'s naive "endorsed by" substring scan (verified failing on a clean 768585d tree). Reworded to "no affiliation with / no endorsement from / no sponsorship from" — the disclaimer is unchanged in strength, the trust-scanner stays strict. Fixes #369
This was referenced Aug 19, 2026
Closed
Merged
Merged
eaitbrahim
added a commit
that referenced
this pull request
Aug 20, 2026
Minor, not patch. Three things since v0.9.3 change what an implementor or a deployment can rely on: * A NEW DISTRIBUTION. `keel-broker-alpaca` (#382, #384) plus the paper-equities profile that selects it (#386), so a deployment can now be US equities via the broker port rather than crypto only. * THE PORT CONTRACT MOVED TWICE. `market_clock`/`market_schedule` made venues session-aware (#385), and `place_order` gained `idempotency_key` (#419). Both carry defaults so no CALLER breaks, but a third-party adapter that does not accept them is no longer a `Broker` -- the conformance suite now says so. That is exactly the kind of change a patch bump must not hide. * THE OPERATOR CONSOLE. The TUI became keel's console across #399-#408, and `keel update` (#415/#417) makes a deployment self-updating. Every pinned sibling moves with it. The four production distributions are required `==` at this exact version (`RELEASING.md`, "Release assets"), so a bump that missed one would install a mixed set -- the `keel-trader 0.5.7` against `keel-core 0.5.5` failure `keel versions` exists to catch, and which `~/keel` actually ran across two releases. Also in this window, on the Robinhood adapter: the best_bid_ask fixture corrected against the live venue (#414), a credential guard that catches the error it only claimed to (#416), pre-flight sizing reported on the preview (#418), transport backoff (#420), and the fenced one-order probe (#421). Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase A of the keel-broker-alpaca PRD (#369):
packages/keel-broker-alpaca, an original implementation of thekeel-broker-apiport against Alpaca's publicly documented Trading + Market Data REST APIs — raw REST over an injectedrequeststransport (thekeel-broker-robinhoodconvention), noalpaca-pySDK, no third-party adapter code. Zero changes underkeel/.FR-by-FR coverage
keel-core==0.9.3,keel-broker-api==0.9.3),py.typed, registered underkeel.brokersasalpaca, wired into the dev group (optional venue, same posture as robinhood), mypy strict from birth, Dependabot entry + pip-audit export exclusion.venue = "alpaca", USD quotes,asset_classes = {"equity"}(the port's keel-side vocabulary, not the venue's casing).market_ioc_quote→ Alpaca's notional market order (notional+type: market+time_in_force: day, as the docs require),market_ioc_base→ fractionalqtymarket,limit_gtc/stop_limit_gtc→gtclimit/stop-limit. Fractional sizes render positionally (format(d, "f")), never scientific notation.extended_hours: falseis pinned on every body.keel-broker-robinhood:synthesizes_preview=True/supports_native_preview=False, and its reasoning that no endpoint which prices a quantity is a broker's quote). The book read isGET /v2/stocks/{sym}/quotes/latest;best_bid/best_askride inPreview.detail(feeding the fix(executor): warn loudly when a conditional entry price is overridden by market routing (#260) #332 warning and the A routing-time maximum-spread gate — refuse entries when the live book is too wide #350 spread gate), and pricing uses only the crossed side — ask for buys, bid for sells.ap/bp= 0 is the venue's documented "no active side" and lands inPreview.errors, never as a price.15Min/1Hour/1Day→FIFTEEN_MINUTE/ONE_HOUR/ONE_DAY; every other granularity refused withValueError(the port's sanctioned refusal). Pagination followsnext_page_token, bounded like the sibling transports. Data tier is a declared capability: constructor-validatediex|sip, sent on every data request — never the venue's silentsipdefault.available = min(buying_power, cash): on a cash account (multiplier == 1) the gap between the two is exactly the unsettled T+1 proceeds, surfaced honestly. Long positions mapqty_available→ available,qty→ total; short rows are skipped (long-only by construction).fees.pyas provenance-commented constants: SEC Section 31 $22.90 per $1M of proceeds (Alpaca's own regulatory-fees page, live-read 2026-08-17; the SEC's advisory 2026-2 moves it to $20.60/$1M as of 2026-04-04 — recorded as a documented re-measurement point per PRD §8's regulatory-drift rule), FINRA TAF $0.000166/share capped at $8.30 (the cap is on Alpaca's page; the rate is FINRA Schedule A §4(b)(7), in force since 2021-01-01). The model feedsPreview.est_feeon sells; buys are honestly zero. CAT is a documented sub-cent omission.keel_broker_api.conformance.suite.BrokerConformanceTests) runs against the adapter viatests/conformance/test_alpaca_conformance.py, wired exactly the way the coinbase/robinhood/fake suites are (fixture-backedFakeTransport); green.Retry-Afterwhen sent, exponential backoff otherwise, bounded attempt budget, then a typedAlpacaAPIError. Paper/live hosts derive from an endpoint enum (TRADING_HOSTS): there is no URL-shaped parameter for the trading host, so a paper configuration cannot reachapi.alpaca.markets— tested structurally (tests/broker_alpaca/test_adapter.py::TestPaperLiveIsolation), Venue visibility must be capability-based, not key-presence-based — a read-only key looks identical to a working one #233-aligned.Declared capability gaps (also in the package README)
OrderSpechas no bracket concept and no stop-market kind, and this adapter does not invent venue-side order kinds the engine cannot ask for.MarketOnOpen/MarketOnCloselikewise (available at the venue, unused, unexpressible in the port).supports_fee_summaryis false: the Trading API publishes no fee tiers, no fees-paid total, no volume window — a fabricatedFeeSummarywould read as coverage (the Robinhood: fees_usd is always 0, so subscription-lapse detection is inert against this venue #197 lesson recorded in the robinhood adapter).transport.BAR_ADJUSTMENT = "split", so a cached series can always state its policy); consuming split/dividend announcements and the dividend-purification recording flow are Phase B.is_market_open()reads the venue's/v2/clock(no local calendar that drifts); extended/overnight hours are off by posture.TDD
Tests were written first and confirmed red (5 collection errors,
ModuleNotFoundError: No module named 'keel_broker_alpaca') before the package existed; then implemented to green. Mocking follows the sibling pattern: a cannedFakeTransport+tests/fixtures/alpaca_*.jsonfor the adapter and the conformance suite, a_RecordingHTTPmonkeypatched overrequests.requestfor the transport (zero network in CI).Gates
uv run pytest -q— 3103 passed, 1 skipped (pre-existing)uv run ruff check keel tests packages— cleanuv run mypy— no issues in 249 filesOne fix outside the package
tests/test_scholarly_review.py::test_no_document_claims_a_review_has_occurredis red on a clean 768585d tree (verified by stashing this branch's changes): #380's README sentence "not affiliated with, endorsed by, or sponsored by" trips the scanner's naiveendorsed bysubstring match, negated or not. This PR rewords the README disclaimer to "no affiliation with / no endorsement from / no sponsorship from" — unchanged in strength, and the trust-scanner stays strict rather than being taught about negation.Fixes #369