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>
90 lines
3.8 KiB
PHP
90 lines
3.8 KiB
PHP
<?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();
|
|
}
|
|
}
|