Audit 99: Empty company charts fix (2026-05-20)
Scope
Companies with at least one Declaration row but zero BacktestResult rows
where returnFromPub90d IS NOT NULL. These companies render the StockChart
component with valid trades props but no price history from Yahoo Finance,
which caused the chart area and the trade marker list to both silently disappear.
Query
SELECT COUNT(*) AS impacted
FROM "Company" c
WHERE (SELECT COUNT(*) FROM "Declaration" WHERE "companyId" = c.id) > 0
AND (
SELECT COUNT(*)
FROM "Declaration" d
JOIN "BacktestResult" br ON br."declarationId" = d.id
WHERE d."companyId" = c.id
AND br."returnFromPub90d" IS NOT NULL
) = 0;
Result (2026-05-20): 17,758 companies impacted.
Sub-query: companies among those with no Yahoo ticker or ISIN: 4,333. The remaining ~13,425 have a ticker/ISIN but Yahoo returned no price history (micro-caps, recent IPOs, cross-listed tickers not on Euronext Paris, etc.).
Concrete example: Heritage Mining Ltd
slug:heritage-mining-ltdyahooSymbol:HML.CN(Canadian TSX-V ticker)isin: NULLdeclarations: 1 (dated 2026-05-15)returnFromPub90d: NULL (T+90 not elapsed yet)
Yahoo Finance API does not serve price history for HML.CN via the Euronext
path used by /api/stock. The API returns a successful response with
points: [], which caused data to be set to null after the component
detected empty points (auto-fallback logic set data to null on empty response).
Root cause
In src/components/StockChart.tsx, the guard at the start of the render block:
if (!data) return null;
This early return fires when the /api/stock call fails or returns no points.
Because the component renders null, the trade markers and the declaration
list below the chart are never shown, even though the trades prop is
non-empty. Users see a blank section with no explanation.
The tradeBubbles and allTrades memos were also gated on
data?.points.length, so even a partial fix (showing the chart shell) would
not have surfaced the trade list.
Fix applied
File changed: src/components/StockChart.tsx
When data is null but trades is non-empty, the component now renders a
compact fallback table instead of returning null. The fallback shows:
- A header row with trade count and a "Price data pending ingestion" badge
- Each trade as a row: buy/sell indicator, date, insider name, amount
- Same visual design as the existing trade list below the chart
Additionally, the tradeBubbles and allTrades useMemo guards were tightened
from data?.points.length to data?.points?.length (optional chaining) to
be consistent with the fallback path.
The fix does not alter the chart rendering path. When price data is available, behaviour is identical to before.
Impact by sub-case
| Case | Companies | Behaviour after fix |
|---|---|---|
| No ticker, no ISIN | ~4,333 | Fallback table shown |
| Ticker present, no Yahoo data | ~13,425 | Fallback table shown |
| Price data available, no returnFromPub90d | Remainder | Chart + markers shown as before |
Insider page (/insider/[slug])
The insider page (src/app/insider/[slug]/page.tsx) uses a different component
(InsiderHero) that renders trade events independently of StockChart. The
StockChart is not used on that page. No fix needed there.
Diff summary
src/components/StockChart.tsx
- if (!data) return null;
+ if (!data) { if (!trades?.length) return null; /* fallback table */ }
- if (!data?.points.length) return []; // tradeBubbles
+ if (!data?.points?.length) return [];
- if (!data?.points.length) return []; // allTrades
+ if (!data?.points?.length) return [];
Status
Applied 2026-05-20. No schema migration needed. No deploy required (src change only, pending next build).