import type { BackendContext } from '@jaspers-ai/sdk' import { defineSource } from '@jaspers-ai/sdk' import { z } from 'zod' import { LIVE_HOST, PAPER_HOST, TRADING_HOSTS, alpaca, type Query, type Request } from './client' import { num, orderRow, str } from './rows' /** The one way to reach the live account. Shared by every trading source. */ export const liveAccount = z .boolean() .default(false) .describe('LIVE (real-money) account: true uses api.alpaca.markets with the live keys. Default false: the paper account.') /** A Trading API call on the paper host, or the live host and keys when `live` is true. */ export function trading( ctx: BackendContext, live: boolean, path: string, rest: Omit = {}, ): Promise { return alpaca(ctx, { ...rest, host: live ? LIVE_HOST : PAPER_HOST, path: `/v2${path}`, live }) } export const account = defineSource({ description: 'The Alpaca trading account (paper unless live_account is true), one row: mode (paper|live), account_number, status, currency, cash, buying_power, equity, last_equity, day_pl, day_pl_pct, portfolio_value, long_market_value, short_market_value, daytrade_count, pattern_day_trader, trading_blocked.', hosts: TRADING_HOSTS, input: z.object({ live_account: liveAccount }), async run(args, ctx) { const a = (await trading(ctx, args.live_account, '/account')) as Record const equity = num(a.equity) const lastEquity = num(a.last_equity) const dayPl = equity !== null && lastEquity !== null ? Math.round((equity - lastEquity) * 100) / 100 : null return [ { mode: args.live_account ? 'live' : 'paper', account_number: str(a.account_number), status: str(a.status), currency: str(a.currency), cash: num(a.cash), buying_power: num(a.buying_power), regt_buying_power: num(a.regt_buying_power), daytrading_buying_power: num(a.daytrading_buying_power), equity, last_equity: lastEquity, day_pl: dayPl, day_pl_pct: dayPl !== null && lastEquity ? Math.round((dayPl / lastEquity) * 1_000_000) / 10000 : null, portfolio_value: num(a.portfolio_value), long_market_value: num(a.long_market_value), short_market_value: num(a.short_market_value), multiplier: num(a.multiplier), daytrade_count: num(a.daytrade_count), pattern_day_trader: Boolean(a.pattern_day_trader), trading_blocked: Boolean(a.trading_blocked), account_blocked: Boolean(a.account_blocked), shorting_enabled: Boolean(a.shorting_enabled), options_trading_level: num(a.options_trading_level), created_at: str(a.created_at), }, ] }, }) export const positions = defineSource({ description: 'Open positions on the Alpaca account (paper unless live_account). Rows: symbol, qty, side, avg_entry_price, current_price, market_value, cost_basis, unrealized_pl, unrealized_plpc, unrealized_intraday_pl, change_today, asset_class.', hosts: TRADING_HOSTS, input: z.object({ live_account: liveAccount }), async run(args, ctx) { const list = ((await trading(ctx, args.live_account, '/positions')) ?? []) as Record[] return list.map((p) => ({ symbol: str(p.symbol), asset_class: str(p.asset_class), exchange: str(p.exchange), side: str(p.side), qty: num(p.qty), qty_available: num(p.qty_available), avg_entry_price: num(p.avg_entry_price), current_price: num(p.current_price), lastday_price: num(p.lastday_price), market_value: num(p.market_value), cost_basis: num(p.cost_basis), unrealized_pl: num(p.unrealized_pl), unrealized_plpc: num(p.unrealized_plpc), unrealized_intraday_pl: num(p.unrealized_intraday_pl), change_today: num(p.change_today), })) }, }) export const orders = defineSource({ description: 'Orders on the Alpaca account (paper unless live_account), read only. Rows: id, client_order_id, symbol, side, type, time_in_force, qty, notional, filled_qty, limit_price, stop_price, filled_avg_price, status, submitted_at, filled_at, canceled_at, order_class, parent_id (set on bracket legs when nested).', hosts: TRADING_HOSTS, input: z.object({ status: z.enum(['open', 'closed', 'all']).default('all').describe('Which orders.'), limit: z.number().int().min(1).max(500).default(100).describe('Most orders.'), after: z.string().optional().describe('Only orders submitted after this time, RFC 3339.'), until: z.string().optional().describe('Only orders submitted until this time, RFC 3339.'), direction: z.enum(['asc', 'desc']).default('desc').describe('Time order.'), symbols: z.array(z.string()).optional().describe('Only these symbols.'), nested: z.boolean().default(false).describe('Nest bracket/OCO legs; legs are added as rows after their parent.'), live_account: liveAccount, }), async run(args, ctx) { const query: Query = { status: args.status, limit: args.limit, after: args.after, until: args.until, direction: args.direction, nested: args.nested, symbols: args.symbols?.map((s) => s.trim().toUpperCase()).join(','), } const list = ((await trading(ctx, args.live_account, '/orders', { query })) ?? []) as Record[] const rows: Record[] = [] for (const o of list) { rows.push({ ...orderRow(o), parent_id: null }) for (const leg of (o.legs ?? []) as Record[]) rows.push({ ...orderRow(leg), parent_id: str(o.id) }) } return rows }, }) const assetRow = (a: Record) => ({ symbol: str(a.symbol), name: str(a.name), class: str(a.class), exchange: str(a.exchange), status: str(a.status), tradable: Boolean(a.tradable), marginable: Boolean(a.marginable), shortable: Boolean(a.shortable), easy_to_borrow: Boolean(a.easy_to_borrow), fractionable: Boolean(a.fractionable), id: str(a.id), }) export const assets = defineSource({ description: 'Tradable assets on Alpaca: one asset by symbol, or a list filtered by status, class, and exchange. Rows: symbol, name, class, exchange, status, tradable, marginable, shortable, easy_to_borrow, fractionable.', hosts: TRADING_HOSTS, input: z.object({ symbol: z.string().optional().describe('One asset, by symbol (or OCC symbol). Leave out for a list.'), status: z.enum(['active', 'inactive']).default('active'), asset_class: z.enum(['us_equity', 'us_option', 'crypto']).default('us_equity'), exchange: z.string().optional().describe('Like NASDAQ, NYSE, ARCA, AMEX, BATS, OTC.'), limit: z.number().int().min(1).max(50000).default(500).describe('Most rows of a list.'), live_account: liveAccount, }), async run(args, ctx) { if (args.symbol) { const one = (await trading(ctx, args.live_account, `/assets/${encodeURIComponent(args.symbol.trim().toUpperCase())}`)) as Record return [assetRow(one)] } const query: Query = { status: args.status, asset_class: args.asset_class, exchange: args.exchange } const list = ((await trading(ctx, args.live_account, '/assets', { query })) ?? []) as Record[] return { rows: list.slice(0, args.limit).map(assetRow), total: list.length } }, })