Tutorial
Connecting Alpaca to Your Trading Strategy — A Developer's Guide
Connecting a strategy to Alpaca is straightforward for a single paper trade. For a production strategy that runs every day, handles partial fills, reconnects after network drops, and does not silently fail — the connection layer requires deliberate engineering.
The difference between a script that works once and infrastructure that works reliably is error handling, retry logic, and an explicit stance on every failure mode. A script with no error handling appears to work fine during development — the API is reachable, the account is funded, the market is open. It fails when something expected goes wrong: the network drops for 8 seconds at 9:34am, the account is briefly flagged by a compliance check, the market was closed for a holiday the developer forgot about. Production reliability means handling these cases before they happen, not after.
This guide covers that full scope: authentication, historical data retrieval, real-time streaming, order placement, and the error handling patterns that prevent quiet failures from becoming undiscovered losses.
Prerequisites
This guide builds on the Python environment from Setting Up Your Trading Development Environment. You need:
alpaca-pyversion 0.20 or later (pip install alpaca-py)python-dotenvfor credential loading (pip install python-dotenv)pandasfor data handling (pip install pandas)- A
.envfile containingALPACA_API_KEY,ALPACA_SECRET_KEY, andALPACA_PAPER=true
Verify your installed version before writing any code: pip show alpaca-py | grep Version. The alpaca-py library replaced the older alpaca-trade-api package; the two are not compatible. If your project has alpaca-trade-api in its requirements, you are on the deprecated SDK — the client class names, import paths, and method signatures differ significantly from what this guide uses.
Client Setup: Trading and Data Separately
Alpaca exposes two categories of client. TradingClient handles order management, account operations, and position queries. StockHistoricalDataClient and the streaming clients handle market data. Keep them in separate objects — they serve different purposes, can fail independently, and in some subscription tiers authenticate differently:
import os
from dotenv import load_dotenv
from alpaca.trading.client import TradingClient
from alpaca.data.historical import StockHistoricalDataClient
load_dotenv()
API_KEY = os.getenv("ALPACA_API_KEY")
SECRET_KEY = os.getenv("ALPACA_SECRET_KEY")
PAPER = os.getenv("ALPACA_PAPER", "true").lower() == "true"
trading_client = TradingClient(API_KEY, SECRET_KEY, paper=PAPER)
data_client = StockHistoricalDataClient() # no auth required for free data tier
The PAPER flag routes all orders to paper-api.alpaca.markets instead of api.alpaca.markets. Set this from the environment, not hardcoded in the source. The cost of deploying with PAPER=false unintentionally is real orders on a live account — the environment variable makes the intent explicit, auditable, and easily changed without touching source code.
If ALPACA_API_KEY or ALPACA_SECRET_KEY is not set in the environment, os.getenv returns None. The TradingClient will initialize without error but fail on the first API call with an authentication error. Add an explicit check at startup:
if not API_KEY or not SECRET_KEY:
raise ValueError("ALPACA_API_KEY and ALPACA_SECRET_KEY must be set in environment")
Fail loudly at startup. Do not discover missing credentials at order submission time.
Fetching Historical Data
Most daily strategies compute signals from end-of-day bar data. The StockHistoricalDataClient retrieves that data cleanly:
import pandas as pd
from alpaca.data.requests import StockBarsRequest
from alpaca.data.timeframe import TimeFrame
from datetime import datetime, timedelta
def get_daily_bars(symbol: str, days: int = 60) -> pd.DataFrame:
end = datetime.now()
start = end - timedelta(days=days)
request = StockBarsRequest(
symbol_or_symbols=symbol,
timeframe=TimeFrame.Day,
start=start,
end=end,
)
bars = data_client.get_stock_bars(request).df
if isinstance(bars.index, pd.MultiIndex):
bars = bars.droplevel("symbol")
return bars
The MultiIndex check is necessary when requesting a single symbol. The alpaca-py library returns a multi-symbol index structure even for single-symbol requests — a design that makes multi-symbol requests consistent but requires the extra step for single-symbol usage. Dropping the symbol level gives a clean DatetimeIndex that rolling window calculations expect.
For backtesting or signal validation over longer periods, increase days. The free Alpaca data tier provides up to 5 years of daily bar history for US equities. Minute-level and second-level historical data require a paid data subscription. For daily strategies, the free tier is sufficient.
One practical note: Alpaca's historical data is adjusted for splits and dividends by default. If your strategy's backtest used unadjusted data, the live data feed will differ from what the backtest saw. Use the adjustment parameter on StockBarsRequest to control this explicitly.
Real-Time Data with WebSocket Streaming
For intraday strategies, historical data retrieval is insufficient — you need prices as they occur. The StockDataStream client maintains a persistent WebSocket connection and delivers bars, trades, and quotes in real time:
from alpaca.data.live import StockDataStream
stream = StockDataStream(API_KEY, SECRET_KEY)
async def handle_bar(bar):
print(f"{bar.symbol} | Close: {bar.close} | Volume: {bar.volume}")
stream.subscribe_bars(handle_bar, "AAPL", "SPY")
import asyncio
asyncio.run(stream.run())
The WebSocket reconnects automatically on drop. However, automatic reconnection does not mean transparent reconnection — there is a window during a reconnect where bar events are not delivered. For long-running strategies, subscribe to status events to monitor stream health explicitly:
async def handle_status(status):
import logging
logging.info(f"Stream status: {status}")
stream.subscribe_statuses(handle_status, "AAPL", "SPY")
An intraday strategy that depends on the stream for signal generation should treat a gap in status callbacks as a degraded state, not a normal one. Log the gap, pause signal generation, and wait for the stream to confirm reconnection before resuming. Generating signals during a reconnect window — when you may have missed bars — produces incorrect z-scores, stale moving averages, and other calculation artifacts that can trigger unintended orders.
Order Management
Every live strategy uses three order operations repeatedly: place a market order, place a limit order, and check an existing position before acting.
Market order — use for entries and exits where execution certainty matters more than price precision:
from alpaca.trading.requests import MarketOrderRequest
from alpaca.trading.enums import OrderSide, TimeInForce
def place_market_order(symbol: str, qty: float, side: OrderSide):
request = MarketOrderRequest(
symbol=symbol,
qty=qty,
side=side,
time_in_force=TimeInForce.DAY
)
return trading_client.submit_order(request)
Limit order — use when the entry price matters to the strategy's expected value. A limit order that never fills is a missed trade; a market order that slips 0.5% on a 1% target trade has erased half the expected gain. Know which cost is acceptable for your edge before choosing order type:
from alpaca.trading.requests import LimitOrderRequest
def place_limit_order(symbol: str, qty: float, side: OrderSide, limit_price: float):
request = LimitOrderRequest(
symbol=symbol,
qty=qty,
side=side,
limit_price=round(limit_price, 2),
time_in_force=TimeInForce.DAY
)
return trading_client.submit_order(request)
The round(limit_price, 2) call matters — Alpaca rejects limit prices with more than two decimal places for most equity symbols. Sending a computed limit price like 412.37234 produces an API error that looks like a network problem if you are not reading the error message carefully.
Position check before ordering — always query the current position before placing an entry order. Sending two entry signals for the same symbol without checking doubles the position size, doubling risk exposure silently:
def get_position(symbol: str):
try:
return trading_client.get_open_position(symbol)
except Exception:
return None # no open position exists for this symbol
If get_position returns None, no position is open and an entry order is safe to place. If it returns a position object, your strategy logic determines whether to add to the position, hold, or exit. Never assume no position exists — always check.
Position quantities returned by Alpaca are strings, not floats — this is a common source of confusion. position.qty returns "2.0", not 2.0. When computing a closing order's quantity from an existing position, cast explicitly: qty = float(position.qty). Sending a string quantity to the order request will raise a validation error.
For strategies using fractional shares (supported on Alpaca), quantities can be non-integer values. qty=0.5 means half a share. This changes position size math: a 2% portfolio allocation at $100,000 buying power and $412 share price is approximately 4.85 shares. Whether to round down to 4, use fractional, or set a minimum order threshold is a strategy decision — document it in the config, not buried in code.
Error Handling for Production
Every order submission in production requires a try/except block. The trading API rejects orders for reasons that are expected and recoverable, and for reasons that are unexpected and should surface immediately:
from alpaca.common.exceptions import APIError
import logging
logger = logging.getLogger(__name__)
def safe_submit_order(request):
try:
order = trading_client.submit_order(request)
logger.info(
f"Order submitted: {order.id} | {order.symbol} | "
f"{order.side} | qty={order.qty}"
)
return order
except APIError as e:
logger.error(f"Order rejected: {e}")
# Common rejection codes:
# 40310000 -- insufficient buying power
# 42210000 -- market closed, order cannot be placed
return None
except Exception as e:
logger.error(f"Unexpected error submitting order: {e}")
raise # re-raise -- do not silently swallow unexpected errors
The separation between APIError and Exception is intentional and load-bearing. APIError represents a structured rejection from Alpaca — the request reached the server, was evaluated, and was refused for a documented reason. Log it, skip the trade, and continue. Exception represents something outside the expected API contract: a network error, a timeout, a bug in request construction. These should propagate up and surface in monitoring, not be caught and silently dropped. An unexpected exception swallowed in a catch-all is an undiscovered bug.
Rate Limits
Alpaca's paper trading API allows 200 requests per minute. Live API limits vary by subscription tier. For strategies polling frequently — account status checks, position queries, order status polls — the rate limit is reachable. Implement exponential backoff on 429 responses rather than failing immediately:
import time
def get_account_with_retry(max_retries: int = 3):
for attempt in range(max_retries):
try:
return trading_client.get_account()
except APIError as e:
if "429" in str(e) and attempt < max_retries - 1:
wait = 2 ** attempt # 1s, 2s, 4s
logger.warning(f"Rate limited. Retrying in {wait}s.")
time.sleep(wait)
else:
raise
If your strategy is hitting rate limits during normal execution, the architecture is polling too frequently. Rate limit responses should be exceptional, not steady-state. Reaching a 429 consistently means redesigning the polling frequency, not tuning the backoff.
One practical pattern to reduce unnecessary API calls: cache account data locally for short periods. Account buying power does not change between API calls unless a trade executes. Polling it once per strategy cycle rather than once per symbol evaluation cuts request volume proportionally. The same applies to the market clock — is_open does not change during normal market hours, so computing it once at cycle start and reusing it within the cycle is correct.
For order status polling — checking whether a submitted order has filled — use Alpaca's streaming order updates feed rather than polling the REST endpoint. The streaming feed delivers fill and partial fill events as they occur without any request overhead:
from alpaca.trading.stream import TradingStream
trade_stream = TradingStream(API_KEY, SECRET_KEY, paper=PAPER)
async def handle_trade_update(data):
logger.info(f"Order update: {data.event} | {data.order.symbol} | {data.order.status}")
trade_stream.subscribe_trade_updates(handle_trade_update)
This eliminates the need to poll order status after submission — the fill event arrives over the stream when the order executes.
A Minimal Production Connection Wrapper
Encapsulating connection setup into a class enforces correct initialization order and concentrates the failure surface at a single, testable location:
class AlpacaConnection:
def __init__(self, paper: bool = True):
load_dotenv()
self.paper = paper
self.trading = TradingClient(
os.getenv("ALPACA_API_KEY"),
os.getenv("ALPACA_SECRET_KEY"),
paper=paper
)
self.data = StockHistoricalDataClient()
self._verify()
def _verify(self):
account = self.trading.get_account()
assert account.status == "ACTIVE", f"Account not active: {account.status}"
buying_power = float(account.buying_power)
print(f"Connected | Paper: {self.paper} | Buying power: ${buying_power:,.0f}")
def is_market_open(self) -> bool:
clock = self.trading.get_clock()
return clock.is_open
Instantiate this at strategy startup. If _verify raises an AssertionError, the strategy does not proceed — it fails loudly at initialization rather than silently at the first order 35 minutes into the trading day. A strategy that fails at startup with a clear message is always preferable to one that starts successfully, runs silently for an hour, and fails at 9:36am with an unclear error.
The is_market_open method is worth calling before every order submission in daily strategies. Submitting a market order when the market is closed on Alpaca results in a DAY order queued for next open — which may or may not align with your intent, depending on whether the signal is still valid 16 hours later when the market reopens. If your strategy's signal is computed at 3:45pm but execution is not intended until the signal is re-evaluated the next morning, the queued order creates an unintended position entry.
The Oyamori Approach
Oyamori's execution engine wraps this connection layer. Retry logic, rate limit handling, fill confirmation, partial fill tracking, and position state management are handled by the platform — not re-implemented per strategy. The connection wrapper above is instructive for understanding the mechanics of the layer; in production with Oyamori, it is managed infrastructure that runs identically for every strategy in the catalog.
The value is not that this code is difficult to write. The value is that these concerns — rate limits, reconnection, fill confirmation, the is-market-open check — are handled correctly once, tested once, and are not the responsibility of each strategy author to implement correctly in isolation.
Next: Scheduling Trading Algorithms — Cron, Docker, and Serverless →