feat(hype-cycle): add /market-signals endpoint (data-driven per-tech signal score + drivers + recommendation)
This commit is contained in:
parent
b5a961c3bd
commit
b720afc92c
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user