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/: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) => {
|
hypeCycleRouter.get("/:tech", (req: Request, res: Response) => {
|
||||||
const techQuery = String(req.params.tech);
|
const techQuery = String(req.params.tech);
|
||||||
const yearParam = q("year", req);
|
const yearParam = q("year", req);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user