import { defineSource, type BackendContext } from '@jaspers-ai/sdk' import { z } from 'zod' import { DATA_HOST, TRADING_HOSTS, alpaca } from './client.ts' import { mid, num, orderRow, parseOcc, str } from './rows.ts' import { liveAccount, trading } from './trading.ts' // Sources that change the account. Never run from a view's draw: only on the user's explicit ask. export const LIVE_ORDER_DISCLAIMER = 'Live order on your brokerage account. This is your trade, at your risk — see jsprai.com/terms §7.' export const LIVE_ORDER_LIVE_KEY = 'live-order-approval' const APPROVAL_FILE = 'live-approval.json' export interface PendingLiveOrder { id: string connectionId: string symbol: string side: 'buy' | 'sell' quantity: string orderType: string estimatedValue: string disclaimer: string createdAt: string } interface ApprovalRecord { connectionId: string connection_id: string timestamp: string approved_at: string } interface ApprovalState { connections: Record } interface WaitingApproval { pending: PendingLiveOrder resolve: (approved: boolean) => void } let activeApproval: WaitingApproval | null = null function approvalToken(): string { return `live-order-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}-${Math.random().toString(36).slice(2, 12)}` } function fmtUsd(value: number): string { return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` } async function readApprovalState(ctx: BackendContext): Promise { try { const raw = await ctx.files.read(APPROVAL_FILE) const parsed = JSON.parse(raw) as { connections?: Record } if (parsed && typeof parsed === 'object' && parsed.connections && typeof parsed.connections === 'object') { return { connections: parsed.connections } } } catch { // Missing or unreadable file means no connection has been approved yet. } return { connections: {} } } async function hasLiveApproval(ctx: BackendContext, connectionId: string): Promise { const state = await readApprovalState(ctx) const entry = state.connections[connectionId] return Boolean(entry && typeof entry.timestamp === 'string' && entry.timestamp.length > 0) } async function recordLiveApproval(ctx: BackendContext, connectionId: string, timestamp: string): Promise { const state = await readApprovalState(ctx) state.connections[connectionId] = { connectionId, connection_id: connectionId, timestamp, approved_at: timestamp, } await ctx.files.write(APPROVAL_FILE, `${JSON.stringify(state, null, 2)}\n`) } async function liveConnectionId(ctx: BackendContext): Promise { const acct = (await trading(ctx, true, '/account')) as Record | null const id = str(acct?.id) ?? str(acct?.account_number) return id ? `alpaca/live:${id}` : 'alpaca/live' } async function latestPrice(ctx: BackendContext, symbol: string, isOption: boolean): Promise { const base = isOption ? '/v1beta1/options' : '/v2/stocks' const query = { symbols: symbol, feed: isOption ? 'indicative' : 'iex' } try { const trades = (await alpaca(ctx, { host: DATA_HOST, path: `${base}/trades/latest`, query })) as { trades?: Record> } | null const tradePrice = num(trades?.trades?.[symbol]?.p) if (tradePrice !== null && tradePrice > 0) return tradePrice const quotes = (await alpaca(ctx, { host: DATA_HOST, path: `${base}/quotes/latest`, query })) as { quotes?: Record> } | null const q = quotes?.quotes?.[symbol] return mid(num(q?.bp), num(q?.ap)) ?? num(q?.ap) ?? num(q?.bp) } catch { return null } } export async function estimateOrderValue( ctx: BackendContext, order: { symbol: string qty?: number notional?: number type: string limit_price?: number stop_price?: number }, ): Promise { if (order.notional !== undefined) return fmtUsd(order.notional) if (order.qty === undefined) return 'Market price' const symbol = order.symbol.trim().toUpperCase() const isOption = parseOcc(symbol).underlying !== null const multiplier = isOption ? 100 : 1 const unitPrice = order.limit_price ?? order.stop_price ?? (await latestPrice(ctx, symbol, isOption)) if (unitPrice !== null && Number.isFinite(unitPrice) && unitPrice > 0) { return fmtUsd(order.qty * unitPrice * multiplier) } return 'Market price' } async function requireFirstLiveTradeApproval( ctx: BackendContext, order: { symbol: string side: 'buy' | 'sell' qty?: number notional?: number type: string limit_price?: number stop_price?: number }, ): Promise { const connectionId = await liveConnectionId(ctx) if (await hasLiveApproval(ctx, connectionId)) return const symbol = order.symbol.trim().toUpperCase() const quantity = order.qty !== undefined ? String(order.qty) : `${fmtUsd(order.notional ?? 0)} notional` const estimatedValue = await estimateOrderValue(ctx, { ...order, symbol }) const pending: PendingLiveOrder = { id: approvalToken(), connectionId, symbol, side: order.side, quantity, orderType: order.type, estimatedValue, disclaimer: LIVE_ORDER_DISCLAIMER, createdAt: new Date().toISOString(), } if (activeApproval) { activeApproval.resolve(false) activeApproval = null } ctx.live.set(LIVE_ORDER_LIVE_KEY, pending) ctx.notify( `Approve first live Alpaca order (${pending.side.toUpperCase()} ${pending.quantity} ${pending.symbol}, ${pending.orderType}, est. ${pending.estimatedValue}) in the Alpaca panel.`, ) const approved = await new Promise((resolve) => { activeApproval = { pending, resolve } }) if (!approved) { throw new Error('Live order declined by user.') } await recordLiveApproval(ctx, connectionId, new Date().toISOString()) } export const confirmLiveOrder = defineSource({ description: 'Internal view action: approves or declines the pending first live order on a connection.', internal: true, ttlMs: 0, input: z.object({ id: z.string().min(1), approved: z.boolean(), }), async run(args, ctx) { const current = activeApproval if (!current || current.pending.id !== args.id) { if (!current) ctx.live.delete(LIVE_ORDER_LIVE_KEY) return { handled: false } } activeApproval = null ctx.live.delete(LIVE_ORDER_LIVE_KEY) current.resolve(args.approved) return { handled: true, approved: args.approved } }, }) 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). The first live order (live_account: true) on a connection requires the user to click Approve in an open Alpaca view (place alpaca/account or alpaca/orders first if none is open). Answers the order row.', hosts: [DATA_HOST, ...TRADING_HOSTS], ttlMs: 0, 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 symbol = args.symbol.trim().toUpperCase() if (args.live_account) { await requireFirstLiveTradeApproval(ctx, { symbol, side: args.side, qty: args.qty, notional: args.notional, type: args.type, limit_price: args.limit_price, stop_price: args.stop_price, }) } const s = (n: number | undefined) => (n === undefined ? undefined : String(n)) const body: Record = { symbol, 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, ttlMs: 0, 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.' }] }, })