feat: trade mandate (budget + horizon) wired end-to-end
- database.py: add trade_budget_eur / preferred_horizon_min/max config defaults and include them in cycle config migrations - auto_cycle.py: read trade params from config and inject into cycle_meta - ai_analyzer.py: inject INVESTOR TRADE MANDATE block into scoring and suggestion prompts so GPT-4o penalises horizon mismatches and sizes within the capital cap - Config.tsx: Trade Parameters card with budget + horizon sliders and live mandate summary - TradeIdeas.tsx: horizon filter pills (< 1M / 1-3M / 3-6M / > 6M) and budget/horizon indicator pulled from saved config - useApi.ts: extend useUpdateCycleConfig type with new config fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useMemo, useEffect, Fragment } from 'react'
|
||||
import {
|
||||
useAllPatterns, useLastScores, useScorePatterns, useAiStatus,
|
||||
usePortfolioPositions, useTradeMtm, useRiskProfiles, useMacroRegime, useAddPosition,
|
||||
useConfig,
|
||||
} from '../hooks/useApi'
|
||||
import {
|
||||
Target, Brain, Plus, RefreshCw, ChevronDown, ChevronUp, CheckCircle2,
|
||||
@@ -698,8 +699,15 @@ export function TradeIdeasTab() {
|
||||
const allPatterns: any[] = allPatternsData ?? []
|
||||
const scoredAt: string | null = lastScoresData?.scored_at ?? null
|
||||
|
||||
const { data: configData } = useConfig()
|
||||
const cs = (configData as any) ?? {}
|
||||
const tradeBudget: number = cs.trade_budget_eur ?? 5000
|
||||
const horizonMin: number = cs.preferred_horizon_min ?? 30
|
||||
const horizonMax: number = cs.preferred_horizon_max ?? 180
|
||||
|
||||
const [categoryFilter, setCategoryFilter] = useState('all')
|
||||
const [thematicFilter, setThematicFilter] = useState('all')
|
||||
const [horizonFilter, setHorizonFilter] = useState('all')
|
||||
const [topN, setTopN] = useState(10)
|
||||
const [viewMode, setViewMode] = useState<'cards' | 'grid'>('grid')
|
||||
const [toast, setToast] = useState<{ title: string; sub: string } | null>(null)
|
||||
@@ -754,11 +762,25 @@ export function TradeIdeasTab() {
|
||||
return map
|
||||
}, [tradeMtmData])
|
||||
|
||||
const HORIZON_BUCKETS = [
|
||||
{ key: 'all', label: 'All horizons' },
|
||||
{ key: 'lt30', label: '< 1M', min: 0, max: 29 },
|
||||
{ key: '1-3m', label: '1–3M', min: 30, max: 90 },
|
||||
{ key: '3-6m', label: '3–6M', min: 91, max: 180 },
|
||||
{ key: 'gt6m', label: '> 6M', min: 181, max: 9999 },
|
||||
]
|
||||
|
||||
const { topScored, allUnscored } = useMemo(() => {
|
||||
const filtered = allPatterns.filter(p =>
|
||||
(categoryFilter === 'all' || p.asset_class === categoryFilter) &&
|
||||
(thematicFilter === 'all' || p.category === thematicFilter)
|
||||
)
|
||||
const bucket = HORIZON_BUCKETS.find(b => b.key === horizonFilter)
|
||||
const filtered = allPatterns.filter(p => {
|
||||
if (categoryFilter !== 'all' && p.asset_class !== categoryFilter) return false
|
||||
if (thematicFilter !== 'all' && p.category !== thematicFilter) return false
|
||||
if (bucket && bucket.key !== 'all') {
|
||||
const hd = p.horizon_days ?? 0
|
||||
if (hd < bucket.min! || hd > bucket.max!) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
const scored: TradeItem[] = []
|
||||
const unscored: TradeItem[] = []
|
||||
for (const p of filtered) {
|
||||
@@ -808,7 +830,7 @@ export function TradeIdeasTab() {
|
||||
scored.sort((a, b) => convScore(b) - convScore(a))
|
||||
const sliced = topN === 0 ? scored : scored.slice(0, topN)
|
||||
return { topScored: sliced, allUnscored: unscored }
|
||||
}, [allPatterns, scoreMap, categoryFilter, thematicFilter, topN])
|
||||
}, [allPatterns, scoreMap, categoryFilter, thematicFilter, horizonFilter, topN])
|
||||
|
||||
const handleAdd = (item: TradeItem) => {
|
||||
const t = item.trade
|
||||
@@ -878,6 +900,25 @@ export function TradeIdeasTab() {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Horizon filter */}
|
||||
<div className="flex items-center gap-0.5 bg-dark-700/60 rounded p-0.5 border border-teal-700/30">
|
||||
{HORIZON_BUCKETS.map(b => (
|
||||
<button key={b.key} onClick={() => setHorizonFilter(b.key)}
|
||||
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
|
||||
'bg-teal-700 text-white': horizonFilter === b.key,
|
||||
'text-slate-500 hover:text-slate-300': horizonFilter !== b.key,
|
||||
})}>
|
||||
{b.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{/* Budget indicator */}
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-400 border border-slate-700/40 rounded px-2 py-1">
|
||||
<span className="text-slate-500">Budget</span>
|
||||
<span className="font-semibold text-slate-200">€{tradeBudget.toLocaleString()}</span>
|
||||
<span className="text-slate-600">·</span>
|
||||
<span className="text-slate-500">{horizonMin}–{horizonMax}d</span>
|
||||
</div>
|
||||
{/* Top N */}
|
||||
<div className="flex items-center gap-0.5 bg-dark-700 rounded p-0.5">
|
||||
{[5, 10, 20, 0].map(n => (
|
||||
|
||||
Reference in New Issue
Block a user