profiler: multi-source quote aggregation with confidence scoring

Replaces the single-source quote feed with a 3-source consensus system,
the differentiator for the index: pull each ticker from independent
feeds, take the median, and score confidence by how many corroborate it.
Disagreement becomes a signal instead of a silently-wrong number.

Context: Stooq retired its free q/l/ CSV quote API (404s server-side;
q/d/l behind a JS challenge), which broke the profiler basket. The
interim Yahoo-only fix worked but had a hidden wrong-baseline bug —
Yahoo's chartPreviousClose over a 5d window references the pre-window
close, so day-change read +11.65% for CAT when the real figure is
+1.11%. Cross-checking against Nasdaq + CNBC exposes exactly that class
of quiet error, which a single source can never self-report.

- quotes.ts — Yahoo + Nasdaq + CNBC fetchers (all keyless, browser UA;
  Yahoo prev-close now derived from the daily candle, not the buggy
  meta field), median consensus, confidence = corroborating sources / 3,
  outlier flagging, and a bun:sqlite hourly time-series cache.
- quote_ingest.ts + ops/systemd/lakehouse-quote-ingest.{service,timer} —
  hourly snapshot into the cache (real-time not required per product
  call). Runs as root to match the mcp-server that co-owns the db;
  busy_timeout serializes the two writers.
- /intelligence/ticker_quotes — serves cache-first (fast), aggregates
  cold tickers on-demand once and caches them. New /quote_ingest admin
  trigger.
- profiler.html — per-card confidence badge (green/amber/red) + a
  per-source breakdown tooltip; "3-source consensus" labeling.
- entity.ts — the earlier Stooq->Yahoo swap for the single-ticker brief
  (kept fetchStooqQuote dead-but-reversible).

Verified live on devop.live/lakehouse/profiler: liquid names read
100% / 3-source consensus; thin microcaps (BLPG, ESHSF — Yahoo-only)
honestly read 33% / 1-source. Full basket (17 tickers) seeded; hourly
timer armed. Cache db (data/_quote_cache/) is gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-06-18 01:28:28 -05:00
co-authored by Claude Opus 4.8
parent fcd53168a0
commit ab1f12296d
8 changed files with 531 additions and 37 deletions
+24 -3
View File
@@ -87,6 +87,11 @@ td.role .pill{display:inline-block;padding:2px 7px;border-radius:9px;font-size:9
.bk-card .ch.up{color:#3fb950}
.bk-card .ch.down{color:#f85149}
.bk-card .ch.flat{color:#545d68}
.bk-card .cf{font-family:ui-monospace,monospace;font-size:9px;margin-top:3px;font-weight:700;letter-spacing:0.3px;display:inline-block;padding:1px 5px;border-radius:4px}
.bk-card .cf.full{background:#0d2818;border:1px solid #2ea04366;color:#3fb950}
.bk-card .cf.part{background:#1a1410;border:1px solid #d2992266;color:#d29922}
.bk-card .cf.thin{background:#2d1316;border:1px solid #f8514966;color:#f85149}
.bk-card .cf.stale{opacity:0.6;border-style:dashed}
.bk-card .meta{font-size:9px;color:#545d68;margin-top:5px;text-transform:uppercase;letter-spacing:0.6px}
.bk-card .kind-bar{position:absolute;left:0;top:0;bottom:0;width:3px;border-radius:8px 0 0 8px}
.bk-card .kind-bar.exact,.bk-card .kind-bar.direct{background:#3fb950}
@@ -108,8 +113,8 @@ td.role .pill{display:inline-block;padding:2px 7px;border-radius:9px;font-size:9
<div class="bar">
<h1>Staffing Co-Pilot · Profiler Index</h1>
<nav>
<a href="" id="back-dashboard">← Dashboard</a>
<a href="" id="back-console">Console</a>
<a href="." id="back-dashboard">← Dashboard</a>
<a href="console" id="back-console">Console</a>
</nav>
</div>
<div class="content">
@@ -303,6 +308,7 @@ function buildBasket(){
var tk=document.createElement('div'); tk.className='tk'; tk.textContent=b.ticker; card.appendChild(tk);
var px=document.createElement('div'); px.className='px'; px.textContent='—'; card.appendChild(px);
var ch=document.createElement('div'); ch.className='ch flat'; ch.textContent=' '; card.appendChild(ch);
var cf=document.createElement('div'); cf.className='cf'; cf.textContent=''; card.appendChild(cf);
var meta=document.createElement('div'); meta.className='meta';
meta.textContent=b.count+' attribution'+(b.count===1?'':'s')+' · '+b.kinds.join('+');
card.appendChild(meta);
@@ -328,7 +334,7 @@ function buildBasket(){
}).then(function(r){return r.json()}).then(function(qd){
var quotes=qd.quotes||{};
lastQuotes = quotes;
document.getElementById('bk-meta').textContent='quotes via Stooq · '+(qd.duration_ms||0)+'ms';
document.getElementById('bk-meta').textContent='quotes via 3-source consensus (Yahoo·Nasdaq·CNBC) · '+(qd.duration_ms||0)+'ms';
Array.prototype.forEach.call(track.children, function(card){
var t=card.dataset.ticker; var q=quotes[t];
if(!q || !q.price) return;
@@ -338,6 +344,21 @@ function buildBasket(){
if(q.day_change_pct==null){ ch.textContent='close '+(q.price_date||''); ch.className='ch flat'; }
else if(q.day_change_pct>=0){ ch.textContent='+'+q.day_change_pct.toFixed(2)+'%'; ch.className='ch up'; }
else { ch.textContent=q.day_change_pct.toFixed(2)+'%'; ch.className='ch down'; }
// Multi-source confidence badge: corroborating sources / 3. Green
// at full agreement, amber when one source is missing/divergent,
// red when thin. The whole differentiator made visible per-ticker.
var cf=card.querySelector('.cf');
if(cf && q.confidence!=null){
var cls = q.confidence>=100 ? 'cf full' : q.confidence>=67 ? 'cf part' : 'cf thin';
if(q.stale) cls += ' stale';
cf.className = cls;
cf.textContent = (q.source_count||0)+'/3 ✓ '+q.confidence+'%'+(q.stale?' · stale':'');
// Tooltip: per-source breakdown so a staffer can audit the number.
var lines=(q.sources||[]).map(function(s){
return ' '+s.source+': '+(s.day_change_pct>=0?'+':'')+s.day_change_pct.toFixed(2)+'%'+(s.outlier?' ⚠ outlier':'');
}).join('\n');
card.title=(q.ticker||t)+' — '+q.confidence+'% confidence ('+(q.source_count||0)+'/3 sources, spread '+(q.spread_pp!=null?q.spread_pp.toFixed(2):'?')+'pp)\n'+lines;
}
});
updateThesisMetrics();
}).catch(function(){