Examples
Runnable, end-to-end examples for @alpacahq/alpaca-trade-api, generated from the
examples/ directory so the code below always matches the repo.
The examples import from the local source (../src) so they run straight from a
clone; in your own app the import is simply from "@alpacahq/alpaca-trade-api"
(shown in a comment at the top of each file). They are type-checked against the
current SDK source as part of npm run typecheck, so they can't silently drift
out of sync with the API.
Prerequisites
- Node.js ≥ 20
- Paper-trading API credentials from https://app.alpaca.markets/paper/dashboard/overview
tsxto run TypeScript directly (npx tsx ...will fetch it on first use)
Export your credentials once:
export APCA_API_KEY_ID="your-key-id"
export APCA_API_SECRET_KEY="your-secret"
trading-bot.ts
Minimal paper trading bot.
Demonstrates: the Alpaca facade, reading the account (with the values
money helpers), a market-data lookup, the ergonomic order builders
(orders.limit), the trade-updates stream (with the awaitable auth handshake
and reconnect-lifecycle listeners), submitAndWait (place an order and block
until it reaches a terminal state), and typed-error handling branching on the
ApiError subclasses.
APCA_API_KEY_ID=... APCA_API_SECRET_KEY=... npx tsx examples/trading-bot.ts
// In your own app this import is just:
// import { Alpaca, ApiError, FetchError, RateLimitError, values } from "@alpacahq/alpaca-trade-api";
import { Alpaca, ApiError, FetchError, RateLimitError, values } from "../src/index";
async function main(): Promise<void> {
const keyId = process.env.APCA_API_KEY_ID;
const secret = process.env.APCA_API_SECRET_KEY;
if (!keyId || !secret) {
console.error("Set APCA_API_KEY_ID and APCA_API_SECRET_KEY in the environment.");
process.exit(1);
}
const alpaca = new Alpaca({
keyId,
secret,
paper: true,
timeoutMs: 10_000,
retry: { maxRetries: 3 }, // covers transient 5xx and network errors on safe GETs, never order POSTs
});
// Money/quantity fields are wire-truthful strings; format them for display
// with the `values` helpers instead of printing the raw string.
const account = await alpaca.trading.account.getAccount();
console.log(
`account ${account.accountNumber} status=${account.status} ` +
`buyingPower=${values.formatMoney(account.buyingPower)} ` +
`equity=${values.formatMoney(account.equity)}`,
);
const price = await alpaca.marketData.getLatestPrice("AAPL");
console.log(`AAPL last trade: ${price ?? "n/a"}`);
// Stream order/account updates in the background.
const updates = alpaca.trading.stream();
updates.onTradeUpdate((u) => console.log(`trade update: ${u.event} ${u.order.symbol} -> ${u.order.status}`));
updates.onError((msg) => console.error("stream error:", msg));
// Observe the reconnect lifecycle (auto-reconnect with backoff is built in).
updates.onReconnecting((attempt) => console.warn(`stream reconnecting (attempt ${attempt})`));
// This means re-subscription was dispatched, not acknowledged by the server.
updates.onReconnected(() => console.info("stream reconnected; subscriptions dispatched"));
updates.onConnect(() => updates.subscribeTradeUpdates());
updates.connect();
// Await the authentication handshake (typed result; never throws). Bail out
// early on bad credentials instead of placing orders against a dead stream.
const auth = await updates.waitForAuthenticationResult(10_000);
if (!auth.authenticated) {
console.error(`trade-updates stream auth ${auth.status}: ${auth.message}${auth.code ? ` (code ${auth.code})` : ""}`);
updates.disconnect();
process.exit(1);
}
// A stable, unique client ID makes this placement auditable and provides
// the reconciliation key if the transport fails after Alpaca receives it.
const restingClientOrderId = newClientOrderId("resting-limit-aapl");
// Ergonomic order builder: a limit buy well below the market rests without
// filling. The typed `orders.limit` builder requires `limitPrice` at compile
// time and accepts `number | string` amounts. We place then cancel it to
// show both the builder and a raw generated method (`deleteOrderByOrderID`).
try {
let resting: Awaited<ReturnType<typeof alpaca.trading.orders.limit>>;
try {
resting = await alpaca.trading.orders.limit({
symbol: "AAPL",
qty: 1,
side: "buy",
limitPrice: Math.max(1, Math.floor((price ?? 100) * 0.5)),
clientOrderId: restingClientOrderId,
});
} catch (err) {
if (!(err instanceof FetchError)) throw err;
// The POST outcome is ambiguous. Reconcile by client ID before any
// further submission; a failed lookup is not proof that placement
// failed, so this example stops instead of risking another order.
try {
resting = await alpaca.trading.orders.getOrderByClientOrderId({
clientOrderId: restingClientOrderId,
});
console.warn(`reconciled ambiguous placement as order ${resting.id}`);
} catch (lookupError) {
reportError("limit order reconciliation", lookupError);
updates.disconnect();
return;
}
}
console.log(`placed resting limit order ${resting.id} @ ${resting.limitPrice}`);
if (resting.id) {
await alpaca.trading.orders.deleteOrderByOrderID({ orderId: resting.id });
console.log(`canceled ${resting.id}`);
}
} catch (err) {
reportError("limit order", err);
}
// `submitAndWait` waits for Alpaca's listening acknowledgement, places once
// per invocation, and never re-places on reconnect. It preserves this
// client ID and uses one deadline for subscription, REST, and terminal wait.
// Only this workflow makes one client-ID GET after an ambiguous FetchError;
// the generic order builders require the explicit recovery shown above.
try {
const order = await alpaca.trading.submitAndWait(
{
type: "market",
symbol: "AAPL",
qty: 1,
side: "buy",
clientOrderId: newClientOrderId("market-aapl"),
},
{ timeoutMs: 30_000, stream: updates },
);
console.log(`order ${order.id} reached ${order.status} (filledAvgPrice=${order.filledAvgPrice ?? "n/a"})`);
} catch (err) {
reportError("submitAndWait", err);
} finally {
updates.disconnect();
}
}
/** Human-readable strategy prefix plus collision-resistant Node 20 UUID. */
function newClientOrderId(purpose: string): string {
return `${purpose}-${globalThis.crypto.randomUUID()}`;
}
/** Branch on the typed-error subclasses; always log the request id on an ApiError. */
function reportError(label: string, err: unknown): void {
if (err instanceof RateLimitError) {
console.error(`${label} rate limited; retry in ${err.retryAfterMs ?? "?"}ms (request ${err.requestId})`);
} else if (err instanceof ApiError) {
console.error(`${label} rejected: HTTP ${err.status} ${err.code ?? ""} ${err.message} (request ${err.requestId})`);
} else {
console.error(`${label} failed:`, (err as Error).message);
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
marketdata-backend.ts
Minimal market-data backend for a visualization frontend.
- GET /stream Server-Sent Events of live bars for the configured symbols.
- GET /price?symbol=AAPL Latest trade price (REST).
- GET /bars?symbol=AAPL Historical bars as canonical
Bars (REST, auto-paginated). - GET /candles?symbol=AAPL Historical bars as chart-ready columnar
Candles.
Demonstrates: a long-lived market-data WebSocket (with reconnect-lifecycle
listeners) fanned out to many HTTP clients, plus REST helpers and
auto-pagination - no extra web framework. The live /stream bars and the
historical /bars share ONE shape (Bar), so a frontend can backfill history
then append live updates without remapping.
Upstream failures are surfaced as typed ApiErrors, mapped to the right HTTP
status with Alpaca's request id for debugging.
APCA_API_KEY_ID=... APCA_API_SECRET_KEY=... npx tsx examples/marketdata-backend.ts
curl -N http://localhost:8080/stream
curl http://localhost:8080/price?symbol=AAPL
import * as http from "node:http";
// In your own app this import is just: import { Alpaca, TimeFrame, ApiError } from "@alpacahq/alpaca-trade-api";
import { Alpaca, TimeFrame, ApiError } from "../src/index";
const keyId = process.env.APCA_API_KEY_ID;
const secret = process.env.APCA_API_SECRET_KEY;
if (!keyId || !secret) {
console.error("Set APCA_API_KEY_ID and APCA_API_SECRET_KEY in the environment.");
process.exit(1);
}
const PORT = Number(process.env.PORT ?? 8080);
const SYMBOLS = (process.env.SYMBOLS ?? "AAPL,MSFT").split(",");
const alpaca = new Alpaca({ keyId, secret });
// Fan live bars out to every connected SSE client.
const clients = new Set<http.ServerResponse>();
const stream = alpaca.marketData.stockStream({ feed: "iex" });
stream.onBar((bar) => {
const frame = `data: ${JSON.stringify(bar)}\n\n`;
for (const res of clients) res.write(frame);
});
stream.onError((msg) => console.error("stream error:", msg));
// The client auto-reconnects with backoff and dispatches subscriptions again
// after authentication; onReconnected does not imply server acknowledgement.
stream.onReconnecting((attempt) => console.warn(`market-data stream reconnecting (attempt ${attempt})`));
stream.onReconnected(() => console.info("market-data stream reconnected; subscriptions dispatched"));
stream.onConnect(() => stream.subscribeForBars(SYMBOLS));
stream.connect();
function sendJson(res: http.ServerResponse, status: number, body: unknown): void {
res.writeHead(status, { "Content-Type": "application/json" });
res.end(JSON.stringify(body));
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url ?? "/", `http://localhost:${PORT}`);
try {
if (url.pathname === "/stream") {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
res.write(": connected\n\n");
clients.add(res);
req.on("close", () => clients.delete(res));
return;
}
if (url.pathname === "/price") {
const symbol = url.searchParams.get("symbol") ?? SYMBOLS[0];
const price = await alpaca.marketData.getLatestPrice(symbol);
sendJson(res, 200, { symbol, price });
return;
}
if (url.pathname === "/bars") {
const symbol = url.searchParams.get("symbol") ?? SYMBOLS[0];
// Canonical Bar[] - the same shape the /stream bars arrive in.
const bars = await alpaca.marketData.getStockBars({
symbols: [symbol],
timeframe: TimeFrame.Day,
start: new Date(url.searchParams.get("start") ?? "2024-01-01"),
});
sendJson(res, 200, bars);
return;
}
if (url.pathname === "/candles") {
const symbol = url.searchParams.get("symbol") ?? SYMBOLS[0];
// Columnar { time[], open[], high[], low[], close[], volume[] } for charts.
const candles = await alpaca.marketData.getStockCandles({
symbols: [symbol],
timeframe: TimeFrame.Day,
start: new Date(url.searchParams.get("start") ?? "2024-01-01"),
});
sendJson(res, 200, candles);
return;
}
sendJson(res, 404, { error: "not found" });
} catch (err) {
// Map an upstream Alpaca failure to its real status, and surface the
// request id so the failure is traceable in Alpaca's systems.
if (err instanceof ApiError) {
sendJson(res, err.status, { error: err.message, code: err.code, requestId: err.requestId });
} else {
sendJson(res, 500, { error: (err as Error).message });
}
}
});
server.listen(PORT, () => {
console.log(`listening on http://localhost:${PORT} (streaming bars for ${SYMBOLS.join(", ")})`);
});