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:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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'] }),
|
||||
})
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trade Parameters */}
|
||||
<div className="card">
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
|
||||
<DollarSign className="w-4 h-4 text-emerald-400" /> Trade Parameters
|
||||
</h2>
|
||||
<p className="text-xs text-slate-500 mb-4">
|
||||
Passed into every AI cycle — controls capital sizing and filters out patterns outside your investment horizon.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Simulation budget: <span className="text-emerald-400 font-mono font-bold">€{tradeBudget.toLocaleString()}</span>
|
||||
</label>
|
||||
<input type="range" min="500" max="50000" step="500"
|
||||
value={tradeBudget}
|
||||
onChange={e => setTradeBudget(parseInt(e.target.value))}
|
||||
className="w-full accent-emerald-500" />
|
||||
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
|
||||
<span>€500</span><span>€50,000</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
Max per position: <span className="text-slate-300 font-mono">€{Math.round(tradeBudget * 0.25).toLocaleString()}</span> (25%)
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Min horizon: <span className="text-blue-400 font-mono font-bold">{horizonMin}d</span>
|
||||
</label>
|
||||
<input type="range" min="1" max="90" step="1"
|
||||
value={horizonMin}
|
||||
onChange={e => setHorizonMin(Math.min(parseInt(e.target.value), horizonMax - 5))}
|
||||
className="w-full accent-blue-500" />
|
||||
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
|
||||
<span>1d</span><span>90d</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Patterns shorter than {horizonMin}d penalized in R/R</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Max horizon: <span className="text-blue-400 font-mono font-bold">{horizonMax}d</span>
|
||||
</label>
|
||||
<input type="range" min="30" max="365" step="5"
|
||||
value={horizonMax}
|
||||
onChange={e => setHorizonMax(Math.max(parseInt(e.target.value), horizonMin + 5))}
|
||||
className="w-full accent-blue-500" />
|
||||
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
|
||||
<span>30d</span><span>365d</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Patterns longer than {horizonMax}d penalized in R/R</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 p-3 rounded bg-emerald-900/10 border border-emerald-700/20 text-xs text-emerald-300">
|
||||
Active mandate: budget <span className="font-mono">€{tradeBudget.toLocaleString()}</span> · horizon{' '}
|
||||
<span className="font-mono">{horizonMin}–{horizonMax}d</span> · AI will size + filter suggestions accordingly
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto-Cycle settings */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user