chore: retire legacy oil prediction pipeline

Removes everything that was made redundant by the new forecasting
stack. Per docs/superpowers/specs/2026-05-01-prediction-rebuild-design.md,
this was the cleanup planned at the end of Phase 4.

Deleted services and code:
- App\Services\Prediction\Signals\* (the old six-signal aggregator —
  trend, supermarket, day-of-week, brand-behaviour, stickiness,
  regional-momentum, oil — replaced by RidgeRegressionModel).
- App\Services\NationalFuelPredictionService (the post-Phase-4 thin
  shim; StationSearchService now depends on WeeklyForecastService
  directly, set up in the previous commit).
- App\Services\LlmPrediction\* (AbstractLlmPredictionProvider plus
  the four provider implementations — Anthropic, OpenAI, Gemini, and
  the OilPredictionProvider router. Replaced by LlmOverlayService).
- App\Services\BrentPricePredictor and App\Services\Ewma. The Ewma
  helper had no callers left after BrentPricePredictor went.
- App\Models\PricePrediction and its factory.
- App\Console\Commands\PredictOilPrices (the oil:predict command).
- App\Filament\Resources\OilPredictionResource and its Pages.

Schema and dashboard:
- Drop the price_predictions table via a new migration.
- Repoint the Filament StatsOverviewWidget tile from PricePrediction
  to WeeklyForecast so the dashboard reflects the new pipeline.
- Remove the OilPredictionProvider binding from AppServiceProvider.

Test cleanup:
- Delete tests for every retired service.
- Update StatsOverviewWidgetTest to seed weekly_forecasts instead of
  price_predictions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-05-03 08:40:28 +01:00
parent ddd591ad47
commit 203200acb9
32 changed files with 61 additions and 2727 deletions

View File

@@ -1,141 +0,0 @@
<?php
namespace App\Filament\Resources;
use App\Enums\PredictionSource;
use App\Enums\TrendDirection;
use App\Filament\NavigationGroup;
use App\Filament\Resources\OilPredictionResource\Pages\ListOilPredictions;
use App\Filament\Resources\OilPredictionResource\Pages\ViewOilPrediction;
use App\Models\PricePrediction;
use Filament\Actions\ViewAction;
use Filament\Forms\Components\DatePicker;
use Filament\Infolists\Components\TextEntry;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
class OilPredictionResource extends Resource
{
protected static ?string $model = PricePrediction::class;
protected static string|\UnitEnum|null $navigationGroup = NavigationGroup::Data;
protected static ?string $navigationLabel = 'Oil Predictions';
protected static ?int $navigationSort = 3;
public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('predicted_for')
->date('d M Y')
->sortable(),
TextColumn::make('source')
->badge()
->formatStateUsing(fn (PredictionSource $state) => match ($state) {
PredictionSource::Llm => 'LLM',
PredictionSource::LlmWithContext => 'LLM + Context',
PredictionSource::Ewma => 'EWMA',
})
->color(fn (PredictionSource $state) => match ($state) {
PredictionSource::Llm => 'success',
PredictionSource::LlmWithContext => 'warning',
PredictionSource::Ewma => 'info',
}),
TextColumn::make('direction')
->badge()
->color(fn (TrendDirection $state) => match ($state) {
TrendDirection::Rising => 'danger',
TrendDirection::Falling => 'success',
TrendDirection::Flat => 'gray',
}),
TextColumn::make('confidence')
->suffix('%')
->sortable(),
TextColumn::make('reasoning')
->limit(60)
->placeholder('—'),
TextColumn::make('generated_at')
->dateTime('d M Y H:i')
->sortable(),
])
->defaultSort('predicted_for', 'desc')
->filters([
SelectFilter::make('source')
->options([
PredictionSource::Llm->value => 'LLM',
PredictionSource::LlmWithContext->value => 'LLM + Context',
PredictionSource::Ewma->value => 'EWMA',
]),
SelectFilter::make('direction')
->options([
TrendDirection::Rising->value => 'Rising',
TrendDirection::Falling->value => 'Falling',
TrendDirection::Flat->value => 'Flat',
]),
Filter::make('predicted_for')
->schema([
DatePicker::make('from')->label('From'),
DatePicker::make('until')->label('Until'),
])
->query(function (Builder $query, array $data) {
$query
->when($data['from'], fn ($q, $d) => $q->whereDate('predicted_for', '>=', $d))
->when($data['until'], fn ($q, $d) => $q->whereDate('predicted_for', '<=', $d));
}),
])
->recordActions([
ViewAction::make(),
]);
}
public static function infolist(Schema $schema): Schema
{
return $schema->components([
Section::make('Prediction')->schema([
TextEntry::make('predicted_for')->date('d M Y'),
TextEntry::make('source')
->badge()
->formatStateUsing(fn (PredictionSource $state) => match ($state) {
PredictionSource::Llm => 'LLM',
PredictionSource::LlmWithContext => 'LLM + Context',
PredictionSource::Ewma => 'EWMA',
})
->color(fn (PredictionSource $state) => match ($state) {
PredictionSource::Llm => 'success',
PredictionSource::LlmWithContext => 'warning',
PredictionSource::Ewma => 'info',
}),
TextEntry::make('direction')
->badge()
->color(fn (TrendDirection $state) => match ($state) {
TrendDirection::Rising => 'danger',
TrendDirection::Falling => 'success',
TrendDirection::Flat => 'gray',
}),
TextEntry::make('confidence')->suffix('%'),
TextEntry::make('generated_at')->dateTime('d M Y H:i:s'),
])->columns(3),
Section::make('Reasoning')->schema([
TextEntry::make('reasoning')
->columnSpanFull()
->placeholder('No reasoning recorded'),
]),
]);
}
public static function getPages(): array
{
return [
'index' => ListOilPredictions::route('/'),
'view' => ViewOilPrediction::route('/{record}'),
];
}
}

