Alpaca adapter
Alpaca REST and WebSocket venue. Paper and live. Equities, options, crypto.
horizon.venues.alpaca.Alpaca is the first concrete live venue in L1. Pair it with horizon.data.live.AlpacaLiveFeed for sub-second fill latency over WebSocket.
Tier 1 to 3 usage does not change. Load this module only when placing orders against Alpaca’s sandbox or live brokerage. See Venues for the generic Venue Protocol.
What you get
- Paper or live. One constructor flag. Paper defaults on (
paper=True) so accidents fail safe. - All Alpaca asset classes. Equities, options, crypto through the same venue.
- Every order status mapped. 17 Alpaca states to the FIX-style
OrderStatus. No lossy translation. - Populated
VenueOrder/VenueFill/VenueCapital. Idempotency key, exchange of execution, liquidity flag, day-trade count, PDT flag, maintenance margin, day-trading buying power. Everything the audit, best-ex, and PDT modules read. - Rate-limited, retrying HTTP. Every REST call routes through
RateLimitedHttpClientwith Alpaca’s 200 req/min token bucket, exponential backoff, andRetry-Afterrespect. - WebSocket live feed.
AlpacaLiveFeedcovers trade / quote / bar streams and thetrade_updateschannel. Fills hit the ledger in milliseconds and dedup with REST polling.
Quickstart: paper
import os
import horizon as hz
from horizon.venues.alpaca import Alpaca
from horizon.venues.audited import AuditedVenue
from horizon.audit import AuditLog, SQLiteSink
# Credentials: explicit, Secrets, or ALPACA_KEY / ALPACA_SECRET env vars.
os.environ["ALPACA_KEY"] = "PK..."
os.environ["ALPACA_SECRET"] = "..."
alpaca = Alpaca(paper=True, budget_usd=10_000)
alpaca.connect() # GET /v2/account; raises on 401
print(alpaca.balance()) # buying power, cash, PDT status, day-trade count
audit = AuditLog(sink=SQLiteSink("audit.db"))
venue = AuditedVenue(alpaca, audit_log=audit)
Live mode with WebSocket
REST polling works. For production speed pair it with AlpacaLiveFeed:
from horizon.data.live import AlpacaLiveFeed, SubscriptionKind
feed = AlpacaLiveFeed(
paper=True,
fill_hook=alpaca.push_fill, # WebSocket fills into REST venue buffer
)
feed.subscribe(["AAPL", "MSFT"], SubscriptionKind.Quotes)
result = hz.run(
mode="live",
feed=feed,
venues={"alpaca": venue},
strategies=[MyStrategy],
asset_classes=[AssetClass.Equity],
universe=["AAPL", "MSFT"],
accounts=registry,
audit_log=audit,
max_duration_s=3600,
idle_timeout_s=60,
)
fill_hook=alpaca.push_fill lets the WebSocket feed deposit VenueFill into the same buffer drain_fills() reads. Both paths deduplicate by order_id and execution_id. See Live mode.
Credential resolution
The venue never reads os.environ directly. Resolution order:
api_key=andapi_secret=on the constructor (tests).- A
Secretsinstance viasecrets=(production: Vault, AWS SM, GCP SM, 1Password). EnvSecrets()fallback. Mapsalpaca.keytoALPACA_KEY.
See Secrets.
from horizon.secrets import EnvSecrets, AwsSecretsManagerSecrets
alpaca = Alpaca(paper=True) # env fallback
alpaca = Alpaca(paper=True, secrets=EnvSecrets(prefix="STAGING_")) # namespaced env
alpaca = Alpaca(paper=False, secrets=AwsSecretsManagerSecrets(region="us-east-1"))
Alpaca() with no credentials raises RuntimeError("Alpaca credentials missing...").
URL routing
Alpaca(paper=True) # https://paper-api.alpaca.markets
Alpaca(paper=False) # https://api.alpaca.markets (real money)
Alpaca(base_url="https://my-sandbox.example")
AlpacaLiveFeed selects the stream endpoint matching paper=:
paper=True:wss://paper-api.alpaca.markets/streampaper=False:wss://api.alpaca.markets/stream
The market-data URL (wss://stream.data.alpaca.markets/v2/{iex|sip}) is shared across paper and live. Pass data_feed="iex" (free) or "sip" (paid consolidated tape).
Order status mapping
| Alpaca status | OrderStatus | Notes |
|---|---|---|
new, accepted, calculated | Accepted | Live at the venue |
pending_new, accepted_for_bidding | PendingNew | Submitted, awaiting broker ack |
partially_filled | PartiallyFilled | More fills expected |
filled | Filled | Terminal |
done_for_day | DoneForDay | Terminal until next session |
canceled | Canceled | Terminal |
pending_cancel | PendingCancel | Cancel sent, awaiting ack |
replaced | Replaced | Amend accepted |
pending_replace | PendingReplace | Amend sent, awaiting ack |
rejected | Rejected | Terminal |
expired | Expired | GTD / GTC expired |
suspended, held, stopped | Suspended | Broker-initiated pause |
Unknown statuses fall back to OrderStatus.New and are logged so the map can be extended. Full enum: Order lifecycle.
Fill buffer
alpaca.drain_fills() # pending fills + fresh REST-polled fills, dedup'd
alpaca.push_fill(fill) # inject a fill (used by AlpacaLiveFeed)
drain_fills() combines:
- Pending buffer (fills injected by
push_fillsince the last drain). - REST poll of
/v2/orders?status=closedfor orders withstatusin{filled, partially_filled}that have not been seen.
Dedup via an internal _seen_fill_ids set. The set is process-local. After a restart, StateRecovery replays the audit log to rebuild it.
Order types
| Type | OrderAction.order_type | Requirements |
|---|---|---|
| Market | "market" | None |
| Limit | "limit" | price= |
| Stop | "stop" | stop_price= |
| Stop-limit | "stop_limit" | stop_price= and price= |
| Trailing-stop | "trailing_stop" | trail_pct= or trail_price= |
client_order_id is propagated as Alpaca’s idempotency key. Retry the same action and Alpaca rejects the duplicate instead of placing twice. OrderAction.place(...) generates ULID-style ids by default. See Order lifecycle.
Bracket / OCO / OTO orders
Entry and protective legs as one ticket. Set order_class on the OrderAction plus the relevant leg prices; the venue emits the
canonical Alpaca payload.
order_class | Required fields | Notes |
|---|---|---|
"bracket" | take_profit_price and stop_loss_price | Entry + profit-target + protective stop, atomic ticket. |
"oco" | take_profit_price and stop_loss_price | Two-sided protection on an existing position; no entry leg. |
"oto" | exactly one of take_profit_price or stop_loss_price | “One triggers the other”; entry then a single protective leg. |
None / "simple" | (none) | Plain single-leg order. Backward-compatible default. |
Optional: stop_loss_limit_price upgrades the stop leg from
stop-market to stop-limit.
from horizon.types import OrderAction
action = OrderAction.place(
market_id="AAPL", side="buy", quantity=10, price=180.0,
order_class="bracket",
take_profit_price=190.0,
stop_loss_price=175.0,
stop_loss_limit_price=174.5, # optional — stop-limit instead of stop-market
)
venue.submit(action)
The venue refuses to construct a bracket / OCO ticket with a missing
leg — you get a ValueError at submit time, before anything reaches
Alpaca.
Options chain
Alpaca.options_chain(underlying, ...) pulls live snapshots from the
options data API (/v1beta1/options/snapshots/{underlying}). Each row
is a snapshot dict — OCC symbol, latest quote / trade, Greeks and
implied volatility when available.
chain = alpaca.options_chain(
"AAPL",
expiry="2026-06-19",
option_type="call",
strike_gte=175.0, strike_lte=200.0,
)
for row in chain:
print(row["symbol"], row.get("impliedVolatility"), row.get("greeks"))
Filters all map directly to Alpaca’s query parameters: expiry, option_type, strike_gte, strike_lte, limit (capped at 100 per
the venue). Combine with horizon.stats.implied_vol for ad-hoc IV
extraction on quotes that don’t include the field, and with horizon.stats.iv_rank / iv_hv_spread for analysis.
Requires the [options] feature group and an Alpaca account that has
options access enabled.
Tests
tests/test_l1_alpaca_venue.py # 43 tests, REST, no network
tests/test_l1_alpaca_ws.py # 44 tests, WebSocket parser, no network
A FakeHttpClient covers the venue (the constructor accepts http_client= directly; respx is not needed). A FakeWsConn covers WebSocket lifecycle. Useful references for writing fixtures for other venues.
End-to-end examples
Two runnable references live in examples/:
examples/alpaca_research.py— Pull historical bars from Alpaca’s data API, run the fullhorizon.statsinference suite on each symbol (Sharpe bootstrap CI, Jarque-Bera, Ljung-Box, realized vol), and backtest a strategy against the same data. The research half of “Alpaca fully operational”.examples/alpaca_paper_trading.py— Full paper-trading loop:Alpacavenue +AlpacaLiveFeedover WebSocketAuditedVenuewriting a hash-chained SQLite audit log + a tiny Z-score reversal strategy + risk config +hz.run(mode="live", ...)withmax_duration_sandidle_timeout_sbounds. The trading half.
Both examples shim sys.path so they run from any cwd without
shadowing by another installed horizon package.
Not covered
- PDT enforcement. The venue exposes PDT state via
VenueCapital.is_pattern_day_traderandday_trade_count_5d. Gating belongs to the risk engine. - Fractional shares. Pass
quantitybelow 1.0 for eligible symbols. Fractional price ticks vary; paper-test the specific flow. - Options multi-leg. Single-leg options via OCC symbol (
AAPL240119C00195000) and bracket/OCO/OTO envelopes are supported. Combos and spreads as one ticket (iron condor, vertical, etc.) are on the roadmap. - Sandbox integration tests. Test suite does not hit Alpaca’s paper endpoint. For a smoke test,
Alpaca(paper=True).connect()with real credentials is enough; expectRuntimeErroron rejection.
Regulatory notes
Alpaca is a FINRA-member broker-dealer and a qualified custodian under Advisers Act Rule 206(4)-2. Standard custody path for an RIA: the client opens the Alpaca account, signs a limited power of attorney, the advisor trades with Alpaca(paper=False) against the client’s account. The adapter plugs into that arrangement; it does not replace it.
For multi-account block trading, use ProRataAllocator so each child allocation targets the right account’s Alpaca credential. Each Account can point at its own Alpaca key via a Secrets.get("alpaca.key:acct_<id>") convention.
Wash sale, best execution (Rule 606), and 1099-B tax reporting all work off the audit log that AuditedVenue(Alpaca(...), ...) populates. Nothing downstream of the venue is Alpaca-specific.
Related
- Live feeds. Generic
LiveFeedProtocol. - Live mode.
hz.run(mode="live", ...). - Watchdog. Feed staleness and kill-switch behavior.
- Audit trail. The event log wrapping every Alpaca call.
- Order lifecycle. The
OrderStatusenum.