diff --git a/scripts/shared/sectors.json b/scripts/shared/sectors.json
index 5a8510574..c98cd0b83 100644
--- a/scripts/shared/sectors.json
+++ b/scripts/shared/sectors.json
@@ -1,16 +1,16 @@
{
"sectors": [
- { "symbol": "XLK", "name": "Tech" },
- { "symbol": "XLF", "name": "Finance" },
+ { "symbol": "XLK", "name": "Technology" },
+ { "symbol": "XLF", "name": "Financials" },
{ "symbol": "XLE", "name": "Energy" },
- { "symbol": "XLV", "name": "Health" },
- { "symbol": "XLY", "name": "Consumer" },
- { "symbol": "XLI", "name": "Industrial" },
- { "symbol": "XLP", "name": "Staples" },
+ { "symbol": "XLV", "name": "Health Care" },
+ { "symbol": "XLY", "name": "Consumer Disc." },
+ { "symbol": "XLI", "name": "Industrials" },
+ { "symbol": "XLP", "name": "Con. Staples" },
{ "symbol": "XLU", "name": "Utilities" },
{ "symbol": "XLB", "name": "Materials" },
- { "symbol": "XLRE", "name": "Real Est" },
- { "symbol": "XLC", "name": "Comms" },
- { "symbol": "SMH", "name": "Semis" }
+ { "symbol": "XLRE", "name": "Real Estate" },
+ { "symbol": "XLC", "name": "Comm. Svcs" },
+ { "symbol": "SMH", "name": "Semiconductors" }
]
}
diff --git a/shared/sectors.json b/shared/sectors.json
index 5a8510574..c98cd0b83 100644
--- a/shared/sectors.json
+++ b/shared/sectors.json
@@ -1,16 +1,16 @@
{
"sectors": [
- { "symbol": "XLK", "name": "Tech" },
- { "symbol": "XLF", "name": "Finance" },
+ { "symbol": "XLK", "name": "Technology" },
+ { "symbol": "XLF", "name": "Financials" },
{ "symbol": "XLE", "name": "Energy" },
- { "symbol": "XLV", "name": "Health" },
- { "symbol": "XLY", "name": "Consumer" },
- { "symbol": "XLI", "name": "Industrial" },
- { "symbol": "XLP", "name": "Staples" },
+ { "symbol": "XLV", "name": "Health Care" },
+ { "symbol": "XLY", "name": "Consumer Disc." },
+ { "symbol": "XLI", "name": "Industrials" },
+ { "symbol": "XLP", "name": "Con. Staples" },
{ "symbol": "XLU", "name": "Utilities" },
{ "symbol": "XLB", "name": "Materials" },
- { "symbol": "XLRE", "name": "Real Est" },
- { "symbol": "XLC", "name": "Comms" },
- { "symbol": "SMH", "name": "Semis" }
+ { "symbol": "XLRE", "name": "Real Estate" },
+ { "symbol": "XLC", "name": "Comm. Svcs" },
+ { "symbol": "SMH", "name": "Semiconductors" }
]
}
diff --git a/src/app/data-loader.ts b/src/app/data-loader.ts
index 3852b8fa9..17b0c286d 100644
--- a/src/app/data-loader.ts
+++ b/src/app/data-loader.ts
@@ -19,6 +19,8 @@ import {
fetchCategoryFeeds,
getFeedFailures,
fetchMultipleStocks,
+ fetchCommodityQuotes,
+ fetchSectors,
fetchCrypto,
fetchCryptoSectors,
fetchDefiTokens,
@@ -1254,25 +1256,21 @@ export class DataLoaderManager implements AppModule {
// Sector heatmap: always attempt loading regardless of market rate-limit status
const hydratedSectors = getHydratedData('sectors') as GetSectorSummaryResponse | undefined;
const heatmapPanel = this.ctx.panels['heatmap'] as HeatmapPanel | undefined;
+ const sectorNameMap = new Map(SECTORS.map((s) => [s.symbol, s.name]));
+ const toHeatmapItem = (s: { symbol: string; name: string; change: number }) => ({
+ symbol: s.symbol,
+ name: sectorNameMap.get(s.symbol) ?? s.name,
+ change: s.change,
+ });
if (hydratedSectors?.sectors?.length) {
- const mapped = hydratedSectors.sectors.map((s) => ({ name: s.name, change: s.change }));
- heatmapPanel?.renderHeatmap(mapped);
- } else if (!stocksResult.skipped) {
- const sectorsResult = await fetchMultipleStocks(
- SECTORS.map((s) => ({ ...s, display: s.name })),
- {
- onBatch: (partialSectors) => {
- heatmapPanel?.renderHeatmap(
- partialSectors.map((s) => ({ name: s.name, change: s.change }))
- );
- },
- }
- );
- heatmapPanel?.renderHeatmap(
- sectorsResult.data.map((s) => ({ name: s.name, change: s.change }))
- );
+ heatmapPanel?.renderHeatmap(hydratedSectors.sectors.map(toHeatmapItem));
} else {
- this.ctx.panels['heatmap']?.showConfigError(finnhubConfigMsg);
+ const sectorsResp = await fetchSectors();
+ if (sectorsResp.sectors.length > 0) {
+ heatmapPanel?.renderHeatmap(sectorsResp.sectors.map(toHeatmapItem));
+ } else if (stocksResult.skipped) {
+ this.ctx.panels['heatmap']?.showConfigError(finnhubConfigMsg);
+ }
}
const commoditiesPanel = this.ctx.panels['commodities'] as CommoditiesPanel | undefined;
@@ -1312,14 +1310,13 @@ export class DataLoaderManager implements AppModule {
}
for (let attempt = 0; attempt < 1 && (!metalsLoaded || !energyLoaded); attempt++) {
- const commoditiesResult = await fetchMultipleStocks(COMMODITIES, {
+ const commoditiesResult = await fetchCommodityQuotes(COMMODITIES, {
onBatch: (partial) => {
const commodityMapped = filterCommodityTape(partial).map(mapCommodity);
const energyMapped = filterEnergyTape(partial);
if (commoditiesPanel) commoditiesPanel.renderCommodities(commodityMapped);
energyPanel?.updateTape(energyMapped);
},
- useCommodityBreaker: true,
});
const commodityMapped = filterCommodityTape(commoditiesResult.data).map(mapCommodity);
const energyMapped = filterEnergyTape(commoditiesResult.data);
diff --git a/src/components/MarketPanel.ts b/src/components/MarketPanel.ts
index ea59eccfc..19405078d 100644
--- a/src/components/MarketPanel.ts
+++ b/src/components/MarketPanel.ts
@@ -138,7 +138,7 @@ export class HeatmapPanel extends Panel {
super({ id: 'heatmap', title: t('panels.heatmap'), infoTooltip: t('components.heatmap.infoTooltip') });
}
- public renderHeatmap(data: Array<{ name: string; change: number | null }>): void {
+ public renderHeatmap(data: Array<{ symbol?: string; name: string; change: number | null }>): void {
if (data.length === 0) {
this.showRetrying(t('common.failedSectorData'));
return;
@@ -147,17 +147,19 @@ export class HeatmapPanel extends Panel {
const html =
'
' +
data
- .map(
- (sector) => {
- const change = sector.change ?? 0;
- return `
+ .map((sector) => {
+ const change = sector.change ?? 0;
+ const tickerHtml = sector.symbol
+ ? `
${escapeHtml(sector.symbol)}
`
+ : '';
+ return `
+ ${tickerHtml}
${escapeHtml(sector.name)}
${formatChange(change)}
`;
- }
- )
+ })
.join('') +
'
';
diff --git a/src/services/market/index.ts b/src/services/market/index.ts
index 058eac521..179993619 100644
--- a/src/services/market/index.ts
+++ b/src/services/market/index.ts
@@ -9,6 +9,8 @@ import { getRpcBaseUrl } from '@/services/rpc-client';
import {
MarketServiceClient,
type ListMarketQuotesResponse,
+ type ListCommodityQuotesResponse,
+ type GetSectorSummaryResponse,
type ListCryptoQuotesResponse,
type ListCryptoSectorsResponse,
type CryptoSector,
@@ -27,7 +29,8 @@ import { getHydratedData } from '@/services/bootstrap';
const client = new MarketServiceClient(getRpcBaseUrl(), { fetch: (...args: Parameters) => globalThis.fetch(...args) });
const MARKET_QUOTES_CACHE_TTL_MS = 5 * 60 * 1000;
const stockBreaker = createCircuitBreaker({ name: 'Market Quotes', cacheTtlMs: MARKET_QUOTES_CACHE_TTL_MS, persistCache: true });
-const commodityBreaker = createCircuitBreaker({ name: 'Commodity Quotes', cacheTtlMs: MARKET_QUOTES_CACHE_TTL_MS, persistCache: true });
+const commodityBreaker = createCircuitBreaker({ name: 'Commodity Quotes', cacheTtlMs: MARKET_QUOTES_CACHE_TTL_MS, persistCache: true });
+const sectorBreaker = createCircuitBreaker({ name: 'Sector Summary', cacheTtlMs: MARKET_QUOTES_CACHE_TTL_MS, persistCache: true });
const cryptoBreaker = createCircuitBreaker({ name: 'Crypto Quotes', persistCache: true });
const cryptoSectorsBreaker = createCircuitBreaker({ name: 'Crypto Sectors', persistCache: true });
const defiBreaker = createCircuitBreaker({ name: 'DeFi Tokens', persistCache: true });
@@ -35,6 +38,8 @@ const aiBreaker = createCircuitBreaker({ name: 'AI Tokens'
const otherBreaker = createCircuitBreaker({ name: 'Other Tokens', persistCache: true });
const emptyStockFallback: ListMarketQuotesResponse = { quotes: [], finnhubSkipped: false, skipReason: '', rateLimited: false };
+const emptyCommodityFallback: ListCommodityQuotesResponse = { quotes: [] };
+const emptySectorFallback: GetSectorSummaryResponse = { sectors: [] };
const emptyCryptoFallback: ListCryptoQuotesResponse = { quotes: [] };
const emptyCryptoSectorsFallback: ListCryptoSectorsResponse = { sectors: [] };
const emptyDefiTokensFallback: ListDefiTokensResponse = { tokens: [] };
@@ -87,7 +92,7 @@ function symbolSetKey(symbols: string[]): string {
export async function fetchMultipleStocks(
symbols: Array<{ symbol: string; name: string; display: string }>,
- options: { onBatch?: (results: MarketData[]) => void; useCommodityBreaker?: boolean } = {},
+ options: { onBatch?: (results: MarketData[]) => void } = {},
): Promise {
// Preserve exact requested symbols for cache keys and request payloads so
// case-distinct instruments do not collapse into one cache entry.
@@ -110,8 +115,7 @@ export async function fetchMultipleStocks(
const allSymbolStrings = [...symbolMetaMap.keys()];
const setKey = symbolSetKey(allSymbolStrings);
- const breaker = options.useCommodityBreaker ? commodityBreaker : stockBreaker;
- const resp = await breaker.execute(async () => {
+ const resp = await stockBreaker.execute(async () => {
return client.listMarketQuotes({ symbols: allSymbolStrings });
}, emptyStockFallback, {
cacheKey: setKey,
@@ -151,6 +155,53 @@ export async function fetchStockQuote(
return result.data[0] || { symbol, name, display, price: null, change: null };
}
+// ========================================================================
+// Commodities -- uses listCommodityQuotes (reads market:commodities-bootstrap:v1)
+// ========================================================================
+
+export async function fetchCommodityQuotes(
+ commodities: Array<{ symbol: string; name: string; display: string }>,
+ options: { onBatch?: (results: MarketData[]) => void } = {},
+): Promise {
+ const symbols = commodities.map((c) => c.symbol);
+ const meta = new Map(commodities.map((c) => [c.symbol, c]));
+ const cacheKey = [...symbols].sort().join(',');
+
+ const resp = await commodityBreaker.execute(async () => {
+ return client.listCommodityQuotes({ symbols });
+ }, emptyCommodityFallback, {
+ cacheKey,
+ shouldCache: (r: ListCommodityQuotesResponse) => r.quotes.length > 0,
+ });
+
+ const results: MarketData[] = resp.quotes.map((q) => {
+ const m = meta.get(q.symbol);
+ return {
+ symbol: q.symbol,
+ name: m?.name ?? q.name,
+ display: m?.display ?? q.display ?? q.symbol,
+ price: q.price,
+ change: q.change,
+ sparkline: q.sparkline?.length > 0 ? q.sparkline : undefined,
+ };
+ });
+
+ if (results.length > 0) options.onBatch?.(results);
+ return { data: results };
+}
+
+// ========================================================================
+// Sectors -- uses getSectorSummary (reads market:sectors:v1)
+// ========================================================================
+
+export async function fetchSectors(): Promise {
+ return sectorBreaker.execute(async () => {
+ return client.getSectorSummary({ period: '' });
+ }, emptySectorFallback, {
+ shouldCache: (r: GetSectorSummaryResponse) => r.sectors.length > 0,
+ });
+}
+
// ========================================================================
// Crypto -- replaces fetchCrypto
// ========================================================================
diff --git a/src/styles/main.css b/src/styles/main.css
index cce21076d..2ed7edcac 100644
--- a/src/styles/main.css
+++ b/src/styles/main.css
@@ -5834,9 +5834,18 @@ body.playback-mode .status-dot {
background: var(--surface);
}
-.sector-name {
- font-size: 9px;
+.sector-ticker {
+ font-size: 8px;
+ font-weight: 600;
color: var(--text-dim);
+ letter-spacing: 0.03em;
+ opacity: 0.6;
+ margin-bottom: 1px;
+}
+
+.sector-name {
+ font-size: 8px;
+ color: var(--text-muted);
margin-bottom: 2px;
}