Jaspers Hub

biseroff / alpaca

pluginReviewed★ 0↓ 3

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

5.6 KB
1import { defineSource } from '@jaspers-ai/sdk'
2import { z } from 'zod'
3import { TRADING_HOSTS } from './client'
4import { orderRow } from './rows'
5import { liveAccount, trading } from './trading'
6
7// Sources that change the account. Never run from a view's draw: only on the user's explicit ask.
8
9export const placeOrder = defineSource({
10 description:
11 'PLACE ORDER (paper by default): submits a REAL order to the configured Alpaca account, the paper account unless live_account is true. Only run when the user explicitly asks to place this order. Give qty or notional (not both). Answers the order row.',
12 hosts: TRADING_HOSTS,
13 input: z.object({
14 symbol: z.string().min(1).describe('Stock symbol or OCC option symbol.'),
15 qty: z.number().positive().optional().describe('Shares or contracts. Fractional shares only for market/day orders.'),
16 notional: z.number().positive().optional().describe('Dollar amount instead of qty (market/day orders only).'),
17 side: z.enum(['buy', 'sell']),
18 type: z.enum(['market', 'limit', 'stop', 'stop_limit', 'trailing_stop']).default('market'),
19 time_in_force: z.enum(['day', 'gtc', 'opg', 'cls', 'ioc', 'fok']).default('day'),
20 limit_price: z.number().positive().optional().describe('For limit and stop_limit.'),
21 stop_price: z.number().positive().optional().describe('For stop and stop_limit.'),
22 trail_price: z.number().positive().optional().describe('For trailing_stop: trail in dollars.'),
23 trail_percent: z.number().positive().optional().describe('For trailing_stop: trail in percent.'),
24 extended_hours: z.boolean().optional().describe('Allow pre/after-market fills (limit, day only).'),
25 client_order_id: z.string().max(128).optional().describe('Your own id for the order.'),
26 order_class: z.enum(['simple', 'bracket', 'oco', 'oto']).optional(),
27 take_profit_limit_price: z.number().positive().optional().describe('Bracket/OCO take-profit limit price.'),
28 stop_loss_stop_price: z.number().positive().optional().describe('Bracket/OCO/OTO stop-loss stop price.'),
29 stop_loss_limit_price: z.number().positive().optional().describe('Optional stop-loss limit price.'),
30 live_account: liveAccount,
31 }),
32 async run(args, ctx) {
33 if ((args.qty === undefined) === (args.notional === undefined)) throw new Error('Give exactly one of qty or notional.')
34 if ((args.type === 'limit' || args.type === 'stop_limit') && args.limit_price === undefined)
35 throw new Error(`A ${args.type} order needs limit_price.`)
36 if ((args.type === 'stop' || args.type === 'stop_limit') && args.stop_price === undefined)
37 throw new Error(`A ${args.type} order needs stop_price.`)
38 if (args.type === 'trailing_stop' && (args.trail_price === undefined) === (args.trail_percent === undefined))
39 throw new Error('A trailing_stop order needs exactly one of trail_price or trail_percent.')
40 const s = (n: number | undefined) => (n === undefined ? undefined : String(n))
41 const body: Record<string, unknown> = {
42 symbol: args.symbol.trim().toUpperCase(),
43 qty: s(args.qty),
44 notional: s(args.notional),
45 side: args.side,
46 type: args.type,
47 time_in_force: args.time_in_force,
48 limit_price: s(args.limit_price),
49 stop_price: s(args.stop_price),
50 trail_price: s(args.trail_price),
51 trail_percent: s(args.trail_percent),
52 extended_hours: args.extended_hours,
53 client_order_id: args.client_order_id,
54 order_class: args.order_class,
55 }
56 if (args.take_profit_limit_price !== undefined) body.take_profit = { limit_price: s(args.take_profit_limit_price) }
57 if (args.stop_loss_stop_price !== undefined)
58 body.stop_loss = { stop_price: s(args.stop_loss_stop_price), limit_price: s(args.stop_loss_limit_price) }
59 for (const key of Object.keys(body)) if (body[key] === undefined) delete body[key]
60 const order = (await trading(ctx, args.live_account, '/orders', { method: 'POST', body })) as Record<string, unknown>
61 return [{ account: args.live_account ? 'live' : 'paper', ...orderRow(order) }]
62 },
63})
64
65export const cancelOrder = defineSource({
66 description:
67 'CANCEL ORDER: cancels a REAL open order on the configured Alpaca account (paper unless live_account is true), by order_id, or every open order with cancel_all: true. Only run on the user\'s explicit ask. Answers a status row per order.',
68 hosts: TRADING_HOSTS,
69 input: z.object({
70 order_id: z.string().optional().describe('The order id to cancel.'),
71 cancel_all: z.boolean().default(false).describe('Cancel EVERY open order instead. Needs no order_id.'),
72 live_account: liveAccount,
73 }),
74 async run(args, ctx) {
75 const account = args.live_account ? 'live' : 'paper'
76 if (args.cancel_all) {
77 // 207 Multi-Status: one entry per open order, each with its own HTTP status.
78 const answer = ((await trading(ctx, args.live_account, '/orders', { method: 'DELETE' })) ?? []) as {
79 id?: string
80 status?: number
81 }[]
82 if (answer.length === 0) return [{ account, order_id: null, cancelled: false, http_status: 207, message: 'No open orders.' }]
83 return answer.map((r) => ({
84 account,
85 order_id: r.id ?? null,
86 cancelled: r.status === 200 || r.status === 204,
87 http_status: r.status ?? null,
88 message: r.status === 200 || r.status === 204 ? 'Cancel requested.' : 'Not cancelled.',
89 }))
90 }
91 if (!args.order_id) throw new Error('Give order_id, or cancel_all: true to cancel every open order.')
92 await trading(ctx, args.live_account, `/orders/${encodeURIComponent(args.order_id)}`, { method: 'DELETE' })
93 return [{ account, order_id: args.order_id, cancelled: true, http_status: 204, message: 'Cancel requested.' }]
94 },
95})