diff --git a/backend/services/ai_analyzer.py b/backend/services/ai_analyzer.py index 4388384..a94e423 100644 --- a/backend/services/ai_analyzer.py +++ b/backend/services/ai_analyzer.py @@ -619,10 +619,25 @@ Scoring instructions: _portfolio_sc_section = f"\n{portfolio_context_block}\n" if portfolio_context_block else "" _inst_sc_section = f"\n{institutional_block}\n" if institutional_block else "" + # Trade mandate block from cycle_meta + _budget = _cm.get("trade_budget_eur", 5000) + _h_min = _cm.get("preferred_horizon_min", 30) + _h_max = _cm.get("preferred_horizon_max", 180) + _max_pos = round(_budget * 0.25) + _trade_mandate_sc = ( + f"\n## INVESTOR TRADE MANDATE\n" + f"- Available capital budget: €{_budget:,.0f}\n" + f"- Preferred horizon: {_h_min}–{_h_max} days\n" + f"- Max capital per position: €{_max_pos:,.0f} (25% of budget)\n" + f"⚠️ Patterns whose horizon_days is outside [{_h_min}, {_h_max}] should receive a penalty" + f" in the R/R pillar (poor timing fit). Size trade suggestions within the budget cap.\n" + ) + user = f"""GLOBAL CONTEXT: - Geopolitical risk score: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')}) - Top risks: {geo_score.get('top_risks', [])} {temporal_section_sc} +{_trade_mandate_sc} {macro_section} {_fred_sc_section} {_pd_sc_section} @@ -1411,7 +1426,20 @@ Additional rules: portfolio_section = f"\n{portfolio_context_block}\n" if portfolio_context_block else "" convergence_section = f"\n{convergence_block}\n" if convergence_block else "" + _sg_budget = _cycle_meta.get("trade_budget_eur", 5000) + _sg_h_min = _cycle_meta.get("preferred_horizon_min", 30) + _sg_h_max = _cycle_meta.get("preferred_horizon_max", 180) + _sg_max_pos = round(_sg_budget * 0.25) + _suggest_mandate = ( + f"\n## INVESTOR TRADE MANDATE\n" + f"- Available capital budget: €{_sg_budget:,.0f} | Max per position: €{_sg_max_pos:,.0f}\n" + f"- Target horizon: {_sg_h_min}–{_sg_h_max} days\n" + f"⚠️ Only propose patterns whose horizon_days is within [{_sg_h_min}, {_sg_h_max}]." + f" Patterns outside this window will be rejected. Size each trade so capital ≤ €{_sg_max_pos:,.0f}.\n" + ) + user = f"""You are a senior geopolitical and financial strategist, expert in options. +{_suggest_mandate} {macro_block}{geo_block}{lessons_block}{reliability_block}{iv_block} {temporal_news_block} ## Market prices (D-1 change) diff --git a/backend/services/auto_cycle.py b/backend/services/auto_cycle.py index 568bb10..a445ee1 100644 --- a/backend/services/auto_cycle.py +++ b/backend/services/auto_cycle.py @@ -255,6 +255,10 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]: _market_session = "after_hours" _market_note = "After-hours US — prix indicatifs, liquidité réduite." + _budget_eur = float(get_config("trade_budget_eur") or "5000") + _horizon_min = int(get_config("preferred_horizon_min") or "30") + _horizon_max = int(get_config("preferred_horizon_max") or "180") + cycle_meta = { "current_cycle_ts": _now.isoformat(), "last_cycle_ts": _last_cycle_ts_str, @@ -265,6 +269,9 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]: "is_weekend": _is_weekend, "market_session": _market_session, "market_note": _market_note, + "trade_budget_eur": _budget_eur, + "preferred_horizon_min": _horizon_min, + "preferred_horizon_max": _horizon_max, } logger.info( f"[Cycle {run_id[:16]}] Cycle meta: delta={_delta_minutes:.0f}min depuis dernier cycle" diff --git a/backend/services/database.py b/backend/services/database.py index edbe761..752e810 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -311,6 +311,9 @@ def init_db(): "auto_cycle_similarity_threshold": "0.30", "min_ev_threshold": "0.0", "min_score_threshold": "0", + "trade_budget_eur": "5000", + "preferred_horizon_min": "30", + "preferred_horizon_max": "180", "exit_defaults": json.dumps({ "target_pct": 30.0, "stop_loss_pct": -50.0, diff --git a/frontend/src/components/TradeIdeas.tsx b/frontend/src/components/TradeIdeas.tsx index 6f35d56..dbaab4d 100644 --- a/frontend/src/components/TradeIdeas.tsx +++ b/frontend/src/components/TradeIdeas.tsx @@ -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() { ))} + {/* Horizon filter */} +
+ {HORIZON_BUCKETS.map(b => ( + + ))} +
+ {/* Budget indicator */} +
+ Budget + €{tradeBudget.toLocaleString()} + · + {horizonMin}–{horizonMax}d +
{/* Top N */}
{[5, 10, 20, 0].map(n => ( diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index bb2a72f..3716f1f 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -337,7 +337,7 @@ export const useCycleHistory = (limit = 20) => export const useUpdateCycleConfig = () => { const qc = useQueryClient() return useMutation({ - mutationFn: (cfg: { enabled?: boolean; interval_hours?: number; similarity_threshold?: number; min_ev_threshold?: number; min_score_threshold?: number; journal_retention_days?: number; maturity_threshold_pct?: number; weekend_cycle_enabled?: boolean; weekend_cycle_times?: string }) => + mutationFn: (cfg: { enabled?: boolean; interval_hours?: number; similarity_threshold?: number; min_ev_threshold?: number; min_score_threshold?: number; trade_budget_eur?: number; preferred_horizon_min?: number; preferred_horizon_max?: number; journal_retention_days?: number; maturity_threshold_pct?: number; weekend_cycle_enabled?: boolean; weekend_cycle_times?: string }) => api.post('/cycle/config', cfg).then(r => r.data), onSuccess: () => qc.invalidateQueries({ queryKey: ['cycle-status'] }), }) diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index e1761e2..07acb27 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -430,6 +430,9 @@ export default function Config() { const [cycleSimilarity, setCycleSimilarity] = useState(0.30) const [minEv, setMinEv] = useState(0.0) const [minScore, setMinScore] = useState(0) + const [tradeBudget, setTradeBudget] = useState(5000) + const [horizonMin, setHorizonMin] = useState(30) + const [horizonMax, setHorizonMax] = useState(180) const [retentionDays, setRetentionDays] = useState(90) const [maturityThreshold, setMaturityThreshold] = useState(35) const [weekendEnabled, setWeekendEnabled] = useState(true) @@ -441,6 +444,9 @@ export default function Config() { setCycleSimilarity(cs.similarity_threshold ?? 0.30) setMinEv(cs.min_ev_threshold ?? 0.0) setMinScore(cs.min_score_threshold ?? 0) + setTradeBudget(cs.trade_budget_eur ?? 5000) + setHorizonMin(cs.preferred_horizon_min ?? 30) + setHorizonMax(cs.preferred_horizon_max ?? 180) setRetentionDays(cs.journal_retention_days ?? 90) setMaturityThreshold(cs.maturity_threshold_pct ?? 35) setWeekendEnabled(cs.weekend_cycle_enabled ?? true) @@ -725,6 +731,63 @@ export default function Config() {
+ {/* Trade Parameters */} +
+

+ Trade Parameters +

+

+ Passed into every AI cycle — controls capital sizing and filters out patterns outside your investment horizon. +

+
+
+ + setTradeBudget(parseInt(e.target.value))} + className="w-full accent-emerald-500" /> +
+ €500€50,000 +
+
+ Max per position: €{Math.round(tradeBudget * 0.25).toLocaleString()} (25%) +
+
+
+ + setHorizonMin(Math.min(parseInt(e.target.value), horizonMax - 5))} + className="w-full accent-blue-500" /> +
+ 1d90d +
+
Patterns shorter than {horizonMin}d penalized in R/R
+
+
+ + setHorizonMax(Math.max(parseInt(e.target.value), horizonMin + 5))} + className="w-full accent-blue-500" /> +
+ 30d365d +
+
Patterns longer than {horizonMax}d penalized in R/R
+
+
+
+ Active mandate: budget €{tradeBudget.toLocaleString()} · horizon{' '} + {horizonMin}–{horizonMax}d · AI will size + filter suggestions accordingly +
+
+ {/* Auto-Cycle settings */}
@@ -901,6 +964,9 @@ export default function Config() { similarity_threshold: cycleSimilarity, min_ev_threshold: minEv, min_score_threshold: minScore, + trade_budget_eur: tradeBudget, + preferred_horizon_min: horizonMin, + preferred_horizon_max: horizonMax, journal_retention_days: retentionDays, maturity_threshold_pct: maturityThreshold, weekend_cycle_enabled: weekendEnabled,