Alpaca end-to-end

Pull data, run stats, backtest, then paper-trade — the full research→production loop against Alpaca

Three runnable scripts ship in examples/, each covering one half of the workflow a Series 65 RIA actually goes through against Alpaca:

ScriptPurposeHits real network?
examples/alpaca_research.pyPull historical bars → horizon.stats inference → backtestYes (read-only data API)
examples/alpaca_paper_trading.pyFull live paper-trading loop with WS feed + audit + riskYes (paper sandbox)
examples/alpaca_paper_smoketest.py8-check verifier that confirms every code path against the real paper APIYes (paper sandbox; 1-share lots of F)

All three need:

bash
pip install -e ".[equity,validate]"
export ALPACA_KEY="PK..."           # paper-only key from the Alpaca dashboard
export ALPACA_SECRET="..."
export ALPACA_API_KEY="$ALPACA_KEY"  # the data provider reads the longer form
export ALPACA_SECRET_KEY="$ALPACA_SECRET"

Reinstall from this repo first. If pip show horizon points at another path on your machine, import horizon may resolve to a stale copy. cd to the repo root, pip install -e ., then verify with python -c "import horizon; print(horizon.__file__)".

1. Research workflow

examples/alpaca_research.py — fetch 13 months of daily bars for AAPL, MSFT, NVDA, run the stats suite per symbol (bootstrap Sharpe CI, Jarque-Bera, Ljung-Box, realized vol), then backtest a Bollinger mean-reversion strategy on the same data.

python
import horizon as hz
from datetime import date, timedelta

# 1) Pull historical bars — one line per symbol set
end = date.today().isoformat()
start = (date.today() - timedelta(days=400)).isoformat()
src = hz.data.alpaca(["AAPL", "MSFT", "NVDA"],
                     start=start, end=end, timeframe="1Day")

# 2) Per-symbol stats with proper uncertainty bands
bars_by_symbol = {s: [] for s in ["AAPL", "MSFT", "NVDA"]}
for bar in src.iter_bars():
    bars_by_symbol[bar.market_id].append(bar)

for sym, bars in bars_by_symbol.items():
    prices = [b.close for b in bars]
    rets = hz.stats.returns_from_prices(prices)
    print(hz.stats.summary(rets))
    print(hz.stats.sharpe_ci(rets, n_resamples=5000, seed=0))
    print(hz.stats.jarque_bera(rets))         # normality assumption test
    print(hz.stats.ljung_box(rets, lags=10))  # iid assumption test

# 3) Backtest on the same data source
from horizon.quant import BollingerMeanRev
result = (hz.pipe("AAPL", "MSFT", "NVDA")
    .strategy(BollingerMeanRev(window=20))
    .data(src)
    .stop_loss(pct=0.05)
    .backtest(cash=100_000))
print(f"Sharpe={result.sharpe:+.3f}  trades={result.n_trades}")

Run it:

bash
python examples/alpaca_research.py

Typical output (your numbers will differ based on the window):

[data]  Loaded 252 bars across 3 symbols from Alpaca.
[stats] AAPL Sharpe 95% CI : CI(0.74, [-0.42, 1.96], 95%, n=5000)
[stats] AAPL Jarque-Bera   : p=0.014  (REJECT normality)
[stats] AAPL Ljung-Box(10) : p=0.341  (fail to reject iid)
...
[backtest] Sharpe : +0.12  n_trades=18

The wide CI is the point: a year of daily data is just barely enough to tell a Sharpe of 0.7 from zero. Quote the band, not the midpoint.

2. Paper trading loop

examples/alpaca_paper_trading.py — full live wiring: venue + WebSocket feed + AuditedVenue writing a hash-chained SQLite log + risk config + lifecycle watchdog + hz.run(mode="live", ...) with a 1-hour kill clock and 2-minute idle timeout.

python
import horizon as hz
from horizon.audit import AuditLog, SQLiteSink
from horizon.data.live import AlpacaLiveFeed, SubscriptionKind
from horizon.lifecycle.events import LifecycleConfig
from horizon.risk.config import RiskConfig
from horizon.risk.drawdown import DrawdownAction, DrawdownGuard
from horizon.risk.stops import StopLoss
from horizon.venues.alpaca import Alpaca
from horizon.venues.audited import AuditedVenue

# Venue — paper=True is the safe default
alpaca = Alpaca(paper=True, budget_usd=100_000)
alpaca.connect()
print(alpaca.balance())

# Audit chain — every submit / ack / fill / cancel becomes a
# hash-linked SQLite row.
audit = AuditLog(sink=SQLiteSink("./alpaca_paper_audit.db"))
venue = AuditedVenue(alpaca, audit_log=audit)

# WebSocket feed — fills hit the venue's pending buffer in
# milliseconds via the fill_hook
feed = AlpacaLiveFeed(paper=True, fill_hook=alpaca.push_fill, data_feed="iex")
feed.subscribe(["AAPL", "MSFT", "NVDA"], SubscriptionKind.Quotes)
feed.subscribe(["AAPL", "MSFT", "NVDA"], SubscriptionKind.Trades)
# trade_updates is auto-subscribed by feed.connect()

