feat: risk tier selector on Trading page + confidence filter on Recommendations

- Trading page: added conservative/moderate/aggressive selector that
  updates the trading engine config via PUT /api/trading/config
- Recommendations page: added risk tier dropdown that defaults to the
  engine's current tier and filters recs by the tier's min_confidence
- Backend: added min_confidence query param to GET /api/recommendations
- Risk tier thresholds: conservative ≥0.75, moderate ≥0.55, aggressive ≥0.40
This commit is contained in:
Celes Renata
2026-04-17 05:08:54 +00:00
parent 49e3955fab
commit 734bf001a7
4 changed files with 532 additions and 5 deletions
+2 -1
View File
@@ -299,12 +299,13 @@ export interface RecommendationDetail extends Recommendation {
risk_evaluation: { id: string; eligible: boolean; allowed_mode: string; rejection_reasons: string[] | null; risk_checks: Record<string, unknown> | null; evaluated_at: string } | null;
}
export function useRecommendations(params?: { ticker?: string; action?: string; mode?: string; since?: string; limit?: number; offset?: number }) {
export function useRecommendations(params?: { ticker?: string; action?: string; mode?: string; since?: string; min_confidence?: number; limit?: number; offset?: number }) {
const qs = new URLSearchParams();
if (params?.ticker) qs.set('ticker', params.ticker);
if (params?.action) qs.set('action', params.action);
if (params?.mode) qs.set('mode', params.mode);
if (params?.since) qs.set('since', params.since);
if (params?.min_confidence != null) qs.set('min_confidence', String(params.min_confidence));
if (params?.limit) qs.set('limit', String(params.limit));
if (params?.offset) qs.set('offset', String(params.offset));
const path = `/api/recommendations${qs.toString() ? '?' + qs : ''}`;
+38 -2
View File
@@ -1,14 +1,33 @@
import { useState } from 'react';
import { useNavigate } from '@tanstack/react-router';
import { useRecommendations } from '../api/hooks';
import { useTradingStatus } from '../api/tradingHooks';
import { DataTable, type Column } from '../components/DataTable';
import { StatusBadge, ConfidenceBar, LoadingSpinner, TickerFilter } from '../components/ui';
import type { Recommendation } from '../api/hooks';
const RISK_TIER_CONFIDENCE: Record<string, number> = {
conservative: 0.75,
moderate: 0.55,
aggressive: 0.40,
};
export function RecommendationsPage() {
const navigate = useNavigate();
const [ticker, setTicker] = useState('');
const { data, isLoading } = useRecommendations({ ticker: ticker || undefined, limit: 100 });
const { data: tradingStatus } = useTradingStatus();
const engineTier = tradingStatus?.risk_tier ?? 'moderate';
const [riskTier, setRiskTier] = useState<string | null>(null);
// Use engine tier as default, allow override
const activeTier = riskTier ?? engineTier;
const minConfidence = RISK_TIER_CONFIDENCE[activeTier] ?? 0.55;
const { data, isLoading } = useRecommendations({
ticker: ticker || undefined,
min_confidence: minConfidence,
limit: 100,
});
const columns: Column<Recommendation>[] = [
{ key: 'ticker', header: 'Ticker', className: 'font-mono font-semibold text-brand-300' },
@@ -25,7 +44,24 @@ export function RecommendationsPage() {
<div>
<div className="mb-4 flex items-center justify-between">
<h1 className="text-xl font-semibold text-gray-100">Recommendations</h1>
<TickerFilter value={ticker} onChange={setTicker} />
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<label htmlFor="risk-tier-filter" className="text-xs text-gray-500">Risk Tier</label>
<select
id="risk-tier-filter"
value={activeTier}
onChange={(e) => setRiskTier(e.target.value)}
className="rounded-md border border-surface-700 bg-surface-900 px-2 py-1.5 text-sm text-gray-200 focus:border-brand-500 focus:outline-none"
>
{Object.entries(RISK_TIER_CONFIDENCE).map(([tier, conf]) => (
<option key={tier} value={tier}>
{tier.charAt(0).toUpperCase() + tier.slice(1)} ({conf})
</option>
))}
</select>
</div>
<TickerFilter value={ticker} onChange={setTicker} />
</div>
</div>
<DataTable<Recommendation>
data={data ?? []}
+38 -1
View File
@@ -10,7 +10,7 @@ import {
useCompetitiveStatus,
useToggleCompetitive,
} from '../api/hooks';
import { useResetPaperTrading } from '../api/tradingHooks';
import { useResetPaperTrading, useTradingStatus, useUpdateTradingConfig } from '../api/tradingHooks';
import { StatusBadge, LoadingSpinner, Card } from '../components/ui';
export function TradingPage() {
@@ -21,6 +21,8 @@ export function TradingPage() {
const { data: competitiveStatus } = useCompetitiveStatus();
const setMode = useSetTradingMode();
const resetTrading = useResetPaperTrading();
const { data: tradingStatus } = useTradingStatus();
const updateConfig = useUpdateTradingConfig();
const reviewApproval = useReviewApproval();
const toggleMacro = useToggleMacro();
const toggleCompetitive = useToggleCompetitive();
@@ -95,6 +97,41 @@ export function TradingPage() {
isResetting={resetTrading.isPending}
/>
{/* Risk Tier */}
<Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Risk Tier</h2>
<p className="mb-3 text-[10px] text-gray-600">
Controls confidence gates, position sizing, and portfolio heat limits for the trading engine.
</p>
<div className="flex items-center gap-3">
{(['conservative', 'moderate', 'aggressive'] as const).map((tier) => {
const currentTier = tradingStatus?.risk_tier ?? 'moderate';
const descriptions: Record<string, string> = {
conservative: 'Min confidence 0.75, max 5% position, 10% heat',
moderate: 'Min confidence 0.55, max 10% position, 20% heat',
aggressive: 'Min confidence 0.40, max 15% position, 30% heat',
};
return (
<button
key={tier}
onClick={() => {
if (tier !== currentTier) updateConfig.mutate({ risk_tier: tier });
}}
className={`rounded-md px-4 py-2 text-sm font-medium capitalize transition-colors ${
currentTier === tier
? 'bg-brand-600 text-white'
: 'border border-surface-700 bg-surface-900 text-gray-400 hover:bg-surface-800'
}`}
aria-pressed={currentTier === tier}
title={descriptions[tier]}
>
{tier}
</button>
);
})}
</div>
</Card>
{/* Macro Signal Layer Toggle */}
<Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Macro Signal Layer</h2>