Jaspers Hub

biseroff / alpaca

pluginReviewed★ 0↓ 3

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

6.9 KB
1import { defineSource } from '@jaspers-ai/sdk'
2import { z } from 'zod'
3import { DATA_HOST, alpaca, paged } from './client'
4import { mid, num, parseOcc, str } from './rows'
5import { barRow, history, list, monthAgo } from './stocks'
6
7const contracts = z
8 .array(z.string().min(1))
9 .min(1)
10 .max(100)
11 .describe('OCC option symbols, like ["AAPL250117C00190000"].')
12const feed = z
13 .enum(['indicative', 'opra'])
14 .default('indicative')
15 .describe('Options feed: indicative (free, delayed/derived) or opra (real-time, needs a subscription).')
16const start = z.string().optional().describe('Start, RFC 3339 or YYYY-MM-DD.')
17const end = z.string().optional().describe('End, RFC 3339 or YYYY-MM-DD. Leave out for now.')
18
19type Fields = Record<string, unknown>
20interface OptionSnapshot {
21 latestTrade?: Fields
22 latestQuote?: Fields
23 impliedVolatility?: number
24 greeks?: Fields
25 dailyBar?: Fields
26 prevDailyBar?: Fields
27}
28
29/** One contract's snapshot as a flat row, with the OCC symbol parsed. */
30function snapshotRow(symbol: string, s: OptionSnapshot | null) {
31 const bid = num(s?.latestQuote?.bp)
32 const ask = num(s?.latestQuote?.ap)
33 return {
34 symbol,
35 ...parseOcc(symbol),
36 bid,
37 bid_size: num(s?.latestQuote?.bs),
38 ask,
39 ask_size: num(s?.latestQuote?.as),
40 mid: mid(bid, ask),
41 quote_time: str(s?.latestQuote?.t),
42 last: num(s?.latestTrade?.p),
43 last_time: str(s?.latestTrade?.t),
44 iv: num(s?.impliedVolatility),
45 delta: num(s?.greeks?.delta),
46 gamma: num(s?.greeks?.gamma),
47 theta: num(s?.greeks?.theta),
48 vega: num(s?.greeks?.vega),
49 rho: num(s?.greeks?.rho),
50 day_volume: num(s?.dailyBar?.v),
51 day_close: num(s?.dailyBar?.c),
52 prev_close: num(s?.prevDailyBar?.c),
53 }
54}
55
56export const optionBars = defineSource({
57 description: 'Historical OHLCV bars for option contracts from Alpaca. Rows: symbol, t, o, h, l, c, v, n, vw.',
58 hosts: [DATA_HOST],
59 input: z.object({
60 symbols: contracts,
61 timeframe: z.string().default('1Day').describe('Bar size: 1Min, 5Min, 15Min, 1Hour, 1Day, 1Week, 1Month.'),
62 start: start.describe('Start, RFC 3339 or YYYY-MM-DD. Defaults to 30 days ago.'),
63 end,
64 limit: z.number().int().min(1).max(10000).default(1000).describe('Most bars in all.'),
65 }),
66 async run(args, ctx) {
67 const query = { symbols: list(args.symbols), timeframe: args.timeframe, start: args.start ?? monthAgo(), end: args.end }
68 return history(ctx, '/v1beta1/options/bars', 'bars', query, args.limit, barRow)
69 },
70})
71
72export const optionTrades = defineSource({
73 description: 'Historical trades for option contracts from Alpaca. Rows: symbol, t, p (price), s (size), x (exchange), c (condition).',
74 hosts: [DATA_HOST],
75 input: z.object({
76 symbols: contracts,
77 start,
78 end,
79 limit: z.number().int().min(1).max(10000).default(1000).describe('Most trades in all.'),
80 }),
81 async run(args, ctx) {
82 const query = { symbols: list(args.symbols), start: args.start, end: args.end }
83 return history(ctx, '/v1beta1/options/trades', 'trades', query, args.limit, (t) => ({
84 t: str(t.t),
85 p: num(t.p),
86 s: num(t.s),
87 x: str(t.x),
88 c: str(t.c),
89 }))
90 },
91})
92
93export const optionLatestQuote = defineSource({
94 description:
95 'Latest quote for option contracts from Alpaca. One row per contract: symbol, underlying, expiration, type, strike, bid, bid_size, ask, ask_size, mid, time.',
96 hosts: [DATA_HOST],
97 input: z.object({ symbols: contracts, feed }),
98 async run(args, ctx) {
99 const answer = (await alpaca(ctx, {
100 host: DATA_HOST,
101 path: '/v1beta1/options/quotes/latest',
102 query: { symbols: list(args.symbols), feed: args.feed },
103 })) as { quotes?: Record<string, Fields> } | null
104 return Object.entries(answer?.quotes ?? {}).map(([symbol, q]) => {
105 const bid = num(q.bp)
106 const ask = num(q.ap)
107 return {
108 symbol,
109 ...parseOcc(symbol),
110 bid,
111 bid_size: num(q.bs),
112 ask,
113 ask_size: num(q.as),
114 mid: mid(bid, ask),
115 time: str(q.t),
116 }
117 })
118 },
119})
120
121export const optionLatestTrade = defineSource({
122 description: 'Latest trade for option contracts from Alpaca. One row per contract: symbol, underlying, expiration, type, strike, price, size, exchange, time.',
123 hosts: [DATA_HOST],
124 input: z.object({ symbols: contracts, feed }),
125 async run(args, ctx) {
126 const answer = (await alpaca(ctx, {
127 host: DATA_HOST,
128 path: '/v1beta1/options/trades/latest',
129 query: { symbols: list(args.symbols), feed: args.feed },
130 })) as { trades?: Record<string, Fields> } | null
131 return Object.entries(answer?.trades ?? {}).map(([symbol, t]) => ({
132 symbol,
133 ...parseOcc(symbol),
134 price: num(t.p),
135 size: num(t.s),
136 exchange: str(t.x),
137 time: str(t.t),
138 }))
139 },
140})
141
142export const optionSnapshots = defineSource({
143 description:
144 'Snapshots of option contracts from Alpaca, one row per contract: symbol, underlying, expiration, type, strike, bid, ask, mid, last, iv, delta, gamma, theta, vega, rho.',
145 hosts: [DATA_HOST],
146 input: z.object({ symbols: contracts, feed }),
147 async run(args, ctx) {
148 const { data } = await paged(ctx, '/v1beta1/options/snapshots', { symbols: list(args.symbols), feed: args.feed }, 'snapshots', 1000, 1000)
149 return Object.entries(data).map(([symbol, s]) => snapshotRow(symbol, s as OptionSnapshot | null))
150 },
151})
152
153export const optionChain = defineSource({
154 description:
155 'The option chain of one underlying from Alpaca: one row per contract with symbol, underlying, expiration, type, strike, bid, ask, mid, last, iv, delta, gamma, theta, vega, rho. Sorted by expiration, strike, type. Narrow it with type, strikes, and expirations.',
156 hosts: [DATA_HOST],
157 input: z.object({
158 underlying: z.string().min(1).describe('The underlying stock symbol, like AAPL.'),
159 feed,
160 type: z.enum(['call', 'put']).optional().describe('Only calls or only puts.'),
161 strike_price_gte: z.number().optional().describe('Lowest strike.'),
162 strike_price_lte: z.number().optional().describe('Highest strike.'),
163 expiration_date: z.string().optional().describe('Exactly this expiration, YYYY-MM-DD.'),
164 expiration_date_gte: z.string().optional().describe('Expirations on or after, YYYY-MM-DD.'),
165 expiration_date_lte: z.string().optional().describe('Expirations on or before, YYYY-MM-DD.'),
166 limit: z.number().int().min(1).max(5000).default(500).describe('Most contracts.'),
167 }),
168 async run(args, ctx) {
169 const { underlying, limit, ...filters } = args
170 const path = `/v1beta1/options/snapshots/${encodeURIComponent(underlying.trim().toUpperCase())}`
171 const { data, truncated } = await paged(ctx, path, filters, 'snapshots', limit, 1000)
172 const rows = Object.entries(data).map(([symbol, s]) => snapshotRow(symbol, s as OptionSnapshot | null))
173 rows.sort(
174 (a, b) =>
175 (a.expiration ?? '').localeCompare(b.expiration ?? '') ||
176 (a.strike ?? 0) - (b.strike ?? 0) ||
177 (a.type ?? '').localeCompare(b.type ?? ''),
178 )
179 return { rows: rows.slice(0, limit), truncated }
180 },
181})