What Alpaca Paper Trading Does and Doesn't Simulate (Before You Go Live)
Paper trading is the right place to test an automated strategy, and it is also the reason people are surprised on their first live day. Alpaca's paper environment fills against the real NBBO, ignores available liquidity completely, and inserts random partial fills on purpose. This is a specific account of what it models, what it quietly leaves out, and how to tell the difference before real money is involved.
Paper is a simulation of the market, not of your broker's queue
Alpaca is unusually direct about this. Its own documentation says paper trading "works the same way as live trading end to end — except the order is not routed a live exchange. Instead, the system simulates the order filling based on the real-time quotes." (Alpaca, Paper Trading.)
That sentence contains the whole problem. Everything up to the moment of execution is real: the same API, the same endpoints, the same order validation, the same account rules. The execution itself is a model. So paper is an excellent test of your software and a weak test of your fills. Those are different questions, and conflating them is what produces the recurring forum post that begins "it worked on paper".
If you are wiring TradingView alerts into Alpaca — whether through your own bridge or a service — the software question is the one that will bite you first. So start there.
How a paper order actually fills
Four rules from Alpaca's documentation govern the paper fill model, and each has a consequence for an alert-driven system.
One: orders match against the NBBO. "All orders submitted in paper trading will be matched against the best available current market price (NBBO)." In practice this means a buy is evaluated against the ask and a sell against the bid, which is why a paper buy can report an average entry above the last traded price you saw on your chart. A wide spread in a thin name will look like an enormous slip when it is just the other side of the quote. This is the source of a recurring confusion on Alpaca's forum, where a paper buy filled above $14 on a stock the user had priced at $12.58.
Two: non-marketable limit orders simply wait. A buy limit is not filled until its limit price is at or above the best ask; a sell limit waits for the limit to be at or below the best bid. Nothing unusual there — but it means a limit order from your alert can sit open indefinitely, and your bridge needs to have an opinion about that.
Three: quantity is not checked against available liquidity. Alpaca states plainly that "your order quantity is not checked against the NBBO quantities. In other words, you can submit and receive a fill for an order that is much larger than the actual available liquidity." Paper will fill a million shares of a micro-cap as happily as it fills one. If your position sizing is large relative to the name you are trading, paper will not tell you.
Four — and this is the one almost nobody knows — partial fills are injected deliberately. From the same page: "When orders are eligible to be filled, they will receive partial fills for a random size 10% of the time. If the order price is still marketable, the remaining quantity would be re-evaluated for a subsequent fill."
That 10% is a feature, not a glitch. Partial fills are comparatively rare in live equity trading at retail size, so Alpaca raises the rate in paper on purpose to force your code to encounter them. If you have been treating a partial fill as a quirk of paper and ignoring it, you have been deleting your own test coverage.
The partial-fill bug that lives in almost every DIY bridge
Here is the shape of nearly every webhook-to-broker script on GitHub:
response = requests.post(f"{ALPACA_BASE}/orders", json=order,
headers=ALPACA_HEADERS, timeout=10)
if response.status_code < 400:
print("Done - we are in the position") # this is wrong
An HTTP 200 from POST /v2/orders means Alpaca accepted the order. It
does not mean the order filled, and it certainly does not mean it filled in full. The order
object comes back with a status of accepted or
pending_new, and it moves through new,
partially_filled, filled, expired,
canceled or rejected afterwards.
The failure mode is specific and expensive. Your alert fires a buy for 100 shares. Twelve fill. Your exit logic later sends a sell for 100, because that is what it believes it owns. Depending on timing, you either get rejected for insufficient position or you end up short. Neither is what the strategy intended.
The fix is to read the fill back rather than assume it:
import time
def confirm_fill(order_id, timeout_s=30):
"""Poll until the order reaches a terminal state. Returns (status, filled_qty)."""
terminal = {"filled", "canceled", "expired", "rejected", "done_for_day"}
deadline = time.time() + timeout_s
order = {}
while time.time() < deadline:
r = requests.get(f"{ALPACA_BASE}/orders/{order_id}",
headers=ALPACA_HEADERS, timeout=10)
r.raise_for_status()
order = r.json()
if order["status"] in terminal:
return order["status"], float(order["filled_qty"] or 0)
time.sleep(1)
# Still open. That is an answer too - and a human should hear about it.
return "timeout", float(order.get("filled_qty") or 0)
Polling is the simple version. For anything you depend on, Alpaca recommends its streaming trade-updates interface instead, since it pushes state changes rather than making you ask. Either way the rule is the same: the quantity you ordered and the quantity you own are two different numbers, and only one of them is a fact.
What paper does not account for at all
Alpaca publishes this list directly. Paper trading does not account for market impact, information leakage, price slippage due to latency, order queue position for non-marketable limit orders, price improvement received, regulatory fees, or dividends. The paper-versus-live table adds borrow fees, which paper does not charge.
Translated into things that will actually happen to you:
| Omission | What it means live |
|---|---|
| Slippage due to latency | Paper prices your fill off the quote at evaluation time. Live, your order travels, and the quote moves while it does. |
| Market impact | Your own order moves the price against you in a thin name. Paper never does. |
| Queue position | A resting limit order live sits behind everyone who got there first. In paper it fills as soon as it becomes marketable. |
| Regulatory fees | Small per-sale fees on equities that paper does not deduct. |
| Dividends | Not simulated. A dividend-paying holding behaves differently in the two environments. |
| Borrow fees | Shorting costs nothing in paper and is not free live. |
Corporate actions are the other significant gap. Splits and similar events are not modelled in paper, which means a paper position in a stock that splits will diverge from reality in a way no amount of careful coding on your side can correct.
Crypto has an explicit cost that paper does not charge
Equities on Alpaca are commission-free, so the fee gap there is small. Crypto is not. Alpaca's published fee schedule puts Tier 1 (under $100,000 of 30-day volume, which is where you will be) at 0.15% maker and 0.25% taker. A market order is a taker order by definition, so an automated strategy that enters and exits with market orders pays roughly 0.5% per round trip in fees before anything else happens.
Paper does not deduct that. Crypto fees are charged on the asset you receive and, per Alpaca's docs, are "calculated and posted end of day" rather than in real time. So a crypto strategy whose average edge per round trip is thinner than the fee is indistinguishable in paper from one where it is not. Work out your fee cost per round trip with a pen before you draw any conclusion from a paper equity curve.
What does behave the same — and is therefore worth testing hard
This is the half people skip, and it is where paper earns its keep. The following are real account-level rules that apply in paper exactly as they do live, which makes paper the correct and free place to discover them.
Wash-trade protection, HTTP 403
Alpaca checks every order for potential self-trades and, per the user protection docs, "we reject the order and send back an error message with the HTTP status code 403 (Forbidden)". The rejection matrix is broad: an existing market buy against any new sell is always rejected, and two limit orders are rejected if the buy limit price is at or above the sell limit price. Bracket, OCO and trailing-stop orders are the documented exceptions.
This matters for automation, because a strategy that flips from long to short, or sends an exit immediately followed by a re-entry, is exactly the pattern that trips it. Alpaca explicitly notes the protection applies to paper accounts and recommends "testing your trading algorithm with a realistic balance amount" — which is good advice for a second reason. The default paper balance is $100,000, and a strategy sized against $100,000 behaves nothing like the same strategy sized against $4,000.
Buying power is reserved by open orders
Open buy orders reduce your available buying power immediately, and sell orders do not replenish it until they execute. Alpaca's worked example: with $10,000 of buying power, a $3,000 limit buy leaves $7,000, and a subsequent $8,000 order is rejected — even though the first order has not filled. An automation that fires several alerts in sequence can reject its own later orders this way, and paper will show you that for free.
Market orders outside regular hours
A market order cannot execute in extended hours. Alpaca accepts only
limit orders with a time-in-force of day or gtc as
extended-hours eligible — "all other order types and TIF values will be rejected with
an error". And an order that is not extended-hours eligible, submitted after 4:00pm
ET, is "queued up for release the next trading day". An alert that fires at
8:00pm does not do nothing; it quietly becomes tomorrow's order at tomorrow's price. If
your chart runs around the clock but you trade equities, test this deliberately.
Crypto's narrower order rules
Crypto trades continuously and accepts only gtc and ioc for
time-in-force — day, opg, cls and fok
are not supported. A bridge that hard-codes "time_in_force": "day" works
perfectly for stocks and fails on every crypto order. Paper catches that in a minute.
A go-live sequence that actually finds problems
"Paper trade for a month" is advice with no test in it. Here is a sequence with pass conditions, which is more useful than a duration.
- Reset the paper account to a realistic balance. Not $100,000 unless you intend to go live with $100,000. Position-sizing bugs only appear at the size you will actually use.
- Fire one alert manually and confirm the order appears. End to end, from TradingView to the Alpaca dashboard, before anything runs unattended.
-
Break it on purpose. Send a symbol that does not exist
(
ZZZZZ), an order far larger than your buying power, and a crypto order withtime_in_forceset today. All three should produce a clear rejection that reaches you, not just a log line. - Trigger a wash-trade 403. Place a resting limit sell, then send a market buy. Confirm your system reports the 403 rather than silently continuing as though the position had changed.
- Find a partial fill and let it happen. Given the 10% injection rate, a few dozen orders will produce one. Confirm your exit logic uses the filled quantity rather than the ordered quantity.
- Fire an equity alert after 4:00pm ET. Confirm you can tell, the next morning, that the order was queued rather than executed at the moment of the alert.
- Reconcile daily, automatically. See below. This is the step that separates a tested system from a hopeful one.
The reconciliation check
The single most valuable thing you can run during a paper soak is a daily comparison between what your bridge believes happened and what Alpaca says happened. Most silent failures show up here first.
from datetime import datetime, timedelta, timezone
def reconcile(expected):
"""expected: {client_order_id: qty} for everything we tried to place today."""
after = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
r = requests.get(
f"{ALPACA_BASE}/orders",
params={"status": "all", "after": after, "limit": 500},
headers=ALPACA_HEADERS, timeout=15,
)
r.raise_for_status()
actual = {o["client_order_id"]: o for o in r.json()}
for coid, qty in expected.items():
o = actual.get(coid)
if o is None:
yield f"MISSING {coid}: we think we sent it; Alpaca has no record"
continue
filled = float(o["filled_qty"] or 0)
if o["status"] == "rejected":
yield f"REJECTED {coid}: wanted {qty}"
elif filled == 0:
yield f"UNFILLED {coid}: status={o['status']}"
elif filled < qty:
yield f"PARTIAL {coid}: wanted {qty}, filled {filled}"
Set your own client_order_id when you submit, so you have a key you control on
both sides of this comparison. Alpaca returns it on the order object and lets you look an
order up by it, which is what makes the join above possible. Treat it as your
bookkeeping reference rather than an instruction to the broker to deduplicate for you — the
deduplication logic needs to live in your code.
Run that for a few weeks and read the output. A clean run is a meaningful result. A run full
of MISSING lines means your bridge is losing alerts somewhere between
TradingView and the order endpoint, and you have found that out while it costs nothing.
When you do go live, change one thing at a time
The habit that survives contact with reality is boring: go live at a size small enough that being wrong is affordable, keep the paper account running the identical configuration alongside it, and compare the two. Divergence is either a real-market effect — slippage, queue position, liquidity — or a bug in the live path. Both are worth knowing about, and you can only see either if both are running.
Resist the urge to change the strategy at the same moment you change the environment. If you increase size, switch to live, and adjust a parameter in the same week, you have no way to attribute whatever happens next.
Running paper and live side by side
TradingMixer receives your TradingView or TrendSpider alert and places the corresponding order in your own Alpaca account. It is non-custodial — your money stays with your broker, and you decide what to trade. The plan includes up to 3 webhooks and 5 Alpaca accounts, and paper and live accounts count the same, so you can point one webhook at a paper account and another at a live one and run the comparison described above without paying for anything extra.
One plan, $9/month. Stocks and crypto, paper or live. Failure emails when the broker rejects an order, so a 403 or an insufficient-buying-power error reaches you instead of a log file. New positions are rate-limited to one per webhook per 15 minutes, which suits swing and position trading on hourly-or-slower charts — orders that close a position are never rate-limited.
It is not for you if you are scalping sub-minute timeframes, need a broker other than Alpaca, trade options, or want a backtesting engine. We execute the alerts you configure; we do not generate strategies, choose trades, or exercise any discretion over your account.
Start a 30-day free trialNo credit card required. Works with an Alpaca paper account, so you can watch it run before risking anything.
The short version
- Paper tests your software well and your fills poorly.
- Fills match against the real NBBO, so a wide spread can look like slippage when it is not.
- Liquidity is ignored entirely. Any size fills.
- Partial fills are injected at random 10% of the time on purpose. Handle them.
- Slippage, latency, queue position, market impact, dividends, borrow fees, regulatory fees and corporate actions are not modelled.
- Crypto fees are real and paper does not charge them — 0.25% taker at the entry tier.
- Wash-trade 403s, buying-power reservation and extended-hours rules do apply in paper. Test them there.
- Reconcile daily against the orders endpoint rather than trusting your own logs.
Trading carries a substantial risk of loss, and automation does not reduce that risk — it removes the pause in which you might have reconsidered. A strategy that looked sound on paper can lose money live, and results from a simulated environment do not indicate future results. Test on paper, start small, and only automate a strategy you already understand.
If you have not built the bridge yet, the companion piece to this one is How to Auto-Execute TradingView Alerts on Alpaca, which covers the webhook payload, a working Python receiver, and the failure cases that decide whether you can trust it.