From b720afc92c469f62d464f02ea1de6ff4538c6f80 Mon Sep 17 00:00:00 2001 From: Rene Fichtmueller Date: Thu, 4 Jun 2026 20:47:30 +0000 Subject: [PATCH] feat(hype-cycle): add /market-signals endpoint (data-driven per-tech signal score + drivers + recommendation) --- packages/api/src/routes/hype-cycle.ts | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/api/src/routes/hype-cycle.ts b/packages/api/src/routes/hype-cycle.ts index 567ea84..7ee68af 100644 --- a/packages/api/src/routes/hype-cycle.ts +++ b/packages/api/src/routes/hype-cycle.ts @@ -198,6 +198,74 @@ hypeCycleRouter.get("/analysis", async (_req: Request, res: Response) => { }); // GET /api/hype-cycle/:tech — Specific technology detail (must be last!) +// GET /api/hype-cycle/market-signals — data-driven per-technology market signal score + drivers + recommendation +hypeCycleRouter.get("/market-signals", async (_req: Request, res: Response) => { + try { + const yearParam = q("year", _req); + const year = yearParam ? parseInt(yearParam) : new Date().getFullYear(); + const overridesMap = await getDataDrivenOverrides(); + const speedMetrics = await getSpeedClassMetrics(); + const metricsBySpeed = new Map(speedMetrics.map((m) => [m.speedGbps, m])); + const maxSku = Math.max(1, ...speedMetrics.map((m) => m.skuCount)); + const allTechs = [...TECH_GENERATIONS, ...SPECIAL_TECHS]; + + const technologies = allTechs.map((tech) => { + const r = computeHypeCycle(tech, year, overridesMap.get(tech.speedGbps)); + const sm = metricsBySpeed.get(tech.speedGbps); + const adoption = Math.max(0, Math.round(r.adoptionPct)); + const composite = Math.max(0, Math.round(r.compositeScore)); + const depth = sm ? Math.round(100 * Math.min(1, sm.skuCount / maxSku)) : 0; + const marketSignalScore = Math.max(0, Math.min(100, Math.round(0.4 * adoption + 0.3 * composite + 0.3 * depth))); + + const drivers: string[] = []; + drivers.push(adoption + "% modeled adoption"); + if (sm && sm.skuCount) drivers.push(sm.skuCount + " SKUs / " + sm.vendorCount + " vendors"); + if (sm && sm.avgPrice) drivers.push("avg $" + Math.round(sm.avgPrice)); + drivers.push("phase: " + r.phaseLabel); + + const phase = String(r.phaseLabel || ""); + let recommendation = { label: "Monitor", detail: "Insufficient market momentum — keep watching.", color: "#94a3b8" }; + if (/Plateau/i.test(phase)) recommendation = { label: "Commodity — negotiate", detail: "Mature, broad supply (" + (sm ? sm.vendorCount : 0) + " vendors). Push for price.", color: "#0ea5e9" }; + else if (/Slope/i.test(phase)) recommendation = { label: "Mainstream — buy", detail: "Mainstreaming with falling prices and rising supply.", color: "#16a34a" }; + else if (/Trough/i.test(phase)) recommendation = { label: "Maturing — opportunity", detail: "Past the hype; early mainstream pricing forming.", color: "#ca8a04" }; + else if (/Peak/i.test(phase)) recommendation = { label: "Hype peak — caution", detail: "Elevated expectations; supply and price still volatile.", color: "#f97316" }; + else if (/Innovation|Trigger/i.test(phase)) recommendation = { label: "Emerging — pilot", detail: "Early window; limited supply, premium pricing.", color: "#ca8a04" }; + + return { + technology: tech.name, + phase: r.phaseLabel, + marketSignalScore, + adoptionPct: adoption, + compositeScore: composite, + skuCount: sm ? sm.skuCount : 0, + vendorCount: sm ? sm.vendorCount : 0, + avgPrice: sm && sm.avgPrice ? Math.round(sm.avgPrice * 100) / 100 : null, + drivers, + recommendation, + }; + }); + + technologies.sort((a, b) => b.marketSignalScore - a.marketSignalScore); + + const totalSkus = speedMetrics.reduce((acc, m) => acc + m.skuCount, 0); + const totalVendors = speedMetrics.length ? Math.max(...speedMetrics.map((m) => m.vendorCount)) : 0; + const avgSignal = technologies.length ? Math.round(technologies.reduce((acc, t) => acc + t.marketSignalScore, 0) / technologies.length) : 0; + const globalContext = { + totalSkus, + totalVendors, + avgMarketSignal: avgSignal, + strongest: technologies.length ? technologies[0].technology : null, + strongestScore: technologies.length ? technologies[0].marketSignalScore : null, + generatedAt: new Date().toISOString(), + }; + + res.json({ success: true, year, technologies, globalContext }); + } catch (err) { + console.error("market-signals error:", err); + res.status(500).json({ success: false, error: "Failed to compute market signals" }); + } +}); + hypeCycleRouter.get("/:tech", (req: Request, res: Response) => { const techQuery = String(req.params.tech); const yearParam = q("year", req);