Paper-trading with Alpaca: the gotchas nobody mentions
Alpaca’s free paper API is the easiest on-ramp to algo trading. It also has a half-dozen quirks that’ll bite you in week one. Here’s our list.
Alpaca's a great broker for getting started with algorithmic trading. The API is clean, the paper account is free, and the documentation is mostly good. But after building a bot on top of it for several months, here are the things that cost us hours we'll never get back, written down so they don't cost you the same.
1. Paper data ≠ live data
Free paper accounts get the IEX feed, which is a single exchange — not the consolidated SIP feed paid accounts get. The price you see on Alpaca paper is whatever traded most recently on IEX, which can differ from the “market price” you see in a brokerage app by 5-50 basis points on liquid names and much more on less-liquid ones.
Translation: your paper bot's fills will not perfectly replicate what live would do, especially in the first seconds after a bar close when SIP and IEX diverge most.
2. Stock data API returns 403 on paper accounts (sometimes)
Hitting data.alpaca.markets/v2/stocks/bars with paper-account credentials throws a 403 with no helpful message. The cause: that endpoint serves SIP data, which requires a paid subscription. Free paper accounts get IEX data via a different endpoint path.
Use alpaca-py with StockBarsRequest and the right feed parameter:
from alpaca.data import StockBarsRequest, TimeFrame
from alpaca.data.historical import StockHistoricalDataClient
from alpaca.data.enums import DataFeed
client = StockHistoricalDataClient(api_key, secret_key)
req = StockBarsRequest(
symbol_or_symbols=["SPY"],
timeframe=TimeFrame.Hour,
start=start, end=end,
feed=DataFeed.IEX, # <-- this matters for free paper
)
bars = client.get_stock_bars(req)Crypto data has no such trap; it's served identically to paper and live accounts. Useful workaround when you're just trying to test bot logic — develop against crypto on paper, debug stocks once you've got a paid account.
3. Multi-symbol crypto bars are flaky
Calling get_crypto_bars with a list of 10 symbols and limit=1300sometimes returns bars for only one symbol — usually the last one in the list, but we've seen other patterns. We've never gotten a satisfying explanation; we just stopped relying on the multi-symbol path.
Solution: fetch one symbol per request, in a loop. It's 10x more API calls but always returns what you asked for. The rate limit on Alpaca crypto data is generous (200 req/min) so this isn't a real bottleneck.
4. Order rejection messages are inconsistent
Submit an invalid order and you might get any of:
- HTTP 422 with a clear text reason in the body.
- HTTP 200 with the order accepted, then immediately rejected via the order-update stream.
- HTTP 200 with the order in
newstate for minutes, then silently dropped. - HTTP 403 with a vague “forbidden” message that's actually a rate-limit response.
Defensive client code: subscribe to the order-update WebSocket beforesubmitting, treat 200 as “tentative,” wait for the corresponding fill / rejection event, time out after N seconds with an explicit retry policy. The bot does this; you should too.
5. Account state lags fills by ~1 second
After a fill executes, the account's buying power and cash don't update for ~1 second. If your bot tries to place a second order right after the first fill — even with the same buying-power constraint that just freed up — you'll get a “insufficient buying power” rejection.
Workaround: track buying power locally, decrementing on order submission rather than reading it back from the API between rapid orders. Re-sync from the API on a slower cadence (every 30s).
6. Crypto pairs use slash; stocks don't
Tiny but constant: BTC/USD with the slash for crypto, SPYwithout anything for stocks. If your config drops the slash on a crypto symbol you get a cryptic 404. Worse: writing the symbol in the wrong format in a position-management call leads Alpaca to think you're looking up a different asset entirely. Validate symbol format at config-load time.
7. Paper crypto is real exchange data, not paper-only
For stocks, paper accounts have their own simulated fill engine — your buy doesn't actually move the market. For crypto, paper trades hit the same orderbook as the live venue (Coinbase Prime, in Alpaca's case). Your fills are as real as live in terms of price discovery; the only thing “paper” about them is that no money actually moves on settlement.
Practical implication: paper crypto P&L is a much more honest representation of what live crypto would do than paper stock P&L is. If you're evaluating a strategy, crypto paper is the better signal.
8. Market hours in the response are tz-naive (and lie)
Alpaca returns market-open/close timestamps as ISO strings without tz info, in US Eastern time. Theget_clock() response says 2026-04-29 09:30:00. If you don't parse that as Eastern, you get tz-naive datetime arithmetic that's off by 4-5 hours depending on DST, depending on your bot's host time zone. Half the bot's “market is closed” bugs we've seen are this exact thing.
# Wrong:
clock = client.get_clock()
if datetime.now() < clock.next_open: # tz-naive vs Eastern
sleep_until(clock.next_open)
# Right:
import pytz
ny = pytz.timezone("America/New_York")
now_ny = datetime.now(ny)
if now_ny < clock.next_open.astimezone(ny):
sleep_until(clock.next_open)9. Pattern Day Trader rules apply on paper too
Even on a paper account: 4+ day trades in 5 business days on a margin account flips you to PDT-restricted, which blocks new day trades until you have $25k equity. Paper accounts default to $100k equity so this rarely bites in practice, but if you reset your paper account to a small starting balance to simulate a real retail account, congrats: you've also given yourself the PDT lockout.
10. The reset-paper endpoint is async and silent
POST /v2/account/configurations with a reset flag works, but the reset takes 5-30 seconds to fully propagate. During that window, account-state queries return confusing intermediate values. If your bot is running while you reset, expect a few minutes of anomalies.
The takeaway
None of these are showstoppers; all of them are tax. We learned each one the hard way and now they're documented in our codebase as comments on the affected lines. Skip the education by reading them here first.
And if you're evaluating Alpaca specifically: it's still the easiest broker to start with for retail algo trading. IBKR has more capability and worse ergonomics. Tradier and TastyTrade are mostly options-focused. Robinhood's API is unofficial and can disappear at any time. Alpaca's trade-offs are the most reasonable for the price of zero dollars.