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:
@@ -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 ?? []}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user