demo: profiler — scrolling ticker basket with live prices + click-to-filter
J asked: "kind of like a scrolling ticker that has all of the companies
and their stock prices and where they fit in the map." Implemented as
a horizontal-scroll strip at the top of /profiler:
9 public issuers in this view · quotes via Stooq · 669ms
┌────┬────┬────┬────┬────┬────┬────┐
│TGT │JPM │BALY│ACRE│FCBC│NREF│LSBK│ ← live price + day-change per
│129 │311 │... │... │... │... │... │ ticker, color-banded by
│+.17│+1.5│... │... │... │... │... │ attribution kind
└────┴────┴────┴────┴────┴────┴────┘
Each card carries:
- ticker + live price + day-change % (red/green)
- attribution count + kind (exact / direct / parent / associated)
- left bar color = strongest attribution kind (green for direct
issuer, amber for parent, blue for co-permit associated, gradient
when both direct and associated apply)
- tooltip on hover lists the contractors attributed to this ticker
- click toggles a filter on the table below — clicking TGT cuts the
200-row list down to just TARGET CORPORATION + TORNOW, KYLE F
(Target's primary co-permit contractor)
Server-side:
- entity.ts exports fetchStooqQuote (was internal)
- new POST /intelligence/ticker_quotes — accepts {tickers: [...]},
fans out to Stooq.us in parallel, returns
{ticker, price, price_date, open, high, low, day_change_pct,
stooq_url} per symbol or null for non-US listings (HOC.DE, SKA-B.ST,
LLC.AX). Capped at 50 symbols per call.
Front-end:
- mcp-server/profiler.html — new .basket-wrap section above the
controls. buildBasket() runs after profiler_index loads:
1. Aggregates unique tickers from .tickers.direct + .associated
across all surfaced contractors
2. Renders shells immediately (ticker symbol + "—" placeholder)
3. Batch-fetches quotes via /intelligence/ticker_quotes
4. Updates each card with price + day-change in place
Click on a card sets a tickerFilter; render() skips rows whose
attributions don't include that ticker. "clear filter" button on
the basket strip resets it.
Verified end-to-end on devop.live/lakehouse/profiler:
Default load → 9 issuers, live prices populated in 669ms
TGT click → table filters to TARGET CORPORATION + TORNOW, KYLE F
(the contractor who runs 3 of Target's recent permits
gets the TGT correlation indicator)
JPM card → $311.63, +1.55% — JPMorgan-adjacent contractors
Tooltip → list of contractors attributed to the ticker
This commit is contained in:
@@ -876,6 +876,45 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Batch ticker quotes — used by the profiler page's scrolling
|
||||
// ticker basket. Stooq's HTTP CSV API is single-symbol per call,
|
||||
// so this fans out N tickers in parallel and returns a flat
|
||||
// map. Non-US tickers (HOC.DE, SKA-B.ST, LLC.AX) won't have a
|
||||
// Stooq.us entry; we surface that as null so the UI can render
|
||||
// them with a "—" placeholder.
|
||||
if (url.pathname === "/intelligence/ticker_quotes" && req.method === "POST") {
|
||||
const start = Date.now();
|
||||
const b = await json();
|
||||
const tickers: string[] = Array.isArray(b.tickers) ? b.tickers.map((t: any) => String(t).toUpperCase()).filter(Boolean) : [];
|
||||
if (!tickers.length) return ok({ quotes: {}, duration_ms: 0 });
|
||||
const { fetchStooqQuote } = await import("./entity.js");
|
||||
const dedup = Array.from(new Set(tickers)).slice(0, 50);
|
||||
const results = await Promise.all(dedup.map(async (t) => {
|
||||
// Skip non-US suffixes — Stooq.us won't have them
|
||||
if (t.includes(".")) return [t, null] as const;
|
||||
try {
|
||||
const q = await fetchStooqQuote(t);
|
||||
if (!q || !q.price) return [t, null] as const;
|
||||
const change_pct = q.open && q.open > 0 ? ((q.price - q.open) / q.open) * 100 : null;
|
||||
return [t, {
|
||||
ticker: t,
|
||||
price: q.price,
|
||||
price_date: q.price_date,
|
||||
open: q.open,
|
||||
high: q.high,
|
||||
low: q.low,
|
||||
day_change_pct: change_pct,
|
||||
stooq_url: `https://stooq.com/q/?s=${t.toLowerCase()}.us`,
|
||||
}] as const;
|
||||
} catch {
|
||||
return [t, null] as const;
|
||||
}
|
||||
}));
|
||||
const quotes: Record<string, any> = {};
|
||||
for (const [t, q] of results) quotes[t] = q;
|
||||
return ok({ quotes, count: dedup.length, duration_ms: Date.now() - start });
|
||||
}
|
||||
|
||||
// Profiler index — directory of every contractor that has filed
|
||||
// a Chicago permit recently, ranked by permit count + total
|
||||
// cost. Each name in the response links to the full /contractor
|
||||
|
||||
Reference in New Issue
Block a user