Files
fuel-price/.claude/rules/scoring.md
Ovidiu U 19d5c6eb0b
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 Laravel Fortify skill, condense API data rules, add homepage mockup
2026-04-09 14:19:04 +01:00

85 lines
3.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 5 signals (in priority order)
### Signal 1 — Local price trend (40 pts max)
- Nearest 5 stations within 5km; user's preferred fuel type
- Least-squares regression on `(recorded_at, price_pence)`; adaptive lookback: 5 days first, fall back to 14 if R² < 0.5
- Slope -0.3p/day AND R² 0.5 wait; slope +0.3p/day AND R² 0.5 fill_up; otherwise no signal
- Store: slope, R², lookback_days, data_points
### Signal 2 — Supermarket anchor effect (35 pts max)
- Nearest supermarket (is_supermarket = 1) within 10km
- Supermarket cut > 1p in last 48h AND independents haven't followed → wait
- Inverse (supermarket raised, independents haven't) → mild fill_up
### Signal 3 — Day-of-week pattern (15 pts max)
- Requires 56+ days of station history; average price by day-of-week over last 90 days
- Today 1.5p+ below weekly average → mild fill_up; 1.5p+ above → mild wait
### Signal 4 — Brent crude direction (10 pts max)
- Read from `price_predictions` table only (never query `brent_prices` in scoring)
- LLM (`source='llm'`) preferred; EWMA fallback. Points = `(confidence / 100) * 10`
- `rising` → fill_up; `falling` → wait; `flat` → no signal
### Signal 5 — Price stickiness (confidence modifier, ±5 pts)
- Requires 30+ days history. Applied after all signals are summed.
- avg hold < 2 days -5 pts; 24 days 0; > 5 days → +5 pts
- Store: avg_hold_days, data_points
## 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)
```php
[
'recommendation' => 'wait',
'confidence' => 78,
'signals' => [
'trend' => [
'direction' => 'falling',
'slope' => -1.07, // pence per day
'r_squared' => 0.96,
'lookback_days' => 5,
'data_points' => 5,
'points' => 32,
],
'supermarket' => ['triggered' => true, 'points' => 35],
'day_pattern' => ['triggered' => false, 'points' => 0],
'brent' => ['direction' => 'flat', 'points' => 0],
'stickiness' => ['avg_hold_days' => 2.8, 'modifier' => 0],
],
'local_avg_pence' => 14380, // 143.80p
'trend_delta' => -2.3, // pence change over lookback period
]
```
## Human-readable reason strings
Always generate a plain-English reason for the recommendation:
- "Prices near you have been falling at 1.1p/day for 5 days. Tesco {station} cut 3p yesterday — independents usually follow within 48 hours."
- "Prices are rising sharply in your area (+7.5p expected this week) — 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.
## Data quality — anomaly rejection
Reject before storing or scoring: `price_pence > 25000` or `< 10000`, or single-update change > 20p (flag for review).
Log to `anomalous_prices` table. Never let dirty data skew regression slope or collapse R².
## Accuracy self-tracking
After 3 days, check if `wait` was correct (prices fell further). Store outcome in `scoring_results` for display.