import { defineSource } from '@jaspers-ai/sdk' import { z } from 'zod' import { TRADING_HOSTS } from './client' import { orderRow } from './rows' import { liveAccount, trading } from './trading' // Sources that change the account. Never run from a view's draw: only on the user's explicit ask. export const placeOrder = defineSource({ description: '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.', hosts: TRADING_HOSTS, input: z.object({ symbol: z.string().min(1).describe('Stock symbol or OCC option symbol.'), qty: z.number().positive().optional().describe('Shares or contracts. Fractional shares only for market/day orders.'), notional: z.number().positive().optional().describe('Dollar amount instead of qty (market/day orders only).'), side: z.enum(['buy', 'sell']), type: z.enum(['market', 'limit', 'stop', 'stop_limit', 'trailing_stop']).default('market'), time_in_force: z.enum(['day', 'gtc', 'opg', 'cls', 'ioc', 'fok']).default('day'), limit_price: z.number().positive().optional().describe('For limit and stop_limit.'), stop_price: z.number().positive().optional().describe('For stop and stop_limit.'), trail_price: z.number().positive().optional().describe('For trailing_stop: trail in dollars.'), trail_percent: z.number().positive().optional().describe('For trailing_stop: trail in percent.'), extended_hours: z.boolean().optional().describe('Allow pre/after-market fills (limit, day only).'), client_order_id: z.string().max(128).optional().describe('Your own id for the order.'), order_class: z.enum(['simple', 'bracket', 'oco', 'oto']).optional(), take_profit_limit_price: z.number().positive().optional().describe('Bracket/OCO take-profit limit price.'), stop_loss_stop_price: z.number().positive().optional().describe('Bracket/OCO/OTO stop-loss stop price.'), stop_loss_limit_price: z.number().positive().optional().describe('Optional stop-loss limit price.'), live_account: liveAccount, }), async run(args, ctx) { if ((args.qty === undefined) === (args.notional === undefined)) throw new Error('Give exactly one of qty or notional.') if ((args.type === 'limit' || args.type === 'stop_limit') && args.limit_price === undefined) throw new Error(`A ${args.type} order needs limit_price.`) if ((args.type === 'stop' || args.type === 'stop_limit') && args.stop_price === undefined) throw new Error(`A ${args.type} order needs stop_price.`) if (args.type === 'trailing_stop' && (args.trail_price === undefined) === (args.trail_percent === undefined)) throw new Error('A trailing_stop order needs exactly one of trail_price or trail_percent.') const s = (n: number | undefined) => (n === undefined ? undefined : String(n)) const body: Record = { symbol: args.symbol.trim().toUpperCase(), qty: s(args.qty), notional: s(args.notional), side: args.side, type: args.type, time_in_force: args.time_in_force, limit_price: s(args.limit_price), stop_price: s(args.stop_price), trail_price: s(args.trail_price), trail_percent: s(args.trail_percent), extended_hours: args.extended_hours, client_order_id: args.client_order_id, order_class: args.order_class, } if (args.take_profit_limit_price !== undefined) body.take_profit = { limit_price: s(args.take_profit_limit_price) } if (args.stop_loss_stop_price !== undefined) body.stop_loss = { stop_price: s(args.stop_loss_stop_price), limit_price: s(args.stop_loss_limit_price) } for (const key of Object.keys(body)) if (body[key] === undefined) delete body[key] const order = (await trading(ctx, args.live_account, '/orders', { method: 'POST', body })) as Record return [{ account: args.live_account ? 'live' : 'paper', ...orderRow(order) }] }, }) export const cancelOrder = defineSource({ description: '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.', hosts: TRADING_HOSTS, input: z.object({ order_id: z.string().optional().describe('The order id to cancel.'), cancel_all: z.boolean().default(false).describe('Cancel EVERY open order instead. Needs no order_id.'), live_account: liveAccount, }), async run(args, ctx) { const account = args.live_account ? 'live' : 'paper' if (args.cancel_all) { // 207 Multi-Status: one entry per open order, each with its own HTTP status. const answer = ((await trading(ctx, args.live_account, '/orders', { method: 'DELETE' })) ?? []) as { id?: string status?: number }[] if (answer.length === 0) return [{ account, order_id: null, cancelled: false, http_status: 207, message: 'No open orders.' }] return answer.map((r) => ({ account, order_id: r.id ?? null, cancelled: r.status === 200 || r.status === 204, http_status: r.status ?? null, message: r.status === 200 || r.status === 204 ? 'Cancel requested.' : 'Not cancelled.', })) } if (!args.order_id) throw new Error('Give order_id, or cancel_all: true to cancel every open order.') await trading(ctx, args.live_account, `/orders/${encodeURIComponent(args.order_id)}`, { method: 'DELETE' }) return [{ account, order_id: args.order_id, cancelled: true, http_status: 204, message: 'Cancel requested.' }] }, })