Jaspers Hub

jaspers / alpaca

pluginOfficialReviewed★ 0↓ 0

Alpaca: Connects to Alpaca for stock/options market data, account and position info, and placing or cancelling real orders.

4.8 KB
1import { useData, usePublish, type PanelRef } from '@jaspers-ai/sdk'
2import { fmt, sign } from './format'
3import './styles.css'
4
5interface Account {
6 mode: string
7 account_number: string | null
8 status: string | null
9 equity: number | null
10 cash: number | null
11 buying_power: number | null
12 day_pl: number | null
13 day_pl_pct: number | null
14 portfolio_value: number | null
15 trading_blocked: boolean
16 pattern_day_trader: boolean
17 daytrade_count: number | null
18 [field: string]: unknown
19}
20
21interface Position {
22 symbol: string
23 side: string | null
24 qty: number | null
25 avg_entry_price: number | null
26 current_price: number | null
27 market_value: number | null
28 unrealized_pl: number | null
29 unrealized_plpc: number | null
30 change_today: number | null
31 [field: string]: unknown
32}
33
34export function AccountPanel({ panel }: { panel: PanelRef }) {
35 const state = useData(`workspaces/${panel.workspaceId}/panels/${panel.id}/state`) as
36 | { live_account?: boolean }
37 | undefined
38 if (state === undefined) return <p className="al-note">Loading…</p>
39 return <Panel panel={panel} live={state.live_account === true} />
40}
41
42function Panel({ panel, live }: { panel: PanelRef; live: boolean }) {
43 const acct = useData('alpaca/account', { live_account: live })
44 const pos = useData('alpaca/positions', { live_account: live })
45 const a = ((acct.data ?? []) as Account[])[0]
46 const rows = (pos.data ?? []) as Position[]
47 usePublish(panel, {
48 mode: live ? 'live' : 'paper',
49 equity: a?.equity ?? null,
50 cash: a?.cash ?? null,
51 buying_power: a?.buying_power ?? null,
52 day_pl: a?.day_pl ?? null,
53 positions: rows.length,
54 })
55
56 return (
57 <div className="al">
58 <div className="al-head">
59 <h3 className="al-title">Alpaca account</h3>
60 <span className={live ? 'al-badge al-badge-live' : 'al-badge'}>{live ? 'LIVE' : 'paper'}</span>
61 {a?.account_number && <span className="al-muted">{a.account_number}</span>}
62 {a?.status && <span className="al-muted">{a.status}</span>}
63 {a?.trading_blocked && <span className="al-badge al-badge-live">trading blocked</span>}
64 <button
65 className="al-btn"
66 onClick={() => {
67 acct.refetch?.()
68 pos.refetch?.()
69 }}
70 >
71 Refresh
72 </button>
73 </div>
74 {acct.error ? (
75 <p className="al-note al-error">{acct.error}</p>
76 ) : !a ? (
77 <p className="al-note">Loading…</p>
78 ) : (
79 <div className="al-cards">
80 <Card label="Equity" value={fmt(a.equity)} />
81 <Card label="Cash" value={fmt(a.cash)} />
82 <Card label="Buying power" value={fmt(a.buying_power)} />
83 <Card
84 label="Day P/L"
85 value={`${fmt(a.day_pl)} (${fmt(a.day_pl_pct)}%)`}
86 className={sign(a.day_pl)}
87 />
88 <Card label="Day trades" value={`${a.daytrade_count ?? '–'}${a.pattern_day_trader ? ' · PDT' : ''}`} />
89 </div>
90 )}
91 {pos.error ? (
92 <p className="al-note al-error">{pos.error}</p>
93 ) : !pos.data ? null : rows.length === 0 ? (
94 <p className="al-note">No open positions.</p>
95 ) : (
96 <table className="al-table">
97 <thead>
98 <tr>
99 <th>Symbol</th>
100 <th>Side</th>
101 <th className="al-num">Qty</th>
102 <th className="al-num">Avg entry</th>
103 <th className="al-num">Price</th>
104 <th className="al-num">Market value</th>
105 <th className="al-num">Unrealized P/L</th>
106 <th className="al-num">P/L %</th>
107 <th className="al-num">Today %</th>
108 </tr>
109 </thead>
110 <tbody>
111 {rows.map((p) => (
112 <tr key={p.symbol}>
113 <td>{p.symbol}</td>
114 <td>{p.side ?? '–'}</td>
115 <td className="al-num">{fmt(p.qty, 0)}</td>
116 <td className="al-num">{fmt(p.avg_entry_price)}</td>
117 <td className="al-num">{fmt(p.current_price)}</td>
118 <td className="al-num">{fmt(p.market_value)}</td>
119 <td className={`al-num ${sign(p.unrealized_pl)}`}>{fmt(p.unrealized_pl)}</td>
120 <td className={`al-num ${sign(p.unrealized_plpc)}`}>
121 {p.unrealized_plpc === null ? '–' : `${fmt(p.unrealized_plpc * 100)}%`}
122 </td>
123 <td className={`al-num ${sign(p.change_today)}`}>
124 {p.change_today === null ? '–' : `${fmt(p.change_today * 100)}%`}
125 </td>
126 </tr>
127 ))}
128 </tbody>
129 </table>
130 )}
131 </div>
132 )
133}
134
135function Card({ label, value, className = '' }: { label: string; value: string; className?: string }) {
136 return (
137 <div className="al-card">
138 <div className="al-card-label">{label}</div>
139 <div className={`al-card-value ${className}`}>{value}</div>
140 </div>
141 )
142}