Stock Tracker
A tactical, command-center style panel that tracks a single ticker: the latest
price, its movement over a comparison window (~one week by default), a compact
price chart, and supporting market fields. It runs against a pluggable market
data provider and ships with a keyless demo (mock) provider, so it shows
realistic data with no API key — seeded with Gold (XAU) and Silver
(XAG), plus a few defense equities available from the picker.
At a glance
| Type key | stock-price |
| Default size | 4 × 14 |
| Data source | A ticker symbol (resolved by the market data provider) |
| Emits | — |
| Receives | — |
| Source | src/widgets/widgets/stock-price/ |
Configuration
| Field | Type | Default | Purpose |
|---|---|---|---|
symbol | string | "XAU" | Ticker to track, normalized to uppercase. Empty shows a setup prompt. In manual mode this is the label shown on the panel. |
displayName | string | "Gold (Spot)" | Friendly instrument name, cached from the picker. |
exchange | string | "COMEX" | Listing venue, cached from the picker. |
currency | string | "USD" | ISO 4217 quote currency. |
mode | enum | "live" | Where the numbers come from: live fetches from the market data provider, manual renders the values typed in manual with no network. |
manual | object | { price: 0 } | Hand-entered values used when mode is manual: price (required), previousClose (change baseline), and optional open/high/low/volume/avgVolume/marketCap for the metrics grid. |
comparisonWindowDays | number (1–90) | 5 | Calendar window for the change; resolved to the closest trading day. The settings stepper caps this at 5 days to match the free-tier history. |
refreshIntervalMinutes | number (1–180) | 60 | How often the quote refreshes automatically. The stepper floors at 15 min; live prices are daily-delayed and cached for an hour. |
showSparkline | boolean | true | Show the compact price chart beneath the quote summary. |
title | string | — | Label for the panel header. Empty uses the tracked instrument name (or symbol), falling back to "Stock Tracker". Distinct from the card title. |
showHeader | boolean | true | Show the panel header row (trend icon + title + refresh). |
showRefresh | boolean | true | Show the manual refresh button in the header. |
compact | boolean | false | Compact mode: show only the header, symbol, and current price (with the change), hiding the chart, metrics grid, and footer. |
compactLayout | enum | "label-top" | Arrangement of the symbol label relative to the price in compact mode: label-top, label-bottom, label-left, label-right. |
showMetrics | boolean | true | Show the supporting metrics grid (open/high/low/volume/avg vol/market cap). |
showLastUpdated | boolean | true | Show the "Updated:" timestamp in the footer. |
showSource | boolean | true | Show the exchange, market status, and provider attribution in the footer. |
Config panel
- Data — a Data source toggle (Live / Manual). In Live mode you pick the ticker (free-text, normalized to uppercase, with quick-pick chips for the seeded symbols), the comparison window, and the auto-refresh interval; picking a chip caches that symbol's name, exchange, and currency. In Manual mode you type the values directly — a label, currency, the headline Price and an optional Previous close (the change baseline), plus optional open/high/low/volume/avg volume/market cap for the metrics grid — and the widget renders them with no network access.
- Appearance — a Show header toggle (and, when shown, a Refresh button toggle and a Header label that defaults to the tracked instrument), a Compact mode toggle with a label-position control, and — when not compact — toggles for the price chart, the metrics grid, the last-updated timestamp, and the source attribution. These are separate from the drawer's Show card header / Card title controls.
Cross-widget actions
None.
What it shows
- Header — a trend icon, the panel title (the tracked instrument name or symbol unless a custom label is set), and a manual refresh control.
- Primary summary — the dominant current price with its currency, and the comparison-window change (absolute + percent) with an up/down arrow. Positive movement tints green, negative red — text and icon only, no colored fills.
- Price chart — a thin line with a subtle area fill over the recent history,
with low-opacity grid lines, right-edge price labels, and date labels along
the bottom (optional via
showSparkline). It grows to fill the space freed by any hidden sections. - Metrics grid — open, high, low, volume, average volume, and market cap in
a 2 × 3 grid with thin dividers (optional via
showMetrics). - Footer — the exchange, a market-status dot/label, and the provider
attribution on the left (
showSource), and a last-updated timestamp on the right (showLastUpdated). The row collapses entirely when both are off. - Compact mode — when
compactis on, the widget shows just the symbol label and the current price with its change, arranged percompactLayout. The text scales with the panel; hiding the header lets it grow larger to fill the reclaimed space.
States
- Empty — when no symbol is set, a "Select a ticker to begin market tracking" prompt with a configure button.
- Loading — the standard widget loader (a small spinner with a quiet "Loading" label), shared by every widget for a consistent feel.
- Error — a compact "Market data unavailable for this symbol" message with a retry action; unknown symbols report a more specific not-found message.
- Stale — when a refresh fails, the last good quote stays on screen and the footer timestamp is flagged as stale rather than blanking the panel.
Notable behavior
- Pluggable provider — all data goes through a
StockDataProvidercontract (searchSymbols,getQuote,getHistoricalPrices). The default is the keylessmockStockDataProvider, which generates deterministic, symbol-seeded data. When a live API key is configured the widget uses real market data instead — no secrets are hard-coded, and the UI is provider-agnostic. - Manual entry — switching the Data source to Manual bypasses every
provider:
buildManualStockturns the entered values into the same normalized shape the live path produces, so the chart, change, metrics grid, and footer all render from hand-typed numbers. The change is measured against the entered previous close (one prior session), the refresh button is hidden, and the footer source reads "Manual entry". - Trading-day comparison — the change is measured against the session
closest to
comparisonWindowDaysago in trading days (a 5-day window ≈ a trading week), not an exact calendar offset. - Data isolation — network access and normalization live in
stockDataProvider.ts; the React layer uses theuseStockPriceDatahook and never fetches directly. - Auto-refresh — the widget polls on
refreshIntervalMinutesand supports manual refresh from the header. Rapid symbol edits are debounced into a single request. - Flexible layout — as optional sections are toggled off, the remaining elements expand to take up the available space rather than leaving gaps; the price chart absorbs most of the slack.
Live data (MetalpriceAPI)
By default the widget runs on the keyless mock provider, so it shows realistic data everywhere with no setup. To stream real metal and forex rates, set a MetalpriceAPI key:
# .env.local (frontend)
VITE_STOCK_API_KEY=your-metalpriceapi-key
With a key present, getStockProvider() selects the live
createMetalPriceProvider(...) instead of the mock; with no key it falls back to
the mock. Nothing else in the widget changes — the provider returns the same
normalized shape.
Supported symbols
MetalpriceAPI covers precious metals (XAU, XAG, XPT, XPD, XRH),
base metals, forex (150+ currencies), crypto, and energy — quoted against
USD. The seeded Gold (XAU) and Silver (XAG) symbols work out of the
box; you enter the bare metal code. It does not cover equities, so the
defense-equity quick-picks are only available in mock mode.
Staying within the free tier
The MetalpriceAPI free tier serves daily-delayed rates on a monthly request quota. The live provider is built to stay well within that budget and to avoid showing errors during a live demo:
- One request per refresh. The
timeframeendpoint returns the entire daily series for the comparison window in a single call, and its latest point doubles as the current quote — no separate "latest" request. One cached series serves every range. - Shared cache + in-flight dedupe. Multiple widgets tracking the same symbol (and the parallel quote/history reads) collapse to one network request within the cache window (one hour by default).
- Monthly quota guard. Every response carries
X-API-QUOTA/X-API-CURRENT; as usage nears the cap the provider serves the last cached series instead of firing a request that would be rejected. A per-minute throttle smooths bursts. - Graceful staleness. If a refresh fails, the last good quote stays on screen and the footer is flagged stale rather than dropping to an error state.
Because the free tier is daily-delayed, frequent polling adds no freshness — the
caches and quota guard mean a couple of distinct symbols comfortably fit a small
monthly allowance, and duplicate widgets tracking the same symbol are free. The
cache TTL and quota headroom live as constants at the top of
metalPriceProvider.ts if you move to a paid plan. On the free tier the daily
open/high/low mirror the close, so the metrics grid reflects the daily rate.
The free plan also caps timeframe history to a 5-day span, so the live
provider fetches a 5-day window; longer comparison windows (e.g. 1‑month or
3‑month) gracefully fall back to the days available. Raise SERIES_CALENDAR_DAYS
in metalPriceProvider.ts once you are on a paid plan for deeper history.