fix: use categorical regime fundamentals
This commit is contained in:
@@ -16,10 +16,13 @@ import {
|
||||
updateRegimeFundamentals,
|
||||
} from '../api/regime';
|
||||
import type {
|
||||
CapexState,
|
||||
EventStudyReport,
|
||||
GoodNewsReaction,
|
||||
RegimeBand,
|
||||
RegimeConfig,
|
||||
RegimeFundamentals,
|
||||
RegimeFundamentalsUpdate,
|
||||
RegimeReading,
|
||||
} from '../lib/types';
|
||||
|
||||
@@ -207,6 +210,15 @@ function EventStudyPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
const SELECT_CLASS = 'rounded-md border border-white/[0.08] bg-white/[0.03] px-2 py-1.5 text-xs text-gray-200';
|
||||
|
||||
const CAPEX_OPTIONS: { value: CapexState; label: string }[] = [
|
||||
{ value: 'raising', label: 'Raising' },
|
||||
{ value: 'holding', label: 'Holding' },
|
||||
{ value: 'cutting', label: 'Cutting' },
|
||||
{ value: 'unknown', label: 'Unknown' },
|
||||
];
|
||||
|
||||
function FundamentalsEditor({
|
||||
data,
|
||||
onSave,
|
||||
@@ -215,15 +227,19 @@ function FundamentalsEditor({
|
||||
refreshing,
|
||||
}: {
|
||||
data: RegimeFundamentals;
|
||||
onSave: (body: { f1_score?: number; f3_score?: number; locked?: boolean }) => void;
|
||||
onSave: (body: RegimeFundamentalsUpdate) => void;
|
||||
onRefresh: () => void;
|
||||
saving: boolean;
|
||||
refreshing: boolean;
|
||||
}) {
|
||||
const [f1, setF1] = useState(data.f1_score ?? 0);
|
||||
const [f3, setF3] = useState(data.f3_score ?? 0);
|
||||
const [capex, setCapex] = useState<Record<string, CapexState>>(() => ({ ...data.capex }));
|
||||
const [reaction, setReaction] = useState<GoodNewsReaction>(data.good_news_stock_down);
|
||||
const knownCapex = Object.values(capex).filter((state) => state !== 'unknown');
|
||||
const cutting = knownCapex.filter((state) => state === 'cutting').length;
|
||||
const derivedF1 = knownCapex.length >= 3 ? Math.round((cutting / knownCapex.length) * 1000) / 10 : null;
|
||||
const derivedF3 = reaction === 'yes' ? 100 : reaction === 'no' ? 0 : null;
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs text-gray-500">
|
||||
<span>Source: {data.source}</span>
|
||||
{data.fetched_at && <span>· fetched {new Date(data.fetched_at).toLocaleDateString()}</span>}
|
||||
@@ -231,18 +247,40 @@ function FundamentalsEditor({
|
||||
{data.locked && <Badge label="locked" variant="manual" />}
|
||||
</div>
|
||||
{data.reasoning && <p className="text-xs leading-relaxed text-gray-400">{data.reasoning}</p>}
|
||||
{[
|
||||
['F1 · Capex cuts', f1, setF1],
|
||||
['F3 · Good news, stock down', f3, setF3],
|
||||
].map(([label, value, setter]) => (
|
||||
<label key={String(label)} className="flex items-center gap-3 text-xs text-gray-400">
|
||||
<span className="w-52 shrink-0">{String(label)}</span>
|
||||
<input type="range" min={0} max={100} value={Number(value)} onChange={(event) => (setter as (v: number) => void)(Number(event.target.value))} className="h-2 flex-1 accent-blue-500" />
|
||||
<span className="w-8 text-right num text-gray-300">{Number(value)}</span>
|
||||
</label>
|
||||
))}
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-3 text-xs">
|
||||
<span className="font-medium text-gray-300">F1 · Capex guidance by hyperscaler</span>
|
||||
<span className="num text-gray-500">score {derivedF1 ?? 'n/a'}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{Object.entries(capex).map(([symbol, state]) => (
|
||||
<label key={symbol} className="flex items-center justify-between gap-2 rounded-md bg-white/[0.02] px-2.5 py-2 text-xs text-gray-400">
|
||||
<span className="font-mono text-gray-300">{symbol}</span>
|
||||
<select
|
||||
className={SELECT_CLASS}
|
||||
value={state}
|
||||
onChange={(event) => setCapex((current) => ({ ...current, [symbol]: event.target.value as CapexState }))}
|
||||
>
|
||||
{CAPEX_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-1.5 text-[11px] text-gray-600">Raising/holding = 0 stress; cutting = 100; at least three known names required.</p>
|
||||
</div>
|
||||
<label className="flex items-center justify-between gap-3 text-xs text-gray-400">
|
||||
<span>
|
||||
<span className="font-medium text-gray-300">F3 · Good news, stock down</span>
|
||||
<span className="ml-2 num text-gray-600">score {derivedF3 ?? 'n/a'}</span>
|
||||
</span>
|
||||
<select className={SELECT_CLASS} value={reaction} onChange={(event) => setReaction(event.target.value as GoodNewsReaction)}>
|
||||
<option value="yes">Yes · stress</option>
|
||||
<option value="no">No · ordinary</option>
|
||||
<option value="mixed">Mixed · unavailable</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button className="btn-primary px-3 py-1.5 text-sm disabled:opacity-50" disabled={saving} onClick={() => onSave({ f1_score: f1, f3_score: f3, locked: true })}>Save override</button>
|
||||
<button className="btn-primary px-3 py-1.5 text-sm disabled:opacity-50" disabled={saving} onClick={() => onSave({ capex, good_news_stock_down: reaction, locked: true })}>Save observations</button>
|
||||
<button className="rounded-lg px-3 py-1.5 text-sm text-gray-400 hover:bg-white/[0.04] disabled:opacity-50" disabled={refreshing} onClick={onRefresh}>{refreshing ? 'Refreshing…' : 'Refresh via LLM'}</button>
|
||||
{data.locked && <button className="rounded-lg px-3 py-1.5 text-sm text-gray-400 hover:bg-white/[0.04]" onClick={() => onSave({ locked: false })}>Unlock</button>}
|
||||
</div>
|
||||
@@ -266,7 +304,7 @@ function ConfigEditor({ data, onSave, saving }: { data: RegimeConfig; onSave: (u
|
||||
<span>days</span>
|
||||
</label>
|
||||
<p className="text-[11px] text-gray-600">Changing the basket resets its freeze date and silently reseeds quadrant alerts.</p>
|
||||
<button className="btn-primary px-3 py-1.5 text-sm disabled:opacity-50" disabled={saving || symbols.length < 20} onClick={() => onSave({ breadth_basket: symbols, fundamental_staleness_days: staleness })}>Save monitor settings</button>
|
||||
<button className="btn-primary px-3 py-1.5 text-sm disabled:opacity-50" disabled={saving || symbols.length < 20} onClick={() => onSave({ breadth_basket: symbols, fundamental_staleness_days: staleness })}>Save basket & freshness</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -280,16 +318,22 @@ function AdminControls() {
|
||||
const saveFundamentals = useMutation({ mutationFn: updateRegimeFundamentals, onSuccess: invalidate });
|
||||
const saveConfig = useMutation({ mutationFn: updateRegimeConfig, onSuccess: invalidate });
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Disclosure summary="Admin · Fundamental observations">
|
||||
{fundamentals.data && <FundamentalsEditor key={fundamentals.dataUpdatedAt} data={fundamentals.data} onSave={(body) => saveFundamentals.mutate(body)} onRefresh={() => refresh.mutate()} saving={saveFundamentals.isPending} refreshing={refresh.isPending} />}
|
||||
{refresh.isError && <Callout variant="error">Refresh failed: {(refresh.error as Error).message}</Callout>}
|
||||
</Disclosure>
|
||||
<Disclosure summary="Admin · Fixed basket & freshness">
|
||||
{config.data && <ConfigEditor key={config.dataUpdatedAt} data={config.data} onSave={(updates) => saveConfig.mutate(updates)} saving={saveConfig.isPending} />}
|
||||
{saveConfig.isError && <Callout variant="error">Save failed: {(saveConfig.error as Error).message}</Callout>}
|
||||
</Disclosure>
|
||||
</div>
|
||||
<Disclosure summary="Admin · Monitor settings">
|
||||
<div className="grid gap-5 xl:grid-cols-2 xl:gap-6">
|
||||
<section className="border-b border-white/[0.06] pb-5 xl:border-b-0 xl:border-r xl:pb-0 xl:pr-6">
|
||||
<div className="mb-3 text-[11px] uppercase tracking-wider text-gray-500">Fundamental observations</div>
|
||||
{fundamentals.isLoading && <SkeletonCard className="h-36" />}
|
||||
{fundamentals.data && <FundamentalsEditor key={fundamentals.dataUpdatedAt} data={fundamentals.data} onSave={(body) => saveFundamentals.mutate(body)} onRefresh={() => refresh.mutate()} saving={saveFundamentals.isPending} refreshing={refresh.isPending} />}
|
||||
{refresh.isError && <Callout variant="error">Refresh failed: {(refresh.error as Error).message}</Callout>}
|
||||
</section>
|
||||
<section>
|
||||
<div className="mb-3 text-[11px] uppercase tracking-wider text-gray-500">Fixed basket & freshness</div>
|
||||
{config.isLoading && <SkeletonCard className="h-36" />}
|
||||
{config.data && <ConfigEditor key={config.dataUpdatedAt} data={config.data} onSave={(updates) => saveConfig.mutate(updates)} saving={saveConfig.isPending} />}
|
||||
{saveConfig.isError && <Callout variant="error">Save failed: {(saveConfig.error as Error).message}</Callout>}
|
||||
</section>
|
||||
</div>
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user