Alpaca: Alpaca market data, account, positions, and order management, with watchlist, account, and order-blotter views.
| 1 | import type { BackendContext } from '@jaspers-ai/sdk' |
| 2 | import { defineSource } from '@jaspers-ai/sdk' |
| 3 | import { z } from 'zod' |
| 4 | import { LIVE_HOST, PAPER_HOST, TRADING_HOSTS, alpaca, type Query, type Request } from './client' |
| 5 | import { num, orderRow, str } from './rows' |
| 6 | |
| 7 | /** The one way to reach the live account. Shared by every trading source. */ |
| 8 | export const liveAccount = z |
| 9 | .boolean() |
| 10 | .default(false) |
| 11 | .describe('LIVE (real-money) account: true uses api.alpaca.markets with the live keys. Default false: the paper account.') |
| 12 | |
| 13 | /** A Trading API call on the paper host, or the live host and keys when `live` is true. */ |
| 14 | export function trading( |
| 15 | ctx: BackendContext, |
| 16 | live: boolean, |
| 17 | path: string, |
| 18 | rest: Omit<Request, 'host' | 'path' | 'live'> = {}, |
| 19 | ): Promise<unknown> { |
| 20 | return alpaca(ctx, { ...rest, host: live ? LIVE_HOST : PAPER_HOST, path: `/v2${path}`, live }) |
| 21 | } |
| 22 | |
| 23 | export const account = defineSource({ |
| 24 | description: |
| 25 | 'The Alpaca trading account (paper unless live_account is true), one row: mode (paper|live), account_number, status, currency, cash, buying_power, equity, last_equity, day_pl, day_pl_pct, portfolio_value, long_market_value, short_market_value, daytrade_count, pattern_day_trader, trading_blocked.', |
| 26 | hosts: TRADING_HOSTS, |
| 27 | input: z.object({ live_account: liveAccount }), |
| 28 | async run(args, ctx) { |
| 29 | const a = (await trading(ctx, args.live_account, '/account')) as Record<string, unknown> |
| 30 | const equity = num(a.equity) |
| 31 | const lastEquity = num(a.last_equity) |
| 32 | const dayPl = equity !== null && lastEquity !== null ? Math.round((equity - lastEquity) * 100) / 100 : null |
| 33 | return [ |
| 34 | { |
| 35 | mode: args.live_account ? 'live' : 'paper', |
| 36 | account_number: str(a.account_number), |
| 37 | status: str(a.status), |
| 38 | currency: str(a.currency), |
| 39 | cash: num(a.cash), |
| 40 | buying_power: num(a.buying_power), |
| 41 | regt_buying_power: num(a.regt_buying_power), |
| 42 | daytrading_buying_power: num(a.daytrading_buying_power), |
| 43 | equity, |
| 44 | last_equity: lastEquity, |
| 45 | day_pl: dayPl, |
| 46 | day_pl_pct: dayPl !== null && lastEquity ? Math.round((dayPl / lastEquity) * 1_000_000) / 10000 : null, |
| 47 | portfolio_value: num(a.portfolio_value), |
| 48 | long_market_value: num(a.long_market_value), |
| 49 | short_market_value: num(a.short_market_value), |
| 50 | multiplier: num(a.multiplier), |
| 51 | daytrade_count: num(a.daytrade_count), |
| 52 | pattern_day_trader: Boolean(a.pattern_day_trader), |
| 53 | trading_blocked: Boolean(a.trading_blocked), |
| 54 | account_blocked: Boolean(a.account_blocked), |
| 55 | shorting_enabled: Boolean(a.shorting_enabled), |
| 56 | options_trading_level: num(a.options_trading_level), |
| 57 | created_at: str(a.created_at), |
| 58 | }, |
| 59 | ] |
| 60 | }, |
| 61 | }) |
| 62 | |
| 63 | export const positions = defineSource({ |
| 64 | description: |
| 65 | 'Open positions on the Alpaca account (paper unless live_account). Rows: symbol, qty, side, avg_entry_price, current_price, market_value, cost_basis, unrealized_pl, unrealized_plpc, unrealized_intraday_pl, change_today, asset_class.', |
| 66 | hosts: TRADING_HOSTS, |
| 67 | input: z.object({ live_account: liveAccount }), |
| 68 | async run(args, ctx) { |
| 69 | const list = ((await trading(ctx, args.live_account, '/positions')) ?? []) as Record<string, unknown>[] |
| 70 | return list.map((p) => ({ |
| 71 | symbol: str(p.symbol), |
| 72 | asset_class: str(p.asset_class), |
| 73 | exchange: str(p.exchange), |
| 74 | side: str(p.side), |
| 75 | qty: num(p.qty), |
| 76 | qty_available: num(p.qty_available), |
| 77 | avg_entry_price: num(p.avg_entry_price), |
| 78 | current_price: num(p.current_price), |
| 79 | lastday_price: num(p.lastday_price), |
| 80 | market_value: num(p.market_value), |
| 81 | cost_basis: num(p.cost_basis), |
| 82 | unrealized_pl: num(p.unrealized_pl), |
| 83 | unrealized_plpc: num(p.unrealized_plpc), |
| 84 | unrealized_intraday_pl: num(p.unrealized_intraday_pl), |
| 85 | change_today: num(p.change_today), |
| 86 | })) |
| 87 | }, |
| 88 | }) |
| 89 | |
| 90 | export const orders = defineSource({ |
| 91 | description: |
| 92 | 'Orders on the Alpaca account (paper unless live_account), read only. Rows: id, client_order_id, symbol, side, type, time_in_force, qty, notional, filled_qty, limit_price, stop_price, filled_avg_price, status, submitted_at, filled_at, canceled_at, order_class, parent_id (set on bracket legs when nested).', |
| 93 | hosts: TRADING_HOSTS, |
| 94 | input: z.object({ |
| 95 | status: z.enum(['open', 'closed', 'all']).default('all').describe('Which orders.'), |
| 96 | limit: z.number().int().min(1).max(500).default(100).describe('Most orders.'), |
| 97 | after: z.string().optional().describe('Only orders submitted after this time, RFC 3339.'), |
| 98 | until: z.string().optional().describe('Only orders submitted until this time, RFC 3339.'), |
| 99 | direction: z.enum(['asc', 'desc']).default('desc').describe('Time order.'), |
| 100 | symbols: z.array(z.string()).optional().describe('Only these symbols.'), |
| 101 | nested: z.boolean().default(false).describe('Nest bracket/OCO legs; legs are added as rows after their parent.'), |
| 102 | live_account: liveAccount, |
| 103 | }), |
| 104 | async run(args, ctx) { |
| 105 | const query: Query = { |
| 106 | status: args.status, |
| 107 | limit: args.limit, |
| 108 | after: args.after, |
| 109 | until: args.until, |
| 110 | direction: args.direction, |
| 111 | nested: args.nested, |
| 112 | symbols: args.symbols?.map((s) => s.trim().toUpperCase()).join(','), |
| 113 | } |
| 114 | const list = ((await trading(ctx, args.live_account, '/orders', { query })) ?? []) as Record<string, unknown>[] |
| 115 | const rows: Record<string, unknown>[] = [] |
| 116 | for (const o of list) { |
| 117 | rows.push({ ...orderRow(o), parent_id: null }) |
| 118 | for (const leg of (o.legs ?? []) as Record<string, unknown>[]) rows.push({ ...orderRow(leg), parent_id: str(o.id) }) |
| 119 | } |
| 120 | return rows |
| 121 | }, |
| 122 | }) |
| 123 | |
| 124 | const assetRow = (a: Record<string, unknown>) => ({ |
| 125 | symbol: str(a.symbol), |
| 126 | name: str(a.name), |
| 127 | class: str(a.class), |
| 128 | exchange: str(a.exchange), |
| 129 | status: str(a.status), |
| 130 | tradable: Boolean(a.tradable), |
| 131 | marginable: Boolean(a.marginable), |
| 132 | shortable: Boolean(a.shortable), |
| 133 | easy_to_borrow: Boolean(a.easy_to_borrow), |
| 134 | fractionable: Boolean(a.fractionable), |
| 135 | id: str(a.id), |
| 136 | }) |
| 137 | |
| 138 | export const assets = defineSource({ |
| 139 | description: |
| 140 | 'Tradable assets on Alpaca: one asset by symbol, or a list filtered by status, class, and exchange. Rows: symbol, name, class, exchange, status, tradable, marginable, shortable, easy_to_borrow, fractionable.', |
| 141 | hosts: TRADING_HOSTS, |
| 142 | input: z.object({ |
| 143 | symbol: z.string().optional().describe('One asset, by symbol (or OCC symbol). Leave out for a list.'), |
| 144 | status: z.enum(['active', 'inactive']).default('active'), |
| 145 | asset_class: z.enum(['us_equity', 'us_option', 'crypto']).default('us_equity'), |
| 146 | exchange: z.string().optional().describe('Like NASDAQ, NYSE, ARCA, AMEX, BATS, OTC.'), |
| 147 | limit: z.number().int().min(1).max(50000).default(500).describe('Most rows of a list.'), |
| 148 | live_account: liveAccount, |
| 149 | }), |
| 150 | async run(args, ctx) { |
| 151 | if (args.symbol) { |
| 152 | const one = (await trading(ctx, args.live_account, `/assets/${encodeURIComponent(args.symbol.trim().toUpperCase())}`)) as Record<string, unknown> |
| 153 | return [assetRow(one)] |
| 154 | } |
| 155 | const query: Query = { status: args.status, asset_class: args.asset_class, exchange: args.exchange } |
| 156 | const list = ((await trading(ctx, args.live_account, '/assets', { query })) ?? []) as Record<string, unknown>[] |
| 157 | return { rows: list.slice(0, args.limit).map(assetRow), total: list.length } |
| 158 | }, |
| 159 | }) |