/** * WS5 + WS6: Sales Forecast Engine + Price Trajectory */ import { Router } from "express"; import { pool } from "../db/client"; import { computeHypeCycle, findTechnology, TECH_GENERATIONS } from "../hype-cycle/norton-bass"; export const forecastRouter = Router(); /** * GET /api/forecast/:technology * * Returns sales forecast for 3/9/12/18 months + price trajectory + buy signal */ forecastRouter.get("/:technology", async (req, res) => { try { const techQuery = req.params.technology; const currentYear = new Date().getFullYear(); // Find technology in Norton-Bass model const tech = findTechnology(techQuery); if (!tech) { return res.status(404).json({ error: "Technology not found", available: TECH_GENERATIONS.map(t => t.name), }); } // Compute hype cycle const hype = computeHypeCycle(tech, currentYear); // Get price data from DB const priceHistory = await pool.query(` SELECT date_trunc('week', po.time) AS week, AVG(po.price) AS avg_price, MIN(po.price) AS min_price, MAX(po.price) AS max_price, COUNT(*) AS observations, po.currency FROM price_observations po JOIN transceivers t ON po.transceiver_id = t.id WHERE t.speed_gbps = $1 GROUP BY week, po.currency ORDER BY week DESC LIMIT 52 `, [tech.speedGbps]); // Compute price trajectory based on hype cycle phase const currentPrices = priceHistory.rows.length > 0 ? priceHistory.rows.map(r => parseFloat(r.avg_price)) : []; const currentASP = currentPrices.length > 0 ? currentPrices[0]! : tech.speedGbps * 0.5; // rough estimate // Price decline model based on phase const phaseDeclineRates: Record = { 'INNOVATION_TRIGGER': 0.05, 'PEAK_OF_INFLATED_EXPECTATIONS': 0.12, 'TROUGH_OF_DISILLUSIONMENT': 0.25, 'SLOPE_OF_ENLIGHTENMENT': 0.15, 'PLATEAU_OF_PRODUCTIVITY': 0.05, 'LEGACY_DECLINE': 0.03, }; const annualDecline = phaseDeclineRates[hype.phase] ?? 0.10; const monthlyDecline = 1 - Math.pow(1 - annualDecline, 1/12); const asp3m = currentASP * Math.pow(1 - monthlyDecline, 3); const asp9m = currentASP * Math.pow(1 - monthlyDecline, 9); const asp12m = currentASP * Math.pow(1 - monthlyDecline, 12); const asp18m = currentASP * Math.pow(1 - monthlyDecline, 18); // Price floor estimate (based on mature technology pricing patterns) // Typically 15-25% of peak price at full maturity const priceFloor = currentASP * 0.20; const monthsToFloor = annualDecline > 0 ? Math.ceil(Math.log(priceFloor / currentASP) / Math.log(1 - monthlyDecline)) : 999; // Volume forecast based on adoption curve const adoptionNow = hype.adoptionPct / 100; const adoption3m = Math.min(1, adoptionNow + (hype.forecast?.[0]?.adoptionPct ?? 0) / 100 * 0.25); const adoption9m = Math.min(1, adoptionNow + (hype.forecast?.[0]?.adoptionPct ?? 0) / 100 * 0.75); const adoption12m = Math.min(1, adoptionNow + (hype.forecast?.[1]?.adoptionPct ?? 0) / 100); const adoption18m = Math.min(1, adoptionNow + (hype.forecast?.[2]?.adoptionPct ?? 0) / 100); const totalMarketPorts = tech.m * 1000000; // market potential in units const marketShare = 0.03; // estimated Flexoptix-addressable share const units3m = Math.round(totalMarketPorts * adoption3m * marketShare * 0.25); const units9m = Math.round(totalMarketPorts * adoption9m * marketShare * 0.75); const units12m = Math.round(totalMarketPorts * adoption12m * marketShare); const units18m = Math.round(totalMarketPorts * adoption18m * marketShare * 1.5); // Confidence decreases with forecast horizon const conf3m = Math.min(0.95, 0.85 + (priceHistory.rows.length / 100)); const conf9m = conf3m * 0.78; const conf12m = conf3m * 0.65; const conf18m = conf3m * 0.50; // Buy signal let buySignal: string; let signalReason: string; if (hype.phase === 'SLOPE_OF_ENLIGHTENMENT' || hype.phase === 'PLATEAU_OF_PRODUCTIVITY') { buySignal = 'BUY_NOW'; signalReason = `${tech.name} is in ${hype.phase.replace(/_/g, ' ').toLowerCase()} — prices near floor, volume growing, stable supply chain.`; } else if (hype.phase === 'TROUGH_OF_DISILLUSIONMENT') { buySignal = 'WAIT'; signalReason = `${tech.name} prices dropping >10%/quarter. Wait for trough bottom (estimated ${Math.ceil(monthsToFloor * 0.3)} months).`; } else if (hype.phase === 'PEAK_OF_INFLATED_EXPECTATIONS') { buySignal = 'WAIT'; signalReason = `${tech.name} is at peak hype — prices will drop significantly. Only buy if urgent.`; } else if (hype.phase === 'INNOVATION_TRIGGER') { buySignal = 'HOLD'; signalReason = `${tech.name} is early-stage — limited availability, premium pricing. Wait unless you need bleeding-edge.`; } else { buySignal = 'HOLD'; signalReason = `${tech.name} is in legacy/decline — consider migrating to next generation.`; } // Store forecast in DB await pool.query(` INSERT INTO sales_forecasts ( technology, speed_gbps, form_factor, forecast_3m_units, forecast_3m_revenue, forecast_9m_units, forecast_9m_revenue, forecast_12m_units, forecast_12m_revenue, forecast_18m_units, forecast_18m_revenue, current_asp, asp_3m, asp_12m, price_floor, months_to_floor, confidence_3m, confidence_9m, confidence_12m, confidence_18m, buy_signal, signal_reason, data_points ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23) `, [ tech.name, tech.speedGbps, tech.formFactor, units3m, units3m * asp3m, units9m, units9m * asp9m, units12m, units12m * asp12m, units18m, units18m * asp18m, currentASP, asp3m, asp12m, priceFloor, monthsToFloor, conf3m, conf9m, conf12m, conf18m, buySignal, signalReason, priceHistory.rows.length, ]).catch(() => {}); // Non-critical res.json({ technology: tech.name, speed_gbps: tech.speedGbps, form_factor: tech.formFactor, hype_cycle: { phase: hype.phase, position_pct: hype.positionPct, adoption_pct: hype.adoptionPct, }, forecasts: { "3_months": { units: units3m, revenue_eur: Math.round(units3m * asp3m), confidence: Math.round(conf3m * 100) / 100 }, "9_months": { units: units9m, revenue_eur: Math.round(units9m * asp9m), confidence: Math.round(conf9m * 100) / 100 }, "12_months": { units: units12m, revenue_eur: Math.round(units12m * asp12m), confidence: Math.round(conf12m * 100) / 100 }, "18_months": { units: units18m, revenue_eur: Math.round(units18m * asp18m), confidence: Math.round(conf18m * 100) / 100 }, }, price_trajectory: { current_asp: Math.round(currentASP * 100) / 100, asp_3m: Math.round(asp3m * 100) / 100, asp_9m: Math.round(asp9m * 100) / 100, asp_12m: Math.round(asp12m * 100) / 100, asp_18m: Math.round(asp18m * 100) / 100, price_floor: Math.round(priceFloor * 100) / 100, months_to_floor: Math.max(0, monthsToFloor), annual_decline_pct: Math.round(annualDecline * 100), }, buy_signal: { signal: buySignal, reason: signalReason, }, price_history: priceHistory.rows.slice(0, 12), model: "Norton-Bass Multigenerational Diffusion v1", }); } catch (err) { console.error("Forecast error:", err); res.status(500).json({ error: "Internal server error" }); } }); /** * GET /api/forecast * * Overview of all technology forecasts */ forecastRouter.get("/", async (_req, res) => { try { const currentYear = new Date().getFullYear(); const results = TECH_GENERATIONS.map(tech => { const hype = computeHypeCycle(tech, currentYear); return { technology: tech.name, speed_gbps: tech.speedGbps, form_factor: tech.formFactor, phase: hype.phase, adoption_pct: hype.adoptionPct, position_pct: hype.positionPct, }; }); res.json({ technologies: results }); } catch (err) { res.status(500).json({ error: "Internal server error" }); } });