feat(admin): add Filament resources for the forecasting stack
Adds three resources under a new "Forecasting" navigation group: a full-CRUD WatchedEventResource for the Layer 5 volatility detector, plus read-only WeeklyForecastResource and BacktestResource so the ridge model output and its calibration can be inspected without SQL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\Backtests\Schemas;
|
||||
|
||||
use App\Models\Backtest;
|
||||
use Filament\Infolists\Components\IconEntry;
|
||||
use Filament\Infolists\Components\KeyValueEntry;
|
||||
use Filament\Infolists\Components\TextEntry;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Schema;
|
||||
|
||||
class BacktestInfolist
|
||||
{
|
||||
public static function configure(Schema $schema): Schema
|
||||
{
|
||||
return $schema->components([
|
||||
Section::make('Run')->columns(3)->schema([
|
||||
TextEntry::make('model_version')->columnSpanFull(),
|
||||
TextEntry::make('directional_accuracy')
|
||||
->label('Accuracy')
|
||||
->state(fn (Backtest $record): string => $record->directional_accuracy === null
|
||||
? '—'
|
||||
: round((float) $record->directional_accuracy, 1).'%'),
|
||||
TextEntry::make('mae_pence')
|
||||
->label('MAE')
|
||||
->state(fn (Backtest $record): string => $record->mae_pence === null
|
||||
? '—'
|
||||
: number_format((float) $record->mae_pence, 2).'p'),
|
||||
IconEntry::make('leak_suspected')
|
||||
->label('Leak suspected')
|
||||
->boolean()
|
||||
->trueColor('danger'),
|
||||
TextEntry::make('train_start')->date('d M Y'),
|
||||
TextEntry::make('train_end')->date('d M Y'),
|
||||
TextEntry::make('eval_start')->date('d M Y'),
|
||||
TextEntry::make('eval_end')->date('d M Y'),
|
||||
TextEntry::make('ran_at')->dateTime('d M Y H:i'),
|
||||
]),
|
||||
Section::make('Calibration table')
|
||||
->description('Empirical hit rate per magnitude bin from the eval window.')
|
||||
->schema([
|
||||
KeyValueEntry::make('calibration_table')
|
||||
->hiddenLabel()
|
||||
->keyLabel('Magnitude bin')
|
||||
->valueLabel('Empirical hit rate')
|
||||
->state(fn (Backtest $record): array => collect($record->calibration_table ?? [])
|
||||
->mapWithKeys(fn ($value, $key) => [$key => round((float) $value * 100, 1).'%'])
|
||||
->all())
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Section::make('Feature spec')->schema([
|
||||
KeyValueEntry::make('features_json')
|
||||
->hiddenLabel()
|
||||
->state(fn (Backtest $record): array => self::flattenForKeyValue($record->features_json))
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
Section::make('Coefficients')
|
||||
->visible(fn (Backtest $record) => $record->coefficients_json !== null)
|
||||
->collapsed()
|
||||
->schema([
|
||||
TextEntry::make('coefficients_json')
|
||||
->hiddenLabel()
|
||||
->state(fn (Backtest $record): string => json_encode(
|
||||
$record->coefficients_json,
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
|
||||
) ?: '')
|
||||
->columnSpanFull(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyValueEntry expects a flat string-keyed map, so collapse nested arrays
|
||||
* into JSON strings rather than dropping them.
|
||||
*
|
||||
* @param array<string, mixed>|null $features
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected static function flattenForKeyValue(?array $features): array
|
||||
{
|
||||
return collect($features ?? [])
|
||||
->mapWithKeys(fn ($value, $key) => [
|
||||
(string) $key => is_scalar($value)
|
||||
? (string) $value
|
||||
: (json_encode($value, JSON_UNESCAPED_SLASHES) ?: ''),
|
||||
])
|
||||
->all();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user