fix: use categorical regime fundamentals
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m13s
Deploy / deploy (push) Successful in 37s

This commit is contained in:
2026-07-15 10:13:22 +02:00
parent ced066dc01
commit f714782fa4
7 changed files with 257 additions and 58 deletions
+2 -5
View File
@@ -3,6 +3,7 @@ import type {
RegimeMonitor,
RegimeConfig,
RegimeFundamentals,
RegimeFundamentalsUpdate,
EventStudyReport,
RegimeHistoryPoint,
} from '../lib/types';
@@ -33,11 +34,7 @@ export function getRegimeFundamentals() {
return apiClient.get<RegimeFundamentals>('regime/fundamentals').then((r) => r.data);
}
export function updateRegimeFundamentals(body: {
f1_score?: number;
f3_score?: number;
locked?: boolean;
}) {
export function updateRegimeFundamentals(body: RegimeFundamentalsUpdate) {
return apiClient.put<RegimeFundamentals>('regime/fundamentals', body).then((r) => r.data);
}
+12 -2
View File
@@ -511,6 +511,7 @@ export interface RegimeMonitor {
}
export interface RegimeFundamentals {
methodology: 'v2';
f1_score: number | null;
f3_score: number | null;
locked: boolean;
@@ -518,8 +519,17 @@ export interface RegimeFundamentals {
fetched_at: string | null;
effective_date: string | null;
source: string;
capex?: Record<string, string>;
good_news_stock_down?: string | null;
capex: Record<string, CapexState>;
good_news_stock_down: GoodNewsReaction;
}
export type CapexState = 'raising' | 'holding' | 'cutting' | 'unknown';
export type GoodNewsReaction = 'yes' | 'no' | 'mixed';
export interface RegimeFundamentalsUpdate {
capex?: Record<string, CapexState>;
good_news_stock_down?: GoodNewsReaction;
locked?: boolean;
}
export interface RegimeConfig {
+70 -26
View File
@@ -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 &amp; 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 &amp; 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>
);
}