select(['forecast_for', 'model_version']) ->get() ->mapWithKeys(fn ($r): array => [$r->forecast_for.'|'.$r->model_version => true]) ->all(); $candidates = WeeklyForecast::query() ->where('forecast_for', '<=', now()->toDateString()) ->orderBy('forecast_for') ->get(); foreach ($candidates as $forecast) { $key = $forecast->forecast_for->toDateString().'|'.$forecast->model_version; if (isset($existing[$key])) { continue; } $actualDelta = $this->actualDeltaPence($forecast->forecast_for->toDateString()); if ($actualDelta === null) { continue; } $actualClass = $this->classifyDirection($actualDelta); $absError = (int) round(abs($forecast->magnitude_pence - $actualDelta)); DB::table('forecast_outcomes')->insert([ 'forecast_for' => $forecast->forecast_for->toDateString(), 'model_version' => $forecast->model_version, 'predicted_class' => $forecast->direction, 'actual_class' => $actualClass, 'correct' => $forecast->direction === $actualClass, 'abs_error_pence' => $absError, 'resolved_at' => now(), ]); $resolved++; } return $resolved; } private function actualDeltaPence(string $targetDate): ?float { $current = DB::table('weekly_pump_prices') ->where('date', $targetDate) ->value('ulsp_pence'); $previous = DB::table('weekly_pump_prices') ->where('date', date('Y-m-d', strtotime($targetDate.' -7 days'))) ->value('ulsp_pence'); if ($current === null || $previous === null) { return null; } return (float) ($current - $previous); } private function classifyDirection(float $deltaPence): string { return match (true) { $deltaPence > self::FLAT_THRESHOLD_PENCE_X100 => 'rising', $deltaPence < -self::FLAT_THRESHOLD_PENCE_X100 => 'falling', default => 'flat', }; } }