Stock card
A quote with its chart against the previous close, range buttons your app answers, candlesticks, a watchlist or a ticker.
On this page
<aparte-chat-viewport style="height: 42rem"></aparte-chat-viewport>
<script>
// In an app: import { setupStockCard } from '@aparte/plugin-cards/stock';
const { setupStockCard } = window.apartePlugins.cards.stock;
// A made-up price walk: n values ending at end, spread over span, the same for a seed.
const walk = (seed, n, end, span) => {
let v = 0;
const raw = Array.from({ length: n }, (_, i) => (v += Math.sin(i * 0.7 + seed) * 1.6 + Math.cos(i * 0.23 + seed * 2) + 0.12));
const last = raw[n - 1];
const scale = span / (Math.max(...raw) - Math.min(...raw));
return raw.map((x) => Math.round((end - (last - x) * scale) * 100) / 100);
};
const RANGES = { '1D': [78, 3.6], '5D': [60, 8], '1M': [44, 16], '6M': [60, 30], '1Y': [52, 42] };
const quote = (range) => ({
symbol: 'ACME', name: 'Acme Corp.', exchange: 'NASDAQ', currency: 'USD', marketState: 'open',
price: 184.6, change: 2.35, changePercent: 1.29, previousClose: 182.25,
open: 182.9, high: 185.2, low: 181.6, volume: 48200000, marketCap: 312000000000,
range, ranges: Object.keys(RANGES), series: walk(range.length * 7 + range.charCodeAt(1), RANGES[range][0], 184.6, RANGES[range][1]),
});
// The range buttons are drawn because something answers them: a press redraws the card in
// place. Here the answer is computed; in an app it is a request for the quote over that range.
const answer = async ({ name, value }) => {
if (name !== 'range') return;
await new Promise((done) => setTimeout(done, 250));
return quote(value);
};
setupStockCard({ tool: 'get_quote', onChange: answer });
const viewport = document.querySelector('aparte-chat-viewport');
viewport.appendMessage({ id: 'u1', role: 'user', content: 'How is ACME doing today?', timestamp: Date.now() });
viewport.appendMessage({ id: 'a1', role: 'assistant', timestamp: Date.now(), segments: [{
id: 's1', type: 'tool_call', status: 'resolved',
toolCall: { id: 'c-s1', name: 'get_quote', input: { symbol: 'ACME' } },
structuredResult: quote('1D'),
}] });
</script>A stock, an index, a crypto-currency or a currency pair: the price, the change in the currency
and in percent, the chart split at the previous close, the day’s figures and whether the market is
open. The range buttons over the chart are drawn only because this card has an onChange that
answers them; a press redraws the same element, the focus kept on the button.
Variants
Section titled “Variants”Candlesticks
Section titled “Candlesticks”<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
// In an app: import { setupStockCard } from '@aparte/plugin-cards/stock';
const { setupStockCard } = window.apartePlugins.cards.stock;
// A month of made-up daily candles.
const candles = Array.from({ length: 30 }, (_, i) => {
const open = 170 + Math.sin(i / 3) * 6 + i * 0.4;
const close = open + Math.sin(i * 1.7) * 3;
return { time: Date.now() - (30 - i) * 86400000, open, close,
high: Math.max(open, close) + 1 + (i % 3), low: Math.min(open, close) - 1 - (i % 2) };
});
setupStockCard({ tool: 'get_quote', variant: 'candles' });
document.querySelector('aparte-chat-bubble').setSegments([{
id: 's1', type: 'tool_call', status: 'resolved',
toolCall: { id: 'c-s1', name: 'get_quote', input: { symbol: 'ACME', range: '1M' } },
structuredResult: { symbol: 'ACME', name: 'Acme Corp.', exchange: 'NASDAQ', currency: 'USD', range: '1M', candles },
}]);
</script>The chart card with candlesticks in place of the line, from candles.
A watchlist
Section titled “A watchlist”<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
// In an app: import { setupStockCard } from '@aparte/plugin-cards/stock';
const { setupStockCard } = window.apartePlugins.cards.stock;
// A made-up price walk: n values ending at end, spread over span, the same for a seed.
const walk = (seed, n, end, span) => {
let v = 0;
const raw = Array.from({ length: n }, (_, i) => (v += Math.sin(i * 0.7 + seed) * 1.6 + Math.cos(i * 0.23 + seed * 2) + 0.12));
const last = raw[n - 1];
const scale = span / (Math.max(...raw) - Math.min(...raw));
return raw.map((x) => Math.round((end - (last - x) * scale) * 100) / 100);
};
setupStockCard({ tool: 'get_watchlist', variant: 'compact' });
document.querySelector('aparte-chat-bubble').setSegments([{
id: 's1', type: 'tool_call', status: 'resolved',
toolCall: { id: 'c-s1', name: 'get_watchlist', input: { list: 'Tech' } },
structuredResult: {
title: 'Tech watchlist',
quotes: [
{ symbol: 'ACME', name: 'Acme Corp.', currency: 'USD', price: 184.6, changePercent: 1.29, series: walk(1, 40, 184.6, 3.6) },
{ symbol: 'GLBX', name: 'Globex', currency: 'USD', price: 62.18, changePercent: -0.84, series: walk(4, 40, 62.18, 1.4) },
{ symbol: 'INIT', name: 'Initech', currency: 'USD', price: 23.4, changePercent: 3.12, series: walk(7, 40, 23.4, 1.1) },
{ symbol: 'HOOL', name: 'Hooli', currency: 'USD', price: 412.05, changePercent: 0.06, series: walk(9, 40, 412.05, 4) },
],
},
}]);
</script>One row per quote with its sparkline. Several quotes are drawn as rows whichever variant was asked: a watchlist is read by comparing lines.
A ticker
Section titled “A ticker”<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
// In an app: import { setupStockCard } from '@aparte/plugin-cards/stock';
const { setupStockCard } = window.apartePlugins.cards.stock;
setupStockCard({ tool: 'get_markets', variant: 'ticker' });
document.querySelector('aparte-chat-bubble').setSegments([{
id: 's1', type: 'tool_call', status: 'resolved',
toolCall: { id: 'c-s1', name: 'get_markets', input: {} },
structuredResult: [
{ symbol: 'ACME', currency: 'USD', price: 184.6, changePercent: 1.29 },
{ symbol: 'GLBX', currency: 'USD', price: 62.18, changePercent: -0.84 },
{ symbol: 'INIT', currency: 'USD', price: 23.4, changePercent: 3.12 },
{ symbol: 'BTC-USD', currency: 'USD', price: 68420, changePercent: -1.7 },
{ symbol: 'EUR/USD', price: 1.0842, changePercent: 0.12 },
{ symbol: 'HOOL', currency: 'USD', price: 412.05, changePercent: 0.06 },
],
}]);
</script>A strip of quotes that scrolls sideways.
While the call runs
Section titled “While the call runs”<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
// In an app: import { setupStockCard } from '@aparte/plugin-cards/stock';
const { setupStockCard } = window.apartePlugins.cards.stock;
setupStockCard({ tool: 'get_quote' });
// The call is still running: the card draws its skeleton, with what the input already says.
document.querySelector('aparte-chat-bubble').setSegments([{
id: 's1', type: 'tool_call', status: 'pending',
toolCall: { id: 'c-s1', name: 'get_quote', input: { symbols: ['ACME', 'GLBX', 'INIT'] } },
}]);
</script>The symbols the call asked for, before their prices are in.
npm install @aparte/plugin-cards @aparte/coreimport { setupStockCard } from '@aparte/plugin-cards/stock';
setupStockCard({
tool: 'get_quote',
map: (result) => ({ ...result, ranges: ['1D', '5D', '1M', '6M', '1Y'] }),
onChange: async ({ name, value }, data) => {
const [quote] = data.quotes;
if (name !== 'range' || !quote) return;
const chart = await fetch(`/api/chart?symbol=${quote.symbol}&range=${String(value)}`).then((r) => r.json());
return { ...quote, range: value, series: chart.series };
},
});readQuotes takes Yahoo Finance, Alpha Vantage, Finnhub, CoinGecko and Binance as they come. How
changes queue, what onChange returns and how long it is kept:
cards that change in place; the fields:
the guide.
Related
Section titled “Related”- Stats card — figures with sparklines.
- Charts — any chart a model or a tool writes.