Broker APIs: Alpaca, IBKR and Binance
Lesson 5 · about 11 min
Eventually you want data from the place you will trade, because that is the data your live system will see. The three APIs below cover most retail algorithmic traders across stocks, futures, forex and crypto. This lesson explains how each is shaped and how to keep your code independent of all of them. No code here makes a network call; the examples show the pattern you will write once you have an account.
Three shapes of API
Alpaca (US stocks, ETFs, some crypto) is a plain REST and WebSocket API with JSON. You get an API key and secret; paper trading is a separate key pair against a separate endpoint. Historical bars come from a /bars endpoint with a symbol, timeframe and date range. It is the simplest to start with and the one Module 9 uses as its reference shape.
Interactive Brokers (IBKR) covers stocks, options, futures and forex across many exchanges. Its API is not a simple REST service; you run their Trader Workstation or Gateway application locally and your Python code talks to it over a socket, usually through the ib_insync (now ib_async) library. Historical data requests are paced (a limited number per ten minutes), and the connection must be alive for anything to work. More capable, more moving parts.
Binance (crypto spot and perpetual futures) exposes public REST endpoints for candles that need no key at all, plus signed endpoints for orders. Candles are returned as arrays, not objects, with millisecond timestamps. Availability depends on your jurisdiction.
The details differ, but every one of them ends up giving you the same thing: a list of bars, each with a timestamp, open, high, low, close and volume. That is the observation that keeps your code sane.
The adapter pattern
Write one function per source that returns the standard DataFrame, and make the rest of the project consume only that DataFrame.
# src/broker_data.py (shapes only; fill in when you have an account)
from datetime import datetime, timezone
import pandas as pd
BAR_COLUMNS = ["open", "high", "low", "close", "volume"]
def bars_from_records(records: list[dict], ts_key: str, unit: str | None = None) -> pd.DataFrame:
"""Turn a list of dicts from any API into standard bars indexed by UTC time."""
df = pd.DataFrame.from_records(records)
df["date"] = pd.to_datetime(df[ts_key], unit=unit, utc=True)
df = df.set_index("date").sort_index()
df = df.rename(columns={"o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"})
return df[BAR_COLUMNS].astype(float)
def bars_from_binance_klines(klines: list[list]) -> pd.DataFrame:
"""Binance returns arrays: [open_time, open, high, low, close, volume, close_time, ...]."""
records = [
{"ts": k[0], "open": k[1], "high": k[2], "low": k[3], "close": k[4], "volume": k[5]}
for k in klines
]
return bars_from_records(records, ts_key="ts", unit="ms")
A fake Alpaca-style response and a fake Binance-style response through the same funnel:
alpaca_like = [
{"t": "2024-03-01T05:00:00Z", "o": 100.0, "h": 101.5, "l": 99.2, "c": 101.0, "v": 1_200_000},
{"t": "2024-03-04T05:00:00Z", "o": 101.2, "h": 102.0, "l": 100.1, "c": 100.4, "v": 990_000},
]
binance_like = [
[1709251200000, "62000.0", "62500.0", "61800.0", "62300.0", "1500.5", 1709254799999],
[1709254800000, "62300.0", "62800.0", "62100.0", "62650.0", "1320.2", 1709258399999],
]
print(bars_from_records(alpaca_like, ts_key="t"))
print(bars_from_binance_klines(binance_like))
Both print a DataFrame with the same five float columns and a UTC index. Everything downstream, from indicators to backtests, sees no difference. If you switch brokers, you write one new adapter function and nothing else changes.
Keys and secrets
API keys go in environment variables, never in code and never in git.
import os
def load_credentials(prefix: str) -> tuple[str, str]:
key = os.environ.get(f"{prefix}_API_KEY")
secret = os.environ.get(f"{prefix}_API_SECRET")
if not key or not secret:
raise RuntimeError(f"set {prefix}_API_KEY and {prefix}_API_SECRET in the environment")
return key, secret
Put them in a .env file that is listed in .gitignore, and load it with python-dotenv or your shell. Use paper-trading keys until Module 9 says otherwise, and even then keep live keys on a machine that does not also run experiments.
Key idea: Every API becomes the same DataFrame at the boundary. The broker is a detail behind one adapter function; the strategy never learns which broker it is talking to.
Practical caveats by source
- Alpaca: bars are in UTC; the "daily" bar for US stocks is stamped at 04:00 or 05:00 UTC depending on daylight saving. Free data is from the IEX feed only and has lower volume than the consolidated tape. Paper and live are different hosts; confusing them is a classic error.
- IBKR: historical requests are rate-limited and return in local exchange time unless you ask for UTC. The gateway logs out daily unless configured otherwise. Expect to spend a day on setup.
- Binance: timestamps are milliseconds. Numbers arrive as strings and must be cast. Spot and futures are separate APIs with separate symbols and separate histories. Perpetual futures also have funding payments that do not appear in candles but do appear in your P&L.
Save whatever you fetch into data/raw/ with the source and fetch date in the filename, then run the same validate_bars and data_report as for CSVs. Broker data is not cleaner than free data; it is only closer to what you will trade on.
Try it: Write a third adapter,
bars_from_ibkr_like, for a list of objects with attributesdate, open, high, low, close, volumewheredateis a naive local-time string like"20240301 09:30:00". Localize it toAmerica/New_Yorkbefore converting to UTC (pd.to_datetime(...).tz_localize("America/New_York").tz_convert("UTC")). Confirm the resulting index prints as 14:30 UTC.
Recap
- Alpaca is a simple REST API; IBKR runs through a local gateway with paced requests; Binance returns unauthenticated candle arrays with millisecond timestamps.
- Write one adapter per source that returns the standard UTC OHLCV DataFrame.
- Credentials live in environment variables and never in code or git.
- Each source has its own timestamp and adjustment quirks; check them explicitly.
- Broker data is validated exactly like any other data.