where('fuel_type', $context->fuelType->value) ->where('price_effective_at', '>=', now()->subDays($lookbackDays)) ->selectRaw('DATE(price_effective_at) as day, AVG(price_pence) as avg_price') ->groupBy('day') ->orderBy('day') ->get(); if ($rows->count() < 2) { continue; } $regression = $this->linearRegression($rows->pluck('avg_price')->map(fn ($v) => (float) $v / 100)->values()->all()); if ($regression['r_squared'] >= self::R_SQUARED_THRESHOLD) { $slope = $regression['slope']; $direction = match (true) { $slope >= self::SLOPE_THRESHOLD_PENCE => 'up', $slope <= -self::SLOPE_THRESHOLD_PENCE => 'down', default => 'stable', }; $absSlope = abs($slope); $score = $direction === 'stable' ? 0.0 : min(1.0, $absSlope / self::SLOPE_SATURATION_PENCE) * ($slope > 0 ? 1 : -1); $projected = round($slope * $lookbackDays, 1); $detail = $direction === 'stable' ? "Prices flat over {$lookbackDays} days (slope: {$slope}p/day, R²={$regression['r_squared']})" : sprintf( '%s at %sp/day over %d days (R²=%s, ~%s%sp in %dd)', $slope > 0 ? 'Rising' : 'Falling', abs(round($slope, 2)), $lookbackDays, round($regression['r_squared'], 2), $projected > 0 ? '+' : '', $projected, self::PREDICTION_HORIZON_DAYS, ); if ($lookbackDays === 5) { $detail .= ' [Adaptive lookback active]'; } return [ 'score' => $score, 'confidence' => min(1.0, $regression['r_squared']), 'direction' => $direction, 'detail' => $detail, 'data_points' => $rows->count(), 'enabled' => true, 'slope' => round($slope, 3), 'r_squared' => round($regression['r_squared'], 3), ]; } } return [ 'score' => 0.0, 'confidence' => 0.0, 'direction' => 'stable', 'detail' => 'Insufficient price history or noisy data (R² below threshold)', 'data_points' => 0, 'enabled' => false, 'slope' => 0.0, 'r_squared' => 0.0, ]; } }