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 RateLimitedHttpClient with Alpaca’s 200 req/min token bucket, exponential backoff, and Retry-After respect.
  • WebSocket live feed. AlpacaLiveFeed covers trade / quote / bar streams and the trade_updates channel. Fills hit the ledger in milliseconds and dedup with REST polling.

Quickstart: paper

python
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:

python
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:

  1. api_key= and api_secret= on the constructor (tests).
  2. A Secrets instance via secrets= (production: Vault, AWS SM, GCP SM, 1Password).
  3. EnvSecrets() fallback. Maps alpaca.key to ALPACA_KEY.

See Secrets.

python
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

python
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/stream
  • paper=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 statusOrderStatusNotes
new, accepted, calculatedAcceptedLive at the venue
pending_new, accepted_for_biddingPendingNewSubmitted, awaiting broker ack
partially_filledPartiallyFilledMore fills expected
filledFilledTerminal
done_for_dayDoneForDayTerminal until next session
canceledCanceledTerminal
pending_cancelPendingCancelCancel sent, awaiting ack
replacedReplacedAmend accepted
pending_replacePendingReplaceAmend sent, awaiting ack
rejectedRejectedTerminal
expiredExpiredGTD / GTC expired
suspended, held, stoppedSuspendedBroker-initiated pause

Unknown statuses fall back to OrderStatus.New and are logged so the map can be extended. Full enum: Order lifecycle.

Fill buffer

python
alpaca.drain_fills()     # pending fills + fresh REST-polled fills, dedup'd
alpaca.push_fill(fill)   # inject a fill (used by AlpacaLiveFeed)

drain_fills() combines:

  1. Pending buffer (fills injected by push_fill since the last drain).
  2. REST poll of /v2/orders?status=closed for orders with status in {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

TypeOrderAction.order_typeRequirements
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_classRequired fieldsNotes
"bracket"take_profit_price and stop_loss_priceEntry + profit-target + protective stop, atomic ticket.
"oco"take_profit_price and stop_loss_priceTwo-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.

python
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.

python
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 full horizon.stats inference 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: Alpaca venue + AlpacaLiveFeed over WebSocket
    • AuditedVenue writing a hash-chained SQLite audit log + a tiny Z-score reversal strategy + risk config + hz.run(mode="live", ...) with max_duration_s and idle_timeout_s bounds. 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_trader and day_trade_count_5d. Gating belongs to the risk engine.
  • Fractional shares. Pass quantity below 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; expect RuntimeError on 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