Update: 2026-07-06 17:51:16
This commit is contained in:
@@ -52,7 +52,7 @@ async function main(): Promise<void> {
|
||||
const opts = parseArgs();
|
||||
const startTime = Date.now();
|
||||
|
||||
console.log(`🚀 Siro Pricing Engine v1.0`);
|
||||
console.log(`🚀 Siro Pricing Engine v1.1`);
|
||||
console.log(` Mode: ${opts.mode}`);
|
||||
if (opts.competitor) console.log(` Competitor: ${opts.competitor}`);
|
||||
if (opts.country) console.log(` Country: ${opts.country}`);
|
||||
@@ -95,6 +95,67 @@ async function main(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the peak hours array from surge pattern data.
|
||||
* Returns the longest contiguous block of hours where avg multiplier > 1.05.
|
||||
* Used by both formula saving and surge insight saving.
|
||||
*/
|
||||
function computePeakHours(surgePatterns: Array<{ surgePrices: Array<{ time: string; multiplier: number }> }>): {
|
||||
peakHours: number[];
|
||||
peakStart: number;
|
||||
peakEnd: number;
|
||||
} {
|
||||
// Aggregate all route multipliers per hour of day
|
||||
const hourMults = new Map<number, number[]>();
|
||||
for (const sr of surgePatterns) {
|
||||
for (const sp of sr.surgePrices) {
|
||||
const h = parseInt(sp.time.split(':')[0]);
|
||||
if (isNaN(h)) continue;
|
||||
if (!hourMults.has(h)) hourMults.set(h, []);
|
||||
hourMults.get(h)!.push(sp.multiplier);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep only hours where the average multiplier exceeds 1.05
|
||||
const peakHours: number[] = [];
|
||||
for (const [h, mults] of hourMults) {
|
||||
const avg = mults.reduce((a, b) => a + b, 0) / mults.length;
|
||||
if (avg > 1.05) peakHours.push(h);
|
||||
}
|
||||
peakHours.sort((a, b) => a - b);
|
||||
|
||||
// Find the longest contiguous block of peak hours
|
||||
let bestStart = 0, bestEnd = 0, bestLen = 0;
|
||||
let curStart = -1, curEnd = -1;
|
||||
|
||||
for (let i = 0; i < peakHours.length; i++) {
|
||||
if (curStart < 0) {
|
||||
curStart = peakHours[i];
|
||||
curEnd = peakHours[i];
|
||||
} else if (peakHours[i] === curEnd + 1) {
|
||||
curEnd = peakHours[i];
|
||||
} else {
|
||||
if (curEnd - curStart > bestLen) {
|
||||
bestLen = curEnd - curStart;
|
||||
bestStart = curStart;
|
||||
bestEnd = curEnd;
|
||||
}
|
||||
curStart = peakHours[i];
|
||||
curEnd = peakHours[i];
|
||||
}
|
||||
}
|
||||
if (curEnd - curStart > bestLen) {
|
||||
bestLen = curEnd - curStart;
|
||||
bestStart = curStart;
|
||||
bestEnd = curEnd;
|
||||
}
|
||||
|
||||
const peakStart = bestLen > 0 ? bestStart : 0;
|
||||
const peakEnd = bestLen > 0 ? bestEnd : 23;
|
||||
|
||||
return { peakHours, peakStart, peakEnd };
|
||||
}
|
||||
|
||||
async function processCompetitor(
|
||||
pool: Pool,
|
||||
comp: CompetitorEntry,
|
||||
@@ -126,11 +187,18 @@ async function processCompetitor(
|
||||
competitorName: comp.competitor_name,
|
||||
countryCode: comp.country_code,
|
||||
cleanOutliers: true,
|
||||
surgeThreshold: 0.12,
|
||||
// surgeThresholdFraction defaults to 0.05 (5% of median price) — currency-agnostic
|
||||
tierCount: 3,
|
||||
});
|
||||
|
||||
// Save tier formulas
|
||||
// --- Compute peak hours once, reuse in both formulas and surge insights ---
|
||||
const { peakHours, peakStart, peakEnd } = report.surgePatterns.length > 0
|
||||
? computePeakHours(report.surgePatterns)
|
||||
: { peakHours: [], peakStart: 0, peakEnd: 23 };
|
||||
|
||||
const peakHoursJson = JSON.stringify(peakHours);
|
||||
|
||||
// --- Save tier formulas (includes actual peak hours) ---
|
||||
const formulas = report.tiers
|
||||
.filter(t => t.regression !== null && t.regression!.sampleCount >= 5)
|
||||
.map(tier => ({
|
||||
@@ -145,29 +213,23 @@ async function processCompetitor(
|
||||
rSquared: tier.regression!.rSquared,
|
||||
sampleCount: tier.regression!.sampleCount,
|
||||
surgeMultiplier: 1.0,
|
||||
peakHours: '[]',
|
||||
// Now populated with real peak hours instead of always '[]'
|
||||
peakHours: peakHoursJson,
|
||||
}));
|
||||
|
||||
if (formulas.length > 0) {
|
||||
await saveFormulas(pool, formulas);
|
||||
console.log(` ✅ Saved ${formulas.length} tier formulas`);
|
||||
if (peakHours.length > 0) {
|
||||
console.log(` Peak hours stored: [${peakHours.join(', ')}]`);
|
||||
}
|
||||
}
|
||||
|
||||
// Save surge insights — use the average multiplier across all detected routes
|
||||
// --- Save surge insights ---
|
||||
if (opts.mode !== 'report' && report.surgePatterns.length > 0) {
|
||||
const avgMultiplier = report.surgePatterns
|
||||
.reduce((sum, sr) => sum + sr.maxMultiplier, 0) / report.surgePatterns.length;
|
||||
|
||||
// Find peak hour range from aggregate pattern
|
||||
const allHours = report.surgePatterns.flatMap(sr =>
|
||||
sr.surgePrices
|
||||
.filter(sp => sp.multiplier > 1.05)
|
||||
.map(sp => parseInt(sp.time.split(':')[0]))
|
||||
);
|
||||
|
||||
const peakStart = allHours.length > 0 ? Math.min(...allHours) : 0;
|
||||
const peakEnd = allHours.length > 0 ? Math.max(...allHours) : 23;
|
||||
|
||||
const surgeInsights = [{
|
||||
competitorName: comp.competitor_name,
|
||||
countryCode: comp.country_code,
|
||||
@@ -178,7 +240,7 @@ async function processCompetitor(
|
||||
}];
|
||||
|
||||
await saveSurgeInsights(pool, surgeInsights);
|
||||
console.log(` ✅ Saved surge insight: avg ${(avgMultiplier).toFixed(3)}x, hours ${peakStart}:00-${peakEnd}:00`);
|
||||
console.log(` ✅ Saved surge insight: avg ${avgMultiplier.toFixed(3)}x, hours ${peakStart}:00-${peakEnd}:00`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user