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:
@@ -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}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user