Files
fuel-price/.claude/rules/scoring.md
Ovidiu U d5fb7f85bd
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
feat: add Filament admin panel with migrations and design spec
- Add AdminPanelProvider mounting panel at `/admin` with `is_admin` auth guard
- Add `is_admin` boolean column to users table
- Add brent_prices and price_predictions tables with appropriate indexes
- Add comprehensive admin design spec covering resources, dashboard, navigation, and build order
- Configure default panel with amber primary color and standard middleware stack
- Add compiled Filament assets (actions.js, app.css)
2026-04-04 13:40:56 +01:00

3.5 KiB
Raw Blame History

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)

  • Read from price_predictions table — never query brent_prices directly in scoring
  • OilPriceService::generatePrediction() runs daily at 7am and writes the prediction
  • LLM (source = 'llm') is preferred; EWMA (source = 'ewma') is the fallback
  • Direction rising → mild fill_up pressure; falling → mild wait; flat → no signal
  • Points awarded proportionally to confidence: (confidence / 100) * 10
  • Weight: 10 points max

Confidence thresholds

  • Score 70100: strong signal → fire recommendation + notification
  • Score 4069: weak signal → show in dashboard only, no push/SMS/WhatsApp
  • Score 039: 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."