View File

@@ -1,42 +0,0 @@
<?php
namespace App\Filament\Resources\OilPredictionResource\Pages;
use App\Filament\Resources\OilPredictionResource;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\ListRecords;
use Illuminate\Support\Facades\Artisan;
class ListOilPredictions extends ListRecords
{
protected static string $resource = OilPredictionResource::class;
protected function getHeaderActions(): array
{
return [
Action::make('runPrediction')
->label('Run Prediction Now')
->icon('heroicon-o-cpu-chip')
->requiresConfirmation()
->modalHeading('Run oil price prediction?')
->modalDescription('Generates a new prediction from the stored Brent prices. Runs even if a prediction already exists for the latest price.')
->action(function () {
$result = Artisan::call('oil:predict', ['--force' => true]);
if ($result === 0) {
Notification::make()
->title('Prediction generated successfully')
->success()
->send();
} else {
Notification::make()
->title('Prediction failed')
->body('Check API Logs for details.')
->danger()
->send();
}
}),
];
}
}

View File

@@ -1,16 +0,0 @@
<?php
namespace App\Filament\Resources\OilPredictionResource\Pages;
use App\Filament\Resources\OilPredictionResource;
use Filament\Resources\Pages\ViewRecord;
class ViewOilPrediction extends ViewRecord
{
protected static string $resource = OilPredictionResource::class;
protected function getHeaderActions(): array
{
return [];
}
}

View File

@@ -3,10 +3,10 @@
namespace App\Filament\Widgets;
use App\Models\ApiLog;
use App\Models\PricePrediction;
use App\Models\Search;
use App\Models\Station;
use App\Models\User;
use App\Models\WeeklyForecast;
use Carbon\Carbon;
use Filament\Widgets\StatsOverviewWidget as BaseWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
@@ -21,7 +21,7 @@ class StatsOverviewWidget extends BaseWidget
$this->usersStat(),
$this->searchesStat(),
$this->stationsStat(),
$this->oilPredictionStat(),
$this->weeklyForecastStat(),
$this->apiErrorsStat(),
];
}
@@ -56,23 +56,23 @@ class StatsOverviewWidget extends BaseWidget
->color('success');
}
private function oilPredictionStat(): Stat
private function weeklyForecastStat(): Stat
{
$prediction = PricePrediction::bestFirst()->latest('generated_at')->first();
$forecast = WeeklyForecast::query()->latest('generated_at')->first();
if ($prediction === null) {
return Stat::make('Latest oil prediction', 'None')
if ($forecast === null) {
return Stat::make('Latest weekly forecast', 'None')
->icon('heroicon-o-beaker')
->color('gray');
}
$ageHours = $prediction->generated_at->diffInHours(now());
$color = $ageHours > 24 ? 'warning' : 'success';
$value = $prediction->direction->label().' · '.$prediction->confidence.'%';
$ageHours = $forecast->generated_at->diffInHours(now());
$color = $ageHours > 168 ? 'warning' : 'success'; // weekly forecast → stale after a week
$directionLabel = ucfirst($forecast->direction);
$value = $directionLabel.' · '.$forecast->ridge_confidence.'%';
return Stat::make('Latest oil prediction', $value)
->description('Generated '.$prediction->generated_at->diffForHumans())
->url(route('filament.admin.resources.oil-predictions.index'))
return Stat::make('Latest weekly forecast', $value)
->description('For week of '.$forecast->forecast_for->toDateString())
->icon('heroicon-o-beaker')
->color($color);
}