Jaspers Hub

biseroff / alpaca

pluginReviewed★ 0↓ 3

Alpaca: Alpaca market data, account, positions, and order management, with watchlist, account, and order-blotter views.

7.2 KB
1import type { BackendContext } from '@jaspers-ai/sdk'
2import { defineSource } from '@jaspers-ai/sdk'
3import { z } from 'zod'
4import { LIVE_HOST, PAPER_HOST, TRADING_HOSTS, alpaca, type Query, type Request } from './client'
5import { num, orderRow, str } from './rows'
6
7/** The one way to reach the live account. Shared by every trading source. */
8export 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. */
14export 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
23export 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
63export 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
90export 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
124const 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
138export 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})