Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Dependabot, watching every manifest the workspace ships (#291).
#
# The `pip` ecosystem works per manifest directory, and this workspace is six
# distributions: the root plus five under packages/. A directory not listed here is a
# The `pip` ecosystem works per manifest directory, and this workspace is seven
# distributions: the root plus six under packages/. A directory not listed here is a
# distribution whose dependencies update without review -- tests/test_security_scans.py
# pins the full set so adding a seventh distribution means adding it here or failing CI.
# pins the full set so adding another distribution means adding it here or failing CI.
# Updates are grouped (one PR per week per ecosystem, not one per package) because this
# is a solo-maintained repo: review bandwidth is the scarce resource, and a wall of
# single-package PRs is how "ignore Dependabot" becomes the policy.
Expand Down Expand Up @@ -41,6 +41,14 @@ updates:
python-dependencies:
patterns: ["*"]

- package-ecosystem: pip
directory: /packages/keel-broker-alpaca
schedule:
interval: weekly
groups:
python-dependencies:
patterns: ["*"]

- package-ecosystem: pip
directory: /packages/keel-broker-fake
schedule:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
# versions scanned. Unlike the Snyk export there, dev dependencies are INCLUDED
# deliberately: this job gates nothing, so the wider advisory net costs nothing, and
# a CVE in the dev toolchain is still something a contributor's machine runs. Extras
# are included for the same reason; the repo's own six distributions are excluded
# are included for the same reason; the repo's own seven distributions are excluded
# because they are the code under scan, not third-party dependencies of it.
- name: Export the locked dependency set
run: >
Expand All @@ -59,6 +59,7 @@ jobs:
--no-emit-package keel-core
--no-emit-package keel-broker-api
--no-emit-package keel-broker-coinbase
--no-emit-package keel-broker-alpaca
--no-emit-package keel-broker-fake
--no-emit-package keel-broker-robinhood
> requirements.lock.txt
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ responsible for your own trading decisions. Licensed under
[Apache-2.0](LICENSE).

**Trademarks:** Alpaca, Coinbase, and Robinhood are trademarks of their respective owners.
keel is not affiliated with, endorsed by, or sponsored by any of them. Every `keel-broker-*`
keel has no affiliation with any of them, no endorsement from any of them, and no
sponsorship from any of them. Every `keel-broker-*`
package is an independent, original open-source implementation of keel's broker port against
that venue's publicly documented API — a client of the venue, not a product of it. Venue
names appear here solely to identify what the code talks to.
70 changes: 70 additions & 0 deletions packages/keel-broker-alpaca/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# keel-broker-alpaca

A `Broker` adapter for keel's broker port, implemented against Alpaca's publicly
documented Trading and Market Data APIs (https://docs.alpaca.markets/).

**Not affiliated with, endorsed by, or sponsored by Alpaca.** This is an original
implementation of keel's port against the venue's public API — no Alpaca SDK, no code
from any third-party Alpaca adapter. "Alpaca" appears here solely to identify what this
package talks to.

US equities, cash account, long-only, regular session. Paper and live are separate
hosts selected by an explicit `endpoint` choice — there is no configuration path from a
paper credential to `https://api.alpaca.markets`, by construction.

## What works

| Capability | Detail |
| ------------- | ---------------------------------------------------------------------------- |
| Balances | USD row from the account (`available` = buying power clamped at cash, surfacing the T+1 settlement gap), one row per long position (`available` from `qty_available`). |
| Candles | Split-adjusted bars, `15Min`/`1Hour`/`1Day` mapped onto keel's `Granularity`, paginated to the end of the window. Data tier (IEX/SIP) declared per request. |
| Orders | All four port kinds: notional market, fractional-qty market, GTC limit, GTC stop-limit. `extended_hours: false` pinned on every body. |
| Preview | Synthetic only (`synthetic=True`) — no preview endpoint exists. Prices off the latest quote's crossed side (ask for buys, bid for sells), surfaces `best_bid`/`best_ask`, and computes sell-side regulatory fees. |
| Order status | `get_order` maps Alpaca's status enum to the port's vocabulary; unknown statuses stay `PENDING`. |
| Cancel | 204 from `DELETE /v2/orders/{id}` is the venue confirmation; 404/422 and any transport failure answer `False`. |
| Session | `is_market_open()` reads the venue's clock (`/v2/clock`) — no local calendar. |
| Rate limits | 429 retried with `Retry-After` when sent, exponential backoff otherwise, bounded attempt budget (FR-11). |

## Fees (FR-7)

Commission is $0. Sells carry regulatory pass-throughs, modelled in `fees.py` with the
rates as provenance-commented constants:

- **SEC Section 31**: $22.90 per $1,000,000 of sale proceeds — Alpaca's own
regulatory-fees page ($27.80 previously; the SEC adjusts the rate periodically, and
its advisory 2026-2 moves it to $20.60 per $1M as of 2026-04-04 — a documented
re-measurement point, encoded as the venue's published figure until Alpaca's page
moves).
- **FINRA TAF**: $0.000166 per share, capped at $8.30 per trade — the cap is on
Alpaca's page; the per-share rate is FINRA Schedule A §4(b)(7), in force since
2021-01-01.

CAT (buys and sells, sub-cent per trade) is a documented omission. A fee summary is NOT
offered: `supports_fee_summary` is `false` because Alpaca's Trading API publishes no fee
tiers, no fees-paid total, and no volume window — the three things a `FeeSummary` would
assert. Fabricating zeros would read as coverage (the #197 lesson).

## Declared capability gaps

- **Bracket/OCO and stop-market are not declared.** The port's `OrderSpec` has no
bracket concept and no stop-market kind; keel's stop-loss + take-profit exit legs ride
as the separate `StopLimitGTC`/`LimitGTC` orders the port already models. This adapter
does not invent venue-side order kinds the engine cannot ask for.
- **No fee summary** (above).
- **`MarketOnOpen`/`MarketOnClose`**: available at the venue, unused by the engine, and
not expressible in the port's order vocabulary — recorded per FR-3.
- **Corporate actions (FR-10)**: bars are requested split-adjusted (`adjustment=split`,
pinned in `transport.BAR_ADJUSTMENT` so a cached series can always state its policy);
consuming split/dividend announcements and the dividend-purification recording flow
are Phase B work.
- **Extended/overnight sessions**: OFF by posture; every order body pins
`extended_hours: false` (FR-9).

## Running the conformance suite

```sh
uv run pytest tests/conformance/test_alpaca_conformance.py tests/broker_alpaca -q
```

Everything runs against canned fixtures in `tests/fixtures/alpaca_*.json` — no network,
no credentials, no orders.
10 changes: 10 additions & 0 deletions packages/keel-broker-alpaca/keel_broker_alpaca/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Alpaca US-equities adapter for keel, registered as the `alpaca` broker plugin.

Not affiliated with, endorsed by, or sponsored by Alpaca. This is an original
implementation of keel's broker port against Alpaca's publicly documented Trading and
Market Data APIs -- no Alpaca SDK, no third-party adapter code.
"""

from keel_broker_alpaca.adapter import AlpacaAdapter

__all__ = ["AlpacaAdapter"]
Loading
Loading