Includes verified API authentication flow, correct base URL, all DB table schemas for stations, current prices, history, and archive. Fuel types corrected to match live API (B7_STANDARD, B7_PREMIUM). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3.3 KiB
3.3 KiB
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_pricesfor 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_pricestable - 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)
[
'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."