# Risk + lifecycle (the prod-hardening additions)
risk_cfg = RiskConfig(
    stop_loss=StopLoss(per_position_pct=0.05),
    drawdown=[
        DrawdownGuard(daily_pct=0.03, action=DrawdownAction.HaltNew),
        DrawdownGuard(weekly_pct=0.08, action=DrawdownAction.ReduceHalf),
        DrawdownGuard(monthly_pct=0.15, action=DrawdownAction.Flatten),
    ],
)
lifecycle = LifecycleConfig(stop_loss_pct=0.10)

# Strategy (any horizon Strategy subclass works)
from horizon.asset_classes import AssetClass
from horizon.features import Zscore
from horizon.strategy import Strategy
from horizon.types import Signal

class ZScoreReversal(Strategy):
    asset_classes = [AssetClass.Equity]
    features = {"z": Zscore(20)}

    def evaluate(self, f, universe):
        out = []
        for m in universe:
            z = f.z[m.id]
            if z is None: continue
            if z < -2.0: out.append(Signal.increase(m, edge_bps=30, horizon="1d"))
            elif z > 2.0: out.append(Signal.decrease(m, edge_bps=30, horizon="1d"))
        return out

# Wire everything together — 1-hour cap, 2-minute idle timeout
hz.run(
    mode="live",
    strategies=[ZScoreReversal],
    asset_classes=[AssetClass.Equity],
    universe=["AAPL", "MSFT", "NVDA"],
    venues={"alpaca": venue},
    feed=feed,
    risk=risk_cfg,
    lifecycle=lifecycle,
    audit_log=audit,
    max_duration_s=3600.0,
    idle_timeout_s=120.0,
)

Run during market hours:

bash
python examples/alpaca_paper_trading.py

You’ll see the connect-and-balance line, the subscribe acks, then the loop ticking. Ctrl-C to stop early. The audit log persists at ./alpaca_paper_audit.db; replay with:

python
from horizon.audit import AuditLog, SQLiteSink
from horizon.audit.chain import AuditChain

audit = AuditLog(sink=SQLiteSink("./alpaca_paper_audit.db"))
events = list(audit._sink.read_range())
print(f"{len(events)} events, chain verifies:",
      "OK" if not AuditChain.verify(events) else "BROKEN")

3. Real-network smoke test — before flipping paper=False

examples/alpaca_paper_smoketest.py — eight checks against the real paper API. The unit tests use a FakeHttpClient; this one uses your actual creds and confirms the code does what the mocks claim. Tiny exposure (1-share lots of F at ~$12), auto-cleanup in a finally block.

bash
python examples/alpaca_paper_smoketest.py

Checks:

#What it verifies
0ALPACA_KEY + ALPACA_SECRET env vars present
1connect() + balance() against paper-api.alpaca.markets
2GET /v2/clock — market open or closed
3Market-order lifecycle: submit → accepted → filled in 10s (PARTIAL outside market hours)
4Bracket order with take_profit + stop_loss legs persists with order_class=bracket and the child legs visible via GET /v2/orders/{id}?nested=true
5Cancel propagates to all bracket legs
6AlpacaLiveFeed connects, authenticates, real fill arrives via push_fill within 10s
7Alpaca.options_chain("SPY") returns parseable shape (SKIP cleanly if account lacks options data access)
8AuditChain.verify() on every event the run emitted

Output is colour-coded PASS / FAIL / SKIP / PARTIAL with a final summary table. Outside market hours, checks 3 and 6 SKIP — the rest run normally. The script ends by cancelling every order it opened and flattening any position it took.

Options data

python
# Chain snapshot (current quotes + Greeks + IV)
chain = hz.data.alpaca_options_chain(
    "AAPL",
    expiry="2026-06-19", option_type="call",
    strike_gte=180.0, strike_lte=200.0,
)
for row in chain:
    print(row["symbol"], row.get("impliedVolatility"))

# Historical bars per contract — chain → OCC symbols → time-series
symbols = [row["symbol"] for row in chain[:5]]
src = hz.data.alpaca_options_bars(symbols, start="2026-01-01")
for bar in src.iter_bars():
    print(bar.market_id, bar.timestamp, bar.close)

Full provider reference: Data Providers. Black-Scholes IV / rank / spread analysis on these dicts: see the Stats reference.

Bracket orders

The Alpaca adapter emits bracket / OCO / OTO envelopes as one ticket. Set order_class plus the leg prices:

python
from horizon.types import OrderAction

action = OrderAction.place(
    market_id="AAPL", side="buy", quantity=10, price=180.0,
    order_class="bracket",                # entry + TP + SL atomic
    take_profit_price=190.0,
    stop_loss_price=175.0,
    stop_loss_limit_price=174.5,          # optional — stop-limit instead of stop-market
)
alpaca.submit(action)

bracket requires both legs; oco requires both (no entry); oto requires exactly one of take_profit_price / stop_loss_price. Mismatches raise ValueError before the request hits Alpaca.

Recommended order before going live

  1. Reinstall from this repo so import horizon resolves here.
  2. Run examples/alpaca_paper_smoketest.py during market hours. All eight checks should be PASS or SKIP.
  3. Run examples/alpaca_research.py to sanity-check the numbers you’ll be staring at all day.
  4. Run examples/alpaca_paper_trading.py for a full session. Watch the audit log grow; verify fills look right.
  5. Paper-trade a strategy for at least a few days. Watch for edge cases.
  6. Flip to Alpaca(paper=False) with a small position size.

Don’t compress steps 2–5. The paper sandbox is the cheapest place to find a bug; the live brokerage is the most expensive.

Related