demo: profiler index — ticker associations (direct, parent, co-permit)
J's framing: "if a contractor works for Target, future Target contracts
mean money flows back to the contractor — the ticker is an associated
indicator." Now the profiler index attaches three flavors of ticker per
contractor and renders them as colored pills:
green DIRECT contractor IS the public issuer (Target Corp → TGT)
amber PARENT contractor is a subsidiary of a public parent
(Turner Construction → HOC.DE via Hochtief AG)
blue ASSOCIATED contractor co-appears on permits with a public
entity (TORNOW, KYLE F → TGT, 3 shared permits with
TARGET CORPORATION)
The associated flavor is the correlation signal J described — it pulls
the ticker for whoever the contractor has been working *with*, not
just what they are themselves. Most contractors are private; the
associated link is how the moat shows up.
Server-side:
- entity.ts new export `lookupTickerLite(name)` — cheap in-memory
resolver that does only the SEC tickers index lookup + curated
KNOWN_PARENT_MAP check, no per-call SEC profile or Stooq fetch.
~10ms per name after the index is loaded once.
- /intelligence/profiler_index now runs a third Socrata pull
(5K permit pairs in window) to build a co-occurrence map. For each
contractor in the result, attaches:
.tickers.direct[] — name matches a public issuer
.tickers.associated[] — top 5 co-permit partners that resolve
to a ticker, with partner_name +
co_permits count + partner_via reason
Front-end:
- mcp-server/profiler.html — new .ticker-pill styles (3 colors per
attribution kind), pills render under the contractor name in the
table. Hover title gives the full reason path.
Verified end-to-end on the public URL:
search="tornow" → blue TGT pill, hint "Associated via co-permits
with TARGET CORPORATION (3 shared permits) —
TARGET CORP"
search="target" → green TGT × 2 (TARGET CORPORATION +
CORPORATION TARGET name variants both resolve
direct to the same issuer)
default top 200 → 15 ticker pills surface across the page including
JPM (via JPMORGAN CHASE BANK co-permits) and
parent-link tickers for the construction majors.
This commit is contained in:
parent
08c8debfff
commit
2965b68a9d
@ -1140,6 +1140,46 @@ async function fetchStooqQuote(ticker: string): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
// Cheap name-to-ticker lookup for batch use (e.g. profiler index over
|
||||
// 200 contractors). Hits the in-memory SEC tickers index and the
|
||||
// KNOWN_PARENT_MAP. No SEC profile fetch, no Stooq fetch. Returns
|
||||
// every ticker we can attribute to a name, with a tag describing the
|
||||
// link (direct = name matches a public issuer, parent = the company is
|
||||
// a subsidiary with a public parent in our curated map).
|
||||
export type LiteTicker = {
|
||||
ticker: string;
|
||||
via: "direct" | "parent" | "exact";
|
||||
matched_name?: string;
|
||||
exchange?: string;
|
||||
reason?: string;
|
||||
};
|
||||
export async function lookupTickerLite(name: string): Promise<LiteTicker[]> {
|
||||
const out: LiteTicker[] = [];
|
||||
// Direct SEC match (in-memory)
|
||||
const direct = await resolveSecTicker(name);
|
||||
if (direct) {
|
||||
out.push({
|
||||
ticker: direct.ticker,
|
||||
via: normalizeEntityName(direct.title) === normalizeEntityName(name) ? "exact" : "direct",
|
||||
matched_name: direct.title,
|
||||
});
|
||||
}
|
||||
// Parent map (curated)
|
||||
const key = normalizeEntityName(name);
|
||||
for (const [k, v] of Object.entries(KNOWN_PARENT_MAP)) {
|
||||
if (key && key.includes(normalizeEntityName(k)) && v.status === "ok" && v.parent_ticker) {
|
||||
out.push({
|
||||
ticker: v.parent_ticker,
|
||||
via: "parent",
|
||||
matched_name: v.parent_name,
|
||||
exchange: v.parent_exchange,
|
||||
reason: v.link_source,
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function fetchTickerBrief(name: string): Promise<TickerBrief> {
|
||||
const now = new Date().toISOString();
|
||||
const cached = await cacheGet(normalizeEntityName(name), "ticker");
|
||||
|
||||
@ -908,10 +908,30 @@ async function main() {
|
||||
+ `$where=${encodeURIComponent(where.join(" AND "))}`
|
||||
+ `&$group=${col}&$order=total_cost DESC&$limit=${limit}`;
|
||||
};
|
||||
// Co-occurrence query — pulls the contact_1+contact_2 pairs in
|
||||
// the window so we can attribute tickers across associations
|
||||
// ("Bob's Electric works for Target → show TGT"). Capped 5K
|
||||
// permits, ~200ms cold; resolved tickers are in-memory after.
|
||||
const buildCoQuery = () => {
|
||||
const where = [
|
||||
`reported_cost>${minCost}`,
|
||||
`issue_date>'${sinceDate.replace(/'/g, "")}'`,
|
||||
"contact_1_name IS NOT NULL",
|
||||
"contact_2_name IS NOT NULL",
|
||||
];
|
||||
if (search) {
|
||||
const s = search.replace(/'/g, "''").toUpperCase();
|
||||
where.push(`(upper(contact_1_name) like '%${s}%' OR upper(contact_2_name) like '%${s}%')`);
|
||||
}
|
||||
return `${permitUrl}?$select=contact_1_name,contact_2_name`
|
||||
+ `&$where=${encodeURIComponent(where.join(" AND "))}`
|
||||
+ `&$limit=5000`;
|
||||
};
|
||||
try {
|
||||
const [byC1, byC2] = await Promise.all([
|
||||
const [byC1, byC2, coRows] = await Promise.all([
|
||||
fetch(buildQuery("contact_1_name")).then((r) => r.json()).catch(() => []),
|
||||
fetch(buildQuery("contact_2_name")).then((r) => r.json()).catch(() => []),
|
||||
fetch(buildCoQuery()).then((r) => r.json()).catch(() => []),
|
||||
]);
|
||||
const merged: Record<string, { name: string; permits: number; total_cost: number; last_filed: string; roles: Set<string> }> = {};
|
||||
const consume = (rows: any[], role: string) => {
|
||||
@ -931,16 +951,78 @@ async function main() {
|
||||
};
|
||||
consume(byC1, "applicant");
|
||||
consume(byC2, "contractor");
|
||||
const rows = Object.values(merged)
|
||||
|
||||
// Build co-occurrence map from the permit pairs. For each
|
||||
// contractor key, count how many permits they co-appeared
|
||||
// on with each other party.
|
||||
const coMap: Record<string, Record<string, number>> = {};
|
||||
for (const r of (Array.isArray(coRows) ? coRows : []) as any[]) {
|
||||
const a = String(r.contact_1_name || "").trim();
|
||||
const b = String(r.contact_2_name || "").trim();
|
||||
if (!a || !b) continue;
|
||||
const ka = a.toUpperCase();
|
||||
const kb = b.toUpperCase();
|
||||
if (ka === kb) continue;
|
||||
(coMap[ka] = coMap[ka] || {})[kb] = (coMap[ka][kb] || 0) + 1;
|
||||
(coMap[kb] = coMap[kb] || {})[ka] = (coMap[kb][ka] || 0) + 1;
|
||||
}
|
||||
|
||||
// Attach tickers per contractor — direct, parent, and any
|
||||
// tickers attributable to top co-occurring partners ("works
|
||||
// with TARGET CORPORATION → TGT shown as associated"). Resolves
|
||||
// via the in-memory SEC tickers index + curated parent map,
|
||||
// so the cost is per-name index-lookup, not a network call.
|
||||
const { lookupTickerLite } = await import("./entity.js");
|
||||
// Memoize per-name to skip duplicate lookups across the
|
||||
// associated step.
|
||||
const tickerCache: Record<string, any[]> = {};
|
||||
const lookupCached = async (n: string) => {
|
||||
const k = n.toUpperCase();
|
||||
if (tickerCache[k]) return tickerCache[k];
|
||||
tickerCache[k] = await lookupTickerLite(n);
|
||||
return tickerCache[k];
|
||||
};
|
||||
|
||||
const rowsBase = Object.values(merged)
|
||||
.map((r) => ({ ...r, roles: Array.from(r.roles) }))
|
||||
.sort((a, b) => b.total_cost - a.total_cost)
|
||||
.slice(0, limit);
|
||||
|
||||
// Resolve tickers concurrently (in-memory ops, but Promise.all
|
||||
// keeps the code uniform with future remote ticker sources).
|
||||
const enriched = await Promise.all(rowsBase.map(async (r) => {
|
||||
const direct = await lookupCached(r.name);
|
||||
const partners = Object.entries(coMap[r.name.toUpperCase()] || {})
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 6);
|
||||
const associated: any[] = [];
|
||||
const seen = new Set(direct.map((t: any) => t.ticker));
|
||||
for (const [partnerName, occurrences] of partners) {
|
||||
const ts = await lookupCached(partnerName);
|
||||
for (const t of ts) {
|
||||
if (seen.has(t.ticker)) continue;
|
||||
seen.add(t.ticker);
|
||||
associated.push({
|
||||
ticker: t.ticker,
|
||||
via: "associated",
|
||||
partner_name: partnerName,
|
||||
co_permits: occurrences,
|
||||
partner_via: t.via,
|
||||
matched_name: t.matched_name,
|
||||
});
|
||||
if (associated.length >= 5) break;
|
||||
}
|
||||
if (associated.length >= 5) break;
|
||||
}
|
||||
return { ...r, tickers: { direct, associated } };
|
||||
}));
|
||||
|
||||
return ok({
|
||||
count: rows.length,
|
||||
count: enriched.length,
|
||||
since: sinceDate,
|
||||
min_cost: minCost,
|
||||
search,
|
||||
contractors: rows,
|
||||
contractors: enriched,
|
||||
duration_ms: Date.now() - start,
|
||||
});
|
||||
} catch (e: any) {
|
||||
|
||||
@ -30,6 +30,12 @@ td.name a:hover{text-decoration:underline}
|
||||
td.right{text-align:right;font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}
|
||||
td.role{font-size:10px;color:#8b949e}
|
||||
td.role .pill{display:inline-block;padding:2px 7px;border-radius:9px;font-size:9px;font-weight:600;background:#161b22;border:1px solid #21262d;color:#8b949e;margin-right:4px;text-transform:uppercase;letter-spacing:0.5px}
|
||||
.tickers{display:flex;gap:4px;flex-wrap:wrap;margin-top:3px}
|
||||
.ticker-pill{display:inline-block;padding:1px 7px;border-radius:5px;font-size:10px;font-weight:700;font-family:ui-monospace,SFMono-Regular,monospace;letter-spacing:0.3px;cursor:help}
|
||||
.ticker-pill.direct{background:#0d2818;border:1px solid #2ea04388;color:#3fb950}
|
||||
.ticker-pill.parent{background:#1a1410;border:1px solid #d2992288;color:#d29922}
|
||||
.ticker-pill.associated{background:#0d1830;border:1px solid #58a6ff66;color:#58a6ff}
|
||||
.ticker-pill.exact{background:#0d2818;border:1px solid #2ea043;color:#3fb950}
|
||||
.cost-band-1{color:#3fb950}
|
||||
.cost-band-2{color:#d29922}
|
||||
.cost-band-3{color:#f85149}
|
||||
@ -181,6 +187,30 @@ function render(){
|
||||
a.target='_blank'; a.rel='noopener';
|
||||
a.textContent = r.name;
|
||||
ntd.appendChild(a);
|
||||
// Ticker association pills — direct (green) = the contractor is a
|
||||
// public issuer; parent (amber) = subsidiary of a public parent;
|
||||
// associated (blue) = co-appears on permits with a public entity.
|
||||
// Shows the correlation indicator J described — when Bob's Electric
|
||||
// works permits with Target, TGT renders here as associated.
|
||||
var t = r.tickers || {direct:[], associated:[]};
|
||||
var allTk = (t.direct||[]).concat(t.associated||[]);
|
||||
if(allTk.length){
|
||||
var trk = document.createElement('div'); trk.className='tickers';
|
||||
allTk.forEach(function(x){
|
||||
var p = document.createElement('span');
|
||||
p.className = 'ticker-pill ' + (x.via||'direct');
|
||||
p.textContent = x.ticker;
|
||||
// Tooltip shows the full reason path
|
||||
var hint = x.via === 'associated'
|
||||
? 'Associated via co-permits with '+x.partner_name+' ('+(x.co_permits||0)+' shared permits)' + (x.matched_name ? ' — '+x.matched_name : '')
|
||||
: x.via === 'parent'
|
||||
? 'Subsidiary of '+(x.matched_name||x.ticker) + (x.exchange ? ' · '+x.exchange : '')
|
||||
: 'Direct match: '+(x.matched_name||r.name);
|
||||
p.title = hint;
|
||||
trk.appendChild(p);
|
||||
});
|
||||
ntd.appendChild(trk);
|
||||
}
|
||||
tr.appendChild(ntd);
|
||||
var ptd=document.createElement('td'); ptd.className='right';
|
||||
ptd.textContent=(r.permits||0).toLocaleString();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user