Alpaca: Alpaca market data, account, positions, and order management, with watchlist, account, and order-blotter views.
| 1 | import { defineSource } from '@jaspers-ai/sdk' |
| 2 | import { z } from 'zod' |
| 3 | import { TRADING_HOSTS } from './client' |
| 4 | import { orderRow } from './rows' |
| 5 | import { 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 | |
| 9 | export 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 | |
| 65 | export 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 | }) |