# Scoring Engine (AlertScoringService) ## Purpose Produces a "fill up now or wait?" recommendation per user based on their local station history. Output is one of: `fill_up`, `wait`, `no_signal`. Never guess — stay silent (no_signal) when signals conflict or data is insufficient. ## The 4 signals (in priority order) ### Signal 1 — Local price trend (HIGHEST WEIGHT) - Query `station_prices` for user's nearest 5 stations (within 5km of user lat/lng) - Use last 14 days of history for `e10` (or user's preferred fuel type) - Calculate 3-day rolling average vs 7-day rolling average - **Falling**: 3-day avg < 7-day avg by ≥ 0.5p → positive wait signal - **Rising**: 3-day avg > 7-day avg by ≥ 0.5p → fill_up signal - **Flat**: difference < 0.5p → neutral, no signal - Weight: 40 points max ### Signal 2 — Supermarket anchor effect (HIGH WEIGHT) - Find nearest supermarket station (is_supermarket = 1) within 10km - Check if supermarket cut price in last 48 hours (> 1p drop) - Check if nearest non-supermarket stations have NOT yet followed - If supermarket cut AND independents haven't moved → strong wait signal - Weight: 35 points max ### Signal 3 — Day-of-week pattern (MEDIUM WEIGHT — needs 8+ weeks data) - Per station: average price by day-of-week over last 90 days - Only activate if station has 56+ days of history - If today is statistically 1.5p+ cheaper than weekly average → mild fill_up - If today is statistically 1.5p+ more expensive → mild wait - Weight: 15 points max ### Signal 4 — Brent crude direction (LOW WEIGHT) - Fetched daily from FRED API, stored in a simple `brent_prices` table - 5-day trend: rising ≥ 3% → mild fill_up pressure; falling ≥ 3% → mild wait - Weight: 10 points max ## Confidence thresholds - Score 70–100: strong signal → fire recommendation + notification - Score 40–69: weak signal → show in dashboard only, no push/SMS/WhatsApp - Score 0–39: no_signal → stay silent entirely **Only send notifications when confidence ≥ 70.** Never spam. ## Output (stored in scoring_results) ```php [ 'recommendation' => 'wait', // fill_up | wait | no_signal 'confidence' => 78, // 0-100 'signals' => [ 'trend' => ['direction' => 'falling', 'points' => 32], 'supermarket' => ['triggered' => true, 'points' => 35], 'day_pattern' => ['triggered' => false, 'points' => 0], 'brent' => ['direction' => 'flat', 'points' => 0], ], 'local_avg_pence' => 14380, // 143.80p 'trend_delta' => -2.3, // pence change over 7 days ] ``` ## Human-readable reason strings Always generate a plain-English reason for the recommendation: - "Prices near you have been falling for 6 days. Tesco {station} cut 3p yesterday — independents usually follow within 48 hours." - "Prices are rising in your area — filling up today avoids paying more later." - "No clear pattern this week — fill up at the cheapest station near you now." Reason strings are stored in `scoring_results.signals` JSON and shown in the UI and notifications. ## Accuracy self-tracking After 3 days, check if `wait` recommendation was correct (prices did fall further). Store outcome in `scoring_results` for future display: "This signal has been right X% of the time in your area."