Tutorial
Your First Live Trade via API — A CLI Tutorial with Alpaca
Every systematic strategy is one abstraction layer above what this article covers. Before adding signal logic, position sizing, or risk management — understand the execution layer by itself. What happens between "place order" and "trade confirmed" is not a black box; it is a deterministic sequence of API calls, validation checks, and state transitions. Knowing this sequence prevents a category of production errors that no amount of backtesting would catch — orders rejected for reasons the strategy code never anticipated, positions left open because a cancellation call was never made, fills at unexpected prices because time-in-force was set incorrectly.
Prerequisites
This article builds directly on the environment from Setting Up Your Trading Development Environment. You need Python 3.11, alpaca-py installed in a virtual environment, and a .env file containing valid paper trading API credentials.
If you have not completed that setup, do it first. The code here will not run without those foundations. Specifically, the load_dotenv() call in Step 1 depends on the .env file existing in the working directory with the correct key names.
Step 1 — Initialize the Trading Client
The TradingClient is the gateway to all order and account operations. It authenticates using the credentials from your .env file and routes requests to either the paper or live trading environment based on a single parameter:
import os
from dotenv import load_dotenv
from alpaca.trading.client import TradingClient
load_dotenv()
client = TradingClient(
api_key=os.getenv("ALPACA_API_KEY"),
secret_key=os.getenv("ALPACA_SECRET_KEY"),
paper=True # set to False for live trading
)
The paper=True flag routes all requests to https://paper-api.alpaca.markets. When you are ready to trade with real capital, the only change is paper=False — the client switches the base URL to https://api.alpaca.markets. The code is otherwise identical. This is intentional: the paper environment is designed to behave like the live environment so that the switch from paper to live is not a rewrite, not even a refactor.
Every method call on this client — account reads, order submissions, position queries, cancellations — follows the same authentication pattern. The client handles token injection into every request header transparently.
Step 2 — Inspect Account State
Before placing any order, read the current account state. A healthy account read is also a confirmation that authentication succeeded. If this call raises an exception, there is no point proceeding to order submission:
account = client.get_account()
print(f"Status: {account.status}")
print(f"Buying power: ${float(account.buying_power):,.2f}")
print(f"Portfolio value: ${float(account.portfolio_value):,.2f}")
print(f"Day trade count: {account.daytrade_count}")
The daytrade_count field matters more than it looks. Pattern day trader (PDT) rules apply to both paper and live accounts: more than 3 day trades within 5 rolling trading days on an account under $25,000 triggers PDT classification. When that happens, the account is restricted from opening new day trades until the 5-day window clears or the account balance crosses $25,000. Paper trading with the same rules enforced means you learn the constraint before it costs you real money.
A day trade is defined as opening and closing the same position on the same trading day. Buying AAPL at 10am and selling it at 2pm is one day trade. Buying AAPL at 10am and holding through the close, then selling the following morning, is not. For strategies that intend to hold positions overnight, PDT is not a concern. For intraday strategies — particularly ones that enter and exit multiple times per session — the day trade count requires active tracking.
Step 3 — Check Current Positions
Before submitting an order, verify what positions are already open. Placing a buy order on a symbol you already hold adds to the position; it does not replace it. For strategies that manage a target allocation — "hold exactly 100 shares of AAPL" — the buy quantity must account for the shares already owned:
positions = client.get_all_positions()
if not positions:
print("No open positions")
else:
for p in positions:
print(
f"{p.symbol}: {p.qty} shares @ ${float(p.avg_entry_price):.2f} "
f"| P&L: ${float(p.unrealized_pl):.2f}"
)
The avg_entry_price is the volume-weighted average price across all fills that contributed to this position. If you bought 5 shares at $180 and 5 more at $190, the entry price is $185. The unrealized_pl is the current market value minus the cost basis — it updates in real time during market hours.
For a freshly created paper account, get_all_positions() returns an empty list. That is the expected state before placing the first order.
Step 4 — Place a Market Order
A market order executes at the best available price when the order reaches the exchange. It is the most direct order type: you specify a symbol, a quantity, a direction (buy or sell), and a time-in-force rule:
from alpaca.trading.requests import MarketOrderRequest
from alpaca.trading.enums import OrderSide, TimeInForce
order_request = MarketOrderRequest(
symbol="AAPL",
qty=1,
side=OrderSide.BUY,
time_in_force=TimeInForce.DAY
)
order = client.submit_order(order_request)
print(f"Order ID: {order.id}")
print(f"Status: {order.status}")
print(f"Symbol: {order.symbol}")
print(f"Qty: {order.qty}")
print(f"Side: {order.side}")
TimeInForce.DAY instructs the exchange to cancel the order automatically if it has not filled by the end of the trading session. Use TimeInForce.GTC (good-till-cancelled) only when you explicitly want the order to persist across multiple trading sessions. For intraday strategies, DAY is the correct default — it prevents stale orders from executing unexpectedly the following morning when market conditions are entirely different.
The order.id returned here is the reference you use for all subsequent operations on this order: status checks, cancellations, and audit logging. Store it if your code needs to track this order after the initial submission returns.
The initial status returned by submit_order() will typically be new — the order has been received by Alpaca but has not yet been sent to the exchange for execution. The transition to filled happens within milliseconds during market hours.
Step 5 — Check Order Status
Market orders placed during regular trading hours (9:30am–4:00pm Eastern) fill within milliseconds in most cases. Verify the fill:
import time
time.sleep(1) # allow time for fill confirmation to propagate
filled_order = client.get_order_by_id(order.id)
print(f"Status: {filled_order.status}")
if filled_order.filled_avg_price:
print(f"Fill price: ${float(filled_order.filled_avg_price):.2f}")
print(f"Filled qty: {filled_order.filled_qty}")
else:
print("Not yet filled")
Order status values you will encounter:
new— received by Alpaca's order management system, not yet sent to the exchangepartially_filled— some but not all of the requested quantity has executedfilled— complete fill, all shares executed at the reported average pricecanceled— order was canceled, either by you or by the exchange at session endexpired— a GTC order that was not filled before its expiration daterejected— Alpaca's risk checks blocked the order before it reached the exchange
A rejected status is the most important one to handle in production code. A rejection means the order never reached the exchange — no fill, no position change. Common causes: insufficient buying power, market is closed and the order type does not permit extended-hours execution, invalid symbol, or a PDT restriction that is currently active on the account. In any automated strategy, a rejected order must trigger an alert rather than a silent no-op.
Step 6 — Close the Position
Closing a position is the functional inverse of opening one. The API provides a high-level method that handles the order construction internally:
# Close a specific position by symbol
client.close_position("AAPL")
# Close all open positions at once (use deliberately in live trading)
# client.close_all_positions()
Calling close_position("AAPL") submits a market sell order for the full quantity currently held in that symbol. The return value is an Order object, identical to the one returned by submit_order(). You can use that object to track the closing fill exactly as you tracked the opening fill in Step 5.
close_all_positions() is useful in testing scenarios and for emergency risk-off situations — a single call that exits everything. In production code, be deliberate about which positions you close and when. An accidental close_all_positions() call in a multi-strategy context could close positions that belong to strategies that did not intend to exit.
Step 7 — Cancel Pending Orders
If an order is still in new or partially_filled status — for example, a limit order that has not yet reached its target price, or a market order placed outside trading hours — cancel it explicitly:
# Cancel a specific order by ID
client.cancel_order_by_id(order.id)
# Cancel all open orders
client.cancel_orders()
Outside of market hours, market orders enter a pending queue and will execute at the open of the next session unless explicitly canceled. If your strategy does not intend to carry orders overnight, the last action before shutdown should be client.cancel_orders(). This is a defensive pattern — TimeInForce.DAY already handles session-end cancellation for DAY orders, but GTC orders and any order submitted in the minutes before close require explicit handling.
Building order cleanup into the shutdown sequence of every strategy — not as an afterthought but as a required step — is one of the simplest ways to prevent a class of production incidents where stale orders execute at the wrong time, under the wrong market conditions, at sizes that no longer reflect the strategy's intended exposure.
Understanding the Execution Flow
When submit_order() is called, the following sequence occurs:
- The request hits Alpaca's order management system (OMS), which validates the order structure and authentication
- Alpaca runs risk checks — buying power, PDT status, position limits, symbol validity, market hours eligibility
- If checks pass, the order is routed to an exchange or a dark pool (an alternative trading venue that matches orders off-exchange, typically providing better fill prices for small retail orders)
- A fill report is returned from the exchange to Alpaca's OMS
- Alpaca updates the account's position state and available buying power
- The fill becomes visible via
get_order_by_id()and is reflected inget_all_positions()
For paper trading, Alpaca simulates this sequence using real-time market data. Fills execute at the current bid/ask with a small simulated slippage component. The fills are realistic but not real — they do not affect the actual market, and the simulated slippage is typically lower than what a live order would experience in practice. The practical implication: paper trading results tend to be slightly better than live trading results, particularly for larger order sizes and less liquid symbols. Treat paper trading as a functional integration test, not a performance benchmark. Its value is in confirming that the API interaction sequence works correctly end-to-end, not in predicting exact fill quality for live deployment.
The Oyamori Approach
This execution layer — authentication, order placement, fill confirmation, position tracking, order cancellation — is handled by Oyamori's execution engine for every strategy in the catalog. The developer's job is to configure strategy parameters; the platform manages the full API interaction cycle on each signal. Understanding what the engine does on each trade is what allows you to debug it when behavior deviates from expectation.