Add multi-factor conviction gate to activation
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 26s

Make "qualified" mean an edge candidate, not just R:R + confidence.
The gate now also requires (all admin-configurable, defaults on):
- high conviction: recommended_action LONG_HIGH / SHORT_HIGH only
- clean read: risk_level Low (no contradicting signals)
- probable primary target: best target probability >= min (default 60)

- Shared predicate: app/services/qualification.py +
  frontend/src/lib/qualification.ts (mirrored)
- Activation config extended (min_target_probability,
  require_high_conviction, exclude_conflicts) with bool-aware
  get/update + validation
- /trades/performance switched to ?qualified_only=true, applying
  the full gate server-side; confidence breakdown stays unfiltered
- Dashboard "Qualified", Signals "Qualified only" toggle, and
  Track Record all use the one gate; Admin gains the new controls

Sentiment provider runtime config (prior change) included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 11:50:42 +02:00
parent 6da65b8d8f
commit d53ed972d1
25 changed files with 924 additions and 110 deletions
+24
View File
@@ -4,6 +4,8 @@ import type {
AdminUser,
PipelineReadiness,
RecommendationConfig,
SentimentProviderConfig,
SentimentTestResult,
SystemSetting,
TickerUniverse,
TickerUniverseBootstrapResult,
@@ -81,6 +83,28 @@ export function updateActivationSettings(payload: Partial<ActivationConfig>) {
.then((r) => r.data);
}
export function getSentimentSettings() {
return apiClient
.get<SentimentProviderConfig>('admin/settings/sentiment')
.then((r) => r.data);
}
export function updateSentimentSettings(payload: {
provider?: string;
model?: string;
api_key?: string;
}) {
return apiClient
.put<SentimentProviderConfig>('admin/settings/sentiment', payload)
.then((r) => r.data);
}
export function testSentimentSettings(ticker: string) {
return apiClient
.post<SentimentTestResult>('admin/settings/sentiment/test', { ticker })
.then((r) => r.data);
}
export function getTickerUniverseSetting() {
return apiClient
.get<TickerUniverseSetting>('admin/settings/ticker-universe')
+1 -2
View File
@@ -2,8 +2,7 @@ import apiClient from './client';
import type { PerformanceStats } from '../lib/types';
export interface PerformanceParams {
min_rr?: number;
min_confidence?: number;
qualified_only?: boolean;
}
export function getPerformance(params?: PerformanceParams) {
@@ -6,6 +6,9 @@ import { SkeletonTable } from '../ui/Skeleton';
const DEFAULTS: ActivationConfig = {
min_rr: 2,
min_confidence: 70,
min_target_probability: 60,
require_high_conviction: true,
exclude_conflicts: true,
};
export function ActivationSettings() {
@@ -19,12 +22,12 @@ export function ActivationSettings() {
}, [data]);
const onSave = () => {
update.mutate(form as unknown as Record<string, number>);
update.mutate(form);
};
const onReset = () => {
setForm(DEFAULTS);
update.mutate(DEFAULTS as unknown as Record<string, number>);
update.mutate(DEFAULTS);
};
if (isLoading) return <SkeletonTable rows={2} cols={2} />;
@@ -33,16 +36,16 @@ export function ActivationSettings() {
return (
<div className="glass p-5 space-y-4">
<div>
<h3 className="text-sm font-semibold text-gray-200">Activation Thresholds</h3>
<h3 className="text-sm font-semibold text-gray-200">Activation Gate</h3>
<p className="mt-1 text-xs text-gray-500">
What counts as a signal worth acting on. Used as the default Signals filters, the
Dashboard's qualified-setup metrics, and the Track Record's "qualified only" view.
All setups are still evaluated regardless, so these thresholds can be validated
against the confidence breakdown.
What counts as a signal worth acting on. Drives the Dashboard's "Qualified" metric, the
Signals "Qualified only" view, and the Track Record's qualified stats. All setups are
still evaluated regardless tighten the gate, then watch qualified expectancy in the
Track Record to find what actually wins.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="grid gap-4 md:grid-cols-3">
<label className="block space-y-1">
<span className="text-xs text-gray-400">Min Risk:Reward (1 : x)</span>
<input
@@ -53,6 +56,7 @@ export function ActivationSettings() {
onChange={(e) => setForm((prev) => ({ ...prev, min_rr: Number(e.target.value) }))}
className="w-full input-glass px-3 py-2 text-sm"
/>
<span className="text-[11px] text-gray-600">Set above your scanner floor or it does nothing.</span>
</label>
<label className="block space-y-1">
<span className="text-xs text-gray-400">Min Confidence (%)</span>
@@ -66,6 +70,50 @@ export function ActivationSettings() {
className="w-full input-glass px-3 py-2 text-sm"
/>
</label>
<label className="block space-y-1">
<span className="text-xs text-gray-400">Min Target Probability (%)</span>
<input
type="number"
min={0}
max={100}
step={1}
value={form.min_target_probability}
onChange={(e) => setForm((prev) => ({ ...prev, min_target_probability: Number(e.target.value) }))}
className="w-full input-glass px-3 py-2 text-sm"
/>
<span className="text-[11px] text-gray-600">Best target's probability must clear this. 0 disables.</span>
</label>
</div>
<div className="grid gap-3 md:grid-cols-2">
<label className="flex cursor-pointer items-start gap-2.5 text-sm text-gray-300">
<input
type="checkbox"
checked={form.require_high_conviction}
onChange={(e) => setForm((prev) => ({ ...prev, require_high_conviction: e.target.checked }))}
className="mt-0.5 h-4 w-4 cursor-pointer accent-blue-400"
/>
<span>
Require high conviction
<span className="mt-0.5 block text-[11px] text-gray-500">
Only LONG (High) / SHORT (High) — the signals must clearly pick a side.
</span>
</span>
</label>
<label className="flex cursor-pointer items-start gap-2.5 text-sm text-gray-300">
<input
type="checkbox"
checked={form.exclude_conflicts}
onChange={(e) => setForm((prev) => ({ ...prev, exclude_conflicts: e.target.checked }))}
className="mt-0.5 h-4 w-4 cursor-pointer accent-blue-400"
/>
<span>
Exclude conflicted setups
<span className="mt-0.5 block text-[11px] text-gray-500">
Risk level must be Low — drops setups with contradicting signals.
</span>
</span>
</label>
</div>
<div className="flex items-center gap-2">
@@ -0,0 +1,154 @@
import { useEffect, useState } from 'react';
import {
useSentimentSettings,
useUpdateSentimentSettings,
useTestSentimentProvider,
} from '../../hooks/useAdmin';
import { Select } from '../ui/Field';
import { SkeletonTable } from '../ui/Skeleton';
import type { SentimentTestResult } from '../../lib/types';
const SOURCE_LABEL: Record<string, string> = {
database: 'configured here',
environment: 'from environment (.env)',
none: 'not configured',
};
export function SentimentProviderSettings() {
const { data, isLoading, isError, error } = useSentimentSettings();
const update = useUpdateSentimentSettings();
const test = useTestSentimentProvider();
const [provider, setProvider] = useState('openai');
const [model, setModel] = useState('');
const [apiKey, setApiKey] = useState('');
const [testResult, setTestResult] = useState<SentimentTestResult | null>(null);
useEffect(() => {
if (data) {
setProvider(data.provider);
setModel(data.model);
}
}, [data]);
if (isLoading) return <SkeletonTable rows={3} cols={2} />;
if (isError) return <p className="text-sm text-red-400">{(error as Error)?.message || 'Failed to load sentiment settings'}</p>;
if (!data) return null;
const onProviderChange = (next: string) => {
setProvider(next);
// Auto-fill the model with the new provider's default unless the user has a
// custom value that isn't the previous provider's default.
const defaults = data.default_models;
if (!model || Object.values(defaults).includes(model)) {
setModel(defaults[next] ?? '');
}
};
const onSave = () => {
setTestResult(null);
update.mutate({
provider,
model,
...(apiKey ? { api_key: apiKey } : {}),
});
setApiKey('');
};
const onTest = () => {
test.mutate('AAPL', { onSuccess: (res) => setTestResult(res) });
};
const keyConfigured = data.api_key_configured;
return (
<div className="glass p-5 space-y-4">
<div>
<h3 className="text-sm font-semibold text-gray-200">Sentiment LLM Provider</h3>
<p className="mt-1 text-xs text-gray-500">
Switch the model that powers sentiment analysis without redeploying. Applies to the
scheduled sentiment job and manual fetches on the next run.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<label className="block space-y-1">
<span className="text-xs text-gray-400">Provider</span>
<Select value={provider} onChange={(e) => onProviderChange(e.target.value)} className="w-full !py-2">
{data.valid_providers.map((p) => (
<option key={p} value={p}>{p}</option>
))}
</Select>
</label>
<label className="block space-y-1">
<span className="text-xs text-gray-400">Model</span>
<input
type="text"
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder={data.default_models[provider] ?? ''}
className="w-full input-glass px-3 py-2 text-sm"
/>
</label>
</div>
<label className="block space-y-1">
<span className="text-xs text-gray-400">API Key</span>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
autoComplete="new-password"
placeholder={keyConfigured ? '•••••••••• (leave blank to keep current)' : 'Paste API key…'}
className="w-full input-glass px-3 py-2 text-sm"
/>
<span className="text-[11px] text-gray-500">
Key status:{' '}
<span className={keyConfigured ? 'text-emerald-400' : 'text-amber-400'}>
{SOURCE_LABEL[data.api_key_source] ?? data.api_key_source}
</span>
{' '}· write-only, never displayed
</span>
</label>
<div className="flex flex-wrap items-center gap-2">
<button className="btn-primary px-4 py-2 text-sm" onClick={onSave} disabled={update.isPending}>
{update.isPending ? 'Saving…' : 'Save Provider'}
</button>
<button
className="px-4 py-2 text-sm rounded border border-white/[0.1] text-gray-300 hover:text-white disabled:opacity-50"
onClick={onTest}
disabled={test.isPending}
>
{test.isPending ? 'Testing (AAPL)…' : 'Test Connection'}
</button>
<span className="text-[11px] text-gray-500">Test fetches live sentiment for AAPL with the saved config.</span>
</div>
{testResult && (
<div
className={`rounded-lg border px-4 py-3 text-sm ${
testResult.ok
? 'border-emerald-500/20 bg-emerald-500/10 text-emerald-300'
: 'border-red-500/20 bg-red-500/10 text-red-400'
}`}
>
{testResult.ok ? (
<>
<span className="font-medium"> {testResult.provider} / {testResult.model}</span> {testResult.ticker}:{' '}
<span className="font-semibold">{testResult.classification}</span>{' '}
<span className="num">({testResult.confidence}%)</span>
{testResult.reasoning && <p className="mt-1 text-xs text-emerald-300/80">{testResult.reasoning}</p>}
</>
) : (
<>
<span className="font-medium"> Test failed</span>
<p className="mt-1 text-xs">{testResult.error}</p>
</>
)}
</div>
)}
</div>
);
}
+33 -11
View File
@@ -2,6 +2,7 @@ import { useMemo, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useActivation } from '../../hooks/useActivation';
import { useTrades } from '../../hooks/useTrades';
import { qualifiesSetup, activationSummary } from '../../lib/qualification';
import { TradeTable, type SortColumn, type SortDirection, computeTradeAnalysis } from '../scanner/TradeTable';
import { SkeletonTable } from '../ui/Skeleton';
import { useToast } from '../ui/Toast';
@@ -99,18 +100,16 @@ export function SetupsPanel() {
const queryClient = useQueryClient();
const toast = useToast();
// null = user hasn't touched the filter; falls back to admin-configured
// activation thresholds once loaded
const [minRROverride, setMinRROverride] = useState<number | null>(null);
const [minConfidenceOverride, setMinConfidenceOverride] = useState<number | null>(null);
// "Qualified only" applies the admin activation gate; the manual filters
// below refine within whatever is shown.
const [qualifiedOnly, setQualifiedOnly] = useState(true);
const [minRR, setMinRR] = useState(0);
const [minConfidence, setMinConfidence] = useState(0);
const [directionFilter, setDirectionFilter] = useState<DirectionFilter>('both');
const [actionFilter, setActionFilter] = useState<ActionFilter>('all');
const [sortColumn, setSortColumn] = useState<SortColumn>('rr_ratio');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
const minRR = minRROverride ?? activation.data?.min_rr ?? 0;
const minConfidence = minConfidenceOverride ?? activation.data?.min_confidence ?? 0;
const scanMutation = useMutation({
mutationFn: () => triggerJob('rr_scanner'),
onSuccess: () => {
@@ -133,12 +132,35 @@ export function SetupsPanel() {
const processed = useMemo(() => {
if (!trades) return [];
const filtered = filterTrades(trades, minRR, directionFilter, minConfidence, actionFilter);
let base = trades;
if (qualifiedOnly && activation.data) {
base = base.filter((t) => qualifiesSetup(t, activation.data!));
}
const filtered = filterTrades(base, minRR, directionFilter, minConfidence, actionFilter);
return sortTrades(filtered, sortColumn, sortDirection);
}, [trades, minRR, directionFilter, minConfidence, actionFilter, sortColumn, sortDirection]);
}, [trades, qualifiedOnly, activation.data, minRR, directionFilter, minConfidence, actionFilter, sortColumn, sortDirection]);
return (
<div className="space-y-6">
{/* Qualified gate toggle */}
<div className="glass-sm flex flex-wrap items-center justify-between gap-3 px-4 py-3">
<label className="flex cursor-pointer items-center gap-2.5 text-sm text-gray-300">
<input
type="checkbox"
checked={qualifiedOnly}
onChange={(e) => setQualifiedOnly(e.target.checked)}
className="h-4 w-4 cursor-pointer accent-blue-400"
/>
<span>
Qualified only
{activation.data && (
<span className="num ml-2 text-xs text-gray-500">{activationSummary(activation.data)}</span>
)}
</span>
</label>
<span className="text-xs text-gray-500">Manual filters below refine within this.</span>
</div>
{/* Filter toolbar */}
<div className="glass-sm flex flex-wrap items-end gap-4 p-4">
<Field label="Min Risk:Reward" htmlFor="min-rr">
@@ -150,7 +172,7 @@ export function SetupsPanel() {
min={0}
step={0.1}
value={minRR}
onChange={(e) => setMinRROverride(Number(e.target.value) || 0)}
onChange={(e) => setMinRR(Number(e.target.value) || 0)}
className="w-20"
/>
</div>
@@ -174,7 +196,7 @@ export function SetupsPanel() {
max={100}
step={1}
value={minConfidence}
onChange={(e) => setMinConfidenceOverride(Number(e.target.value) || 0)}
onChange={(e) => setMinConfidence(Number(e.target.value) || 0)}
className="w-24"
/>
</Field>
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useActivation } from '../../hooks/useActivation';
import { activationSummary } from '../../lib/qualification';
import { usePerformance } from '../../hooks/usePerformance';
import { triggerJob } from '../../api/admin';
import { Button } from '../ui/Button';
@@ -94,11 +95,9 @@ export function TrackRecordPanel() {
const [qualifiedOnly, setQualifiedOnly] = useState(true);
const activation = useActivation();
const params = qualifiedOnly && activation.data
? { min_rr: activation.data.min_rr, min_confidence: activation.data.min_confidence }
: undefined;
const { data, isLoading, isError, error } = usePerformance(params);
const { data, isLoading, isError, error } = usePerformance(
qualifiedOnly ? { qualified_only: true } : undefined,
);
const queryClient = useQueryClient();
const toast = useToast();
@@ -126,9 +125,7 @@ export function TrackRecordPanel() {
<span>
Qualified signals only
{activation.data && (
<span className="num ml-2 text-xs text-gray-500">
R:R {activation.data.min_rr.toFixed(1)} · conf {activation.data.min_confidence.toFixed(0)}%
</span>
<span className="num ml-2 text-xs text-gray-500">{activationSummary(activation.data)}</span>
)}
</span>
</label>
+37 -2
View File
@@ -126,13 +126,13 @@ export function useUpdateActivationSettings() {
const { addToast } = useToast();
return useMutation({
mutationFn: (payload: Record<string, number>) =>
mutationFn: (payload: Partial<import('../lib/types').ActivationConfig>) =>
adminApi.updateActivationSettings(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'activation-settings'] });
qc.invalidateQueries({ queryKey: ['activation'] });
qc.invalidateQueries({ queryKey: ['performance'] });
addToast('success', 'Activation thresholds updated');
addToast('success', 'Activation gate updated');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to update activation thresholds');
@@ -140,6 +140,41 @@ export function useUpdateActivationSettings() {
});
}
export function useSentimentSettings() {
return useQuery({
queryKey: ['admin', 'sentiment-settings'],
queryFn: () => adminApi.getSentimentSettings(),
});
}
export function useUpdateSentimentSettings() {
const qc = useQueryClient();
const { addToast } = useToast();
return useMutation({
mutationFn: (payload: { provider?: string; model?: string; api_key?: string }) =>
adminApi.updateSentimentSettings(payload),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['admin', 'sentiment-settings'] });
addToast('success', 'Sentiment provider updated');
},
onError: (error: Error) => {
addToast('error', error.message || 'Failed to update sentiment provider');
},
});
}
export function useTestSentimentProvider() {
const { addToast } = useToast();
return useMutation({
mutationFn: (ticker: string) => adminApi.testSentimentSettings(ticker),
onError: (error: Error) => {
addToast('error', error.message || 'Sentiment test failed');
},
});
}
export function useTickerUniverseSetting() {
return useQuery({
queryKey: ['admin', 'ticker-universe'],
+33
View File
@@ -0,0 +1,33 @@
import type { ActivationConfig, TradeSetup } from './types';
const HIGH_CONVICTION_ACTIONS = new Set(['LONG_HIGH', 'SHORT_HIGH']);
export function bestTargetProbability(setup: TradeSetup): number {
return setup.targets?.length ? Math.max(...setup.targets.map((t) => t.probability)) : 0;
}
/**
* Whether a setup clears the activation gate. Mirrors the backend predicate in
* app/services/qualification.py — keep the two in sync.
*/
export function qualifiesSetup(setup: TradeSetup, config: ActivationConfig): boolean {
if (setup.rr_ratio < config.min_rr) return false;
if ((setup.confidence_score ?? 0) < config.min_confidence) return false;
if (config.require_high_conviction && !HIGH_CONVICTION_ACTIONS.has(setup.recommended_action ?? '')) {
return false;
}
if (config.exclude_conflicts && (setup.risk_level ?? '') !== 'Low') return false;
if (config.min_target_probability > 0 && bestTargetProbability(setup) < config.min_target_probability) {
return false;
}
return true;
}
/** Short human summary of the active gate, e.g. for tooltips/labels. */
export function activationSummary(config: ActivationConfig): string {
const parts = [`R:R ≥ ${config.min_rr.toFixed(1)}`, `conf ≥ ${config.min_confidence.toFixed(0)}%`];
if (config.require_high_conviction) parts.push('high-conviction');
if (config.exclude_conflicts) parts.push('clean');
if (config.min_target_probability > 0) parts.push(`target ≥ ${config.min_target_probability.toFixed(0)}%`);
return parts.join(' · ');
}
+25 -1
View File
@@ -152,10 +152,34 @@ export interface PerformanceStats {
by_confidence: Record<string, OutcomeBucketStats>;
}
// Activation thresholds: what counts as an actionable signal
// Activation gate: what counts as an actionable signal
export interface ActivationConfig {
min_rr: number;
min_confidence: number;
min_target_probability: number;
require_high_conviction: boolean;
exclude_conflicts: boolean;
}
// Runtime sentiment LLM configuration
export interface SentimentProviderConfig {
provider: string;
model: string;
api_key_configured: boolean;
api_key_source: 'database' | 'environment' | 'none';
valid_providers: string[];
default_models: Record<string, string>;
}
export interface SentimentTestResult {
ok: boolean;
provider: string;
model: string;
ticker?: string;
classification?: string;
confidence?: number;
reasoning?: string | null;
error?: string;
}
export interface TradeTarget {
+2
View File
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { ActivationSettings } from '../components/admin/ActivationSettings';
import { SentimentProviderSettings } from '../components/admin/SentimentProviderSettings';
import { DataCleanup } from '../components/admin/DataCleanup';
import { JobControls } from '../components/admin/JobControls';
import { PipelineReadinessPanel } from '../components/admin/PipelineReadinessPanel';
@@ -30,6 +31,7 @@ export default function AdminPage() {
{activeTab === 'Settings' && (
<div className="space-y-4">
<ActivationSettings />
<SentimentProviderSettings />
<TickerUniverseBootstrap />
<RecommendationSettings />
<SettingsForm />
+6 -8
View File
@@ -9,6 +9,7 @@ import { Section } from '../components/ui/Section';
import { SkeletonCard, SkeletonTable } from '../components/ui/Skeleton';
import { formatPrice } from '../lib/format';
import { recommendationActionLabel } from '../lib/recommendation';
import { qualifiesSetup, activationSummary } from '../lib/qualification';
import type { TradeSetup } from '../lib/types';
function fmtR(value: number | null): string {
@@ -55,15 +56,12 @@ export default function DashboardPage() {
const activation = useActivation();
const performance = usePerformance();
const minRR = activation.data?.min_rr ?? 2;
const minConfidence = activation.data?.min_confidence ?? 70;
const qualifiedSetups = useMemo(
() =>
(trades.data ?? []).filter(
(t) => t.rr_ratio >= minRR && (t.confidence_score ?? 0) >= minConfidence,
),
[trades.data, minRR, minConfidence],
activation.data
? (trades.data ?? []).filter((t) => qualifiesSetup(t, activation.data!))
: [],
[trades.data, activation.data],
);
// Show qualified setups first; fall back to the full list when none qualify
@@ -112,7 +110,7 @@ export default function DashboardPage() {
<Metric
label="Qualified"
value={String(qualifiedSetups.length)}
sub={`R:R ≥ ${minRR.toFixed(1)} & conf ≥ ${minConfidence.toFixed(0)}%`}
sub={activation.data ? activationSummary(activation.data) : 'clears the activation gate'}
valueClass={qualifiedSetups.length > 0 ? 'text-blue-300' : 'text-gray-100'}
/>
<Metric
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/activation.ts","./src/api/admin.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/fundamentals.ts","./src/api/health.ts","./src/api/indicators.ts","./src/api/ingestion.ts","./src/api/ohlcv.ts","./src/api/performance.ts","./src/api/scores.ts","./src/api/sentiment.ts","./src/api/sr-levels.ts","./src/api/tickers.ts","./src/api/trades.ts","./src/api/watchlist.ts","./src/components/admin/activationsettings.tsx","./src/components/admin/datacleanup.tsx","./src/components/admin/jobcontrols.tsx","./src/components/admin/pipelinereadinesspanel.tsx","./src/components/admin/recommendationsettings.tsx","./src/components/admin/settingsform.tsx","./src/components/admin/tickermanagement.tsx","./src/components/admin/tickeruniversebootstrap.tsx","./src/components/admin/usertable.tsx","./src/components/auth/protectedroute.tsx","./src/components/charts/candlestickchart.tsx","./src/components/layout/appshell.tsx","./src/components/layout/mobilenav.tsx","./src/components/layout/sidebar.tsx","./src/components/rankings/rankingstable.tsx","./src/components/rankings/weightsform.tsx","./src/components/scanner/tradetable.tsx","./src/components/signals/setupspanel.tsx","./src/components/signals/trackrecordpanel.tsx","./src/components/ticker/dimensionbreakdownpanel.tsx","./src/components/ticker/fundamentalspanel.tsx","./src/components/ticker/indicatorselector.tsx","./src/components/ticker/recommendationpanel.tsx","./src/components/ticker/sroverlay.tsx","./src/components/ticker/sentimentpanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/callout.tsx","./src/components/ui/confirmdialog.tsx","./src/components/ui/disclosure.tsx","./src/components/ui/field.tsx","./src/components/ui/pageheader.tsx","./src/components/ui/scorecard.tsx","./src/components/ui/section.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/watchlist/addtickerform.tsx","./src/components/watchlist/watchlisttable.tsx","./src/hooks/useactivation.ts","./src/hooks/useadmin.ts","./src/hooks/useauth.ts","./src/hooks/usefetchsymboldata.ts","./src/hooks/useperformance.ts","./src/hooks/usescores.ts","./src/hooks/usetickerdetail.ts","./src/hooks/usetickers.ts","./src/hooks/usetrades.ts","./src/hooks/usewatchlist.ts","./src/lib/format.ts","./src/lib/ingestionstatus.ts","./src/lib/recommendation.ts","./src/lib/types.ts","./src/pages/adminpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/loginpage.tsx","./src/pages/marketpage.tsx","./src/pages/registerpage.tsx","./src/pages/signalspage.tsx","./src/pages/tickerdetailpage.tsx","./src/stores/authstore.ts"],"version":"5.6.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/activation.ts","./src/api/admin.ts","./src/api/auth.ts","./src/api/client.ts","./src/api/fundamentals.ts","./src/api/health.ts","./src/api/indicators.ts","./src/api/ingestion.ts","./src/api/ohlcv.ts","./src/api/performance.ts","./src/api/scores.ts","./src/api/sentiment.ts","./src/api/sr-levels.ts","./src/api/tickers.ts","./src/api/trades.ts","./src/api/watchlist.ts","./src/components/admin/activationsettings.tsx","./src/components/admin/datacleanup.tsx","./src/components/admin/jobcontrols.tsx","./src/components/admin/pipelinereadinesspanel.tsx","./src/components/admin/recommendationsettings.tsx","./src/components/admin/sentimentprovidersettings.tsx","./src/components/admin/settingsform.tsx","./src/components/admin/tickermanagement.tsx","./src/components/admin/tickeruniversebootstrap.tsx","./src/components/admin/usertable.tsx","./src/components/auth/protectedroute.tsx","./src/components/charts/candlestickchart.tsx","./src/components/layout/appshell.tsx","./src/components/layout/mobilenav.tsx","./src/components/layout/sidebar.tsx","./src/components/rankings/rankingstable.tsx","./src/components/rankings/weightsform.tsx","./src/components/scanner/tradetable.tsx","./src/components/signals/setupspanel.tsx","./src/components/signals/trackrecordpanel.tsx","./src/components/ticker/dimensionbreakdownpanel.tsx","./src/components/ticker/fundamentalspanel.tsx","./src/components/ticker/indicatorselector.tsx","./src/components/ticker/recommendationpanel.tsx","./src/components/ticker/sroverlay.tsx","./src/components/ticker/sentimentpanel.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/callout.tsx","./src/components/ui/confirmdialog.tsx","./src/components/ui/disclosure.tsx","./src/components/ui/field.tsx","./src/components/ui/pageheader.tsx","./src/components/ui/scorecard.tsx","./src/components/ui/section.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/tabs.tsx","./src/components/ui/toast.tsx","./src/components/watchlist/addtickerform.tsx","./src/components/watchlist/watchlisttable.tsx","./src/hooks/useactivation.ts","./src/hooks/useadmin.ts","./src/hooks/useauth.ts","./src/hooks/usefetchsymboldata.ts","./src/hooks/useperformance.ts","./src/hooks/usescores.ts","./src/hooks/usetickerdetail.ts","./src/hooks/usetickers.ts","./src/hooks/usetrades.ts","./src/hooks/usewatchlist.ts","./src/lib/format.ts","./src/lib/ingestionstatus.ts","./src/lib/qualification.ts","./src/lib/recommendation.ts","./src/lib/types.ts","./src/pages/adminpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/loginpage.tsx","./src/pages/marketpage.tsx","./src/pages/registerpage.tsx","./src/pages/signalspage.tsx","./src/pages/tickerdetailpage.tsx","./src/stores/authstore.ts"],"version":"5.6.3"}