feat: add Laravel Fortify skill, condense API data rules, add homepage mockup
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

This commit is contained in:
Ovidiu U
2026-04-09 14:19:04 +01:00
parent 1848c070da
commit 19d5c6eb0b
7 changed files with 753 additions and 394 deletions

View File

@@ -8,52 +8,30 @@ Never guess — stay silent (no_signal) when signals conflict or data is insuffi
## The 5 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)
- **Use linear regression, not rolling averages:**
- Run least-squares regression on `(recorded_at, price_pence)` pairs
- Calculate slope (pence/day) and R² (goodness of fit, 01)
- Only use the regression result if R² ≥ 0.5 — below that, data is too noisy
- Use adaptive lookback: try 5 days first (best signal on sharp moves), fall back to 14 days if R² < 0.5
- **Falling**: slope ≤ -0.3p/day AND R² ≥ 0.5 → wait signal, points scale with slope magnitude
- **Rising**: slope ≥ +0.3p/day AND R² ≥ 0.5 → fill_up signal
- **Flat / noisy**: |slope| < 0.3 OR R² < 0.5 no signal from this source
- Store slope, R², lookback_days, and data_points in signal output
- Weight: 40 points max
### 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 (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
- Also check the inverse: if supermarket RAISED and independents haven't → mild fill_up
- Weight: 35 points max
### 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 (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 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 (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
### 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)
- Per station: calculate average hold duration (days between price changes) from history
- Requires 30+ days of history to activate
- Use as a confidence modifier, not a directional signal:
- avg hold < 2 days reduce overall confidence by 5 points (volatile, hard to predict)
- avg hold 24 days → neutral, no adjustment
- avg hold > 5 days → increase overall confidence by 5 points (predictable, sticky)
- Store avg_hold_days and data_points in signal output
- Applied after all other signals are summed (±5 points)
### 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
@@ -99,18 +77,9 @@ Reason strings are stored in `scoring_results.signals` JSON and shown in the UI
## Data quality — anomaly rejection
The Fuel Finder API contains dirty data (live example: 1369.0p/litre in national index).
Reject a price record before storing or scoring if:
- `price_pence > 25000` (over 250p/litre — physically implausible for UK pump prices)
- `price_pence < 10000` (under 100p/litre — almost certainly a decimal entry error)
- Price changed by more than 20p in a single update from the same station
(flag for review, do not use in scoring)
Log rejected records to an `anomalous_prices` table for monitoring.
Never let a dirty data point skew the regression slope or collapse R².
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` 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."
After 3 days, check if `wait` was correct (prices fell further). Store outcome in `scoring_results` for display.