New API routes: - GET /api/finder — Switch→Flexoptix transceiver finder with FlexBox coding - GET /api/competitor-alerts — Competitor intelligence (price changes, new products, stock) - GET /api/forecast/:technology — Sales forecast 3/9/12/18 months + buy/wait/hold signal - POST /api/transport/plan — Transport system planner (city→city BOM with fiber providers) New MCP tools: - find_flexoptix_for_switch — Customer switch → Flexoptix products - get_competitor_alerts — Competitor monitoring - plan_transport — Network transport planning - forecast_sales — Volume/revenue prediction - generate_blog — Enhanced blog generation New DB tables (migration 013): - competitor_alerts, price_changes, flexoptix_product_map - sales_forecasts, fiber_providers, fiber_routes, cities - generated_datasheets, blog_series - Views: v_price_coverage, v_image_coverage, v_switch_flexoptix_finder Seed data (migration 014): - 25 European cities with IX/DC locations + coordinates - 15 fiber providers (euNetworks, Telia, DTAG, Colt, Zayo, etc.) - 16 fiber routes with pricing (Germany focus) Infrastructure: - Scraper scheduler: 2h Flexoptix, 4h FS.com/Optcore (was 6-8h) - Change detector for competitor price/stock monitoring - Image downloader utility with coverage tracking
202 lines
8.1 KiB
TypeScript
202 lines
8.1 KiB
TypeScript
/**
|
|
* 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<string, number> = {
|
|
'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" });
|
|
}
|
|
});
|