Skip to content

feat(broker-alpaca): keel-broker-alpaca — original Alpaca port adapter, conformance green - #382

Merged
eaitbrahim merged 1 commit into
mainfrom
feat/369-keel-broker-alpaca
Aug 19, 2026
Merged

feat(broker-alpaca): keel-broker-alpaca — original Alpaca port adapter, conformance green#382
eaitbrahim merged 1 commit into
mainfrom
feat/369-keel-broker-alpaca

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

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 — raw REST over an injected requests transport (the keel-broker-robinhood convention), no alpaca-py SDK, no third-party adapter code. 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, same posture as robinhood), mypy strict from birth, Dependabot entry + pip-audit export exclusion.
  • FR-2 Venue identityvenue = "alpaca", USD quotes, asset_classes = {"equity"} (the port's keel-side vocabulary, not the venue's casing).
  • FR-3 Orders — all four port kinds: market_ioc_quote → Alpaca's notional market order (notional + type: market + time_in_force: day, as the docs require), market_ioc_base → fractional qty market, limit_gtc/stop_limit_gtcgtc limit/stop-limit. Fractional sizes render positionally (format(d, "f")), never scientific notation. extended_hours: false is pinned on every body.
  • FR-4 Preview — Alpaca has no preview endpoint, so the preview is synthesized, following the sibling precedent for exactly this case (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 is GET /v2/stocks/{sym}/quotes/latest; best_bid/best_ask ride in Preview.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 in Preview.errors, never as a price.
  • FR-5 Market data15Min/1Hour/1DayFIFTEEN_MINUTE/ONE_HOUR/ONE_DAY; every other granularity refused with ValueError (the port's sanctioned refusal). Pagination follows next_page_token, bounded like the sibling transports. 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 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 feeds Preview.est_fee on sells; buys are honestly zero. CAT is a documented sub-cent omission.
  • FR-8 Conformance — the shared suite (keel_broker_api.conformance.suite.BrokerConformanceTests) runs against the adapter via tests/conformance/test_alpaca_conformance.py, wired exactly the 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 derive from an endpoint enum (TRADING_HOSTS): there is no URL-shaped parameter for the trading host, so a paper configuration cannot reach api.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)

  • 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 Robinhood: fees_usd is always 0, so subscription-lapse detection is inert against this venue #197 lesson recorded in the robinhood adapter).
  • FR-10 corporate actions: bars are requested split-adjusted (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.
  • FR-9 session awareness is wired ahead of the staleness rails: 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 canned FakeTransport + tests/fixtures/alpaca_*.json for the adapter and the conformance suite, a _RecordingHTTP monkeypatched over requests.request for the transport (zero network in CI).

Gates

  • uv run pytest -q — 3103 passed, 1 skipped (pre-existing)
  • uv run ruff check keel tests packages — clean
  • uv run mypy — no issues in 249 files

One fix outside the package

tests/test_scholarly_review.py::test_no_document_claims_a_review_has_occurred is 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 naive endorsed by substring 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

…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
@eaitbrahim
eaitbrahim merged commit 96caef7 into main Aug 19, 2026
5 checks passed
@eaitbrahim
eaitbrahim deleted the feat/369-keel-broker-alpaca branch August 19, 2026 00:04
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>
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.

keel-broker-alpaca Phase A: adapter package + port conformance suite green

1 participant