feat: Config tab "Options — Paramètres" with IV gate controls + exit params

- New tab replaces "Journal & Sortie" — consolidates all options-specific settings
- IV Gate section: toggle on/off + 3 sliders (IVR High/Extreme/Skew threshold)
  with live color-coded summary and interdependency guards (high < extreme)
- Exit params section: target, stop-loss, reversal mode + threshold (moved from removed journal tab)
- Backend: GET/PUT /api/config/options-gate endpoint reads/writes 4 config DB keys
- useApi.ts: useOptionsGate + useSaveOptionsGate hooks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-20 10:23:31 +02:00
parent c7ccf237d7
commit 39da3b8945
3 changed files with 282 additions and 77 deletions

View File

@@ -2,7 +2,7 @@ from fastapi import APIRouter
from pydantic import BaseModel from pydantic import BaseModel
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
import os import os
from services.database import get_all_config, set_config, get_sources, update_sources, get_analysis_config, save_analysis_config from services.database import get_all_config, set_config, get_config, get_sources, update_sources, get_analysis_config, save_analysis_config
router = APIRouter(prefix="/api/config", tags=["config"]) router = APIRouter(prefix="/api/config", tags=["config"])
@@ -89,3 +89,33 @@ def save_analysis_config_endpoint(req: AnalysisConfigRequest):
current["template"] = req.template current["template"] = req.template
save_analysis_config(current) save_analysis_config(current)
return {"status": "ok", "config": current} return {"status": "ok", "config": current}
class OptionsGateRequest(BaseModel):
iv_gate_enabled: Optional[bool] = None
iv_gate_ivr_high: Optional[float] = None
iv_gate_ivr_extreme: Optional[float] = None
iv_gate_skew_threshold: Optional[float] = None
@router.get("/options-gate")
def get_options_gate():
return {
"iv_gate_enabled": (get_config("iv_gate_enabled") or "true").lower() == "true",
"iv_gate_ivr_high": float(get_config("iv_gate_ivr_high") or 60),
"iv_gate_ivr_extreme": float(get_config("iv_gate_ivr_extreme") or 80),
"iv_gate_skew_threshold": float(get_config("iv_gate_skew_threshold") or 8),
}
@router.put("/options-gate")
def save_options_gate(req: OptionsGateRequest):
if req.iv_gate_enabled is not None:
set_config("iv_gate_enabled", "true" if req.iv_gate_enabled else "false")
if req.iv_gate_ivr_high is not None:
set_config("iv_gate_ivr_high", str(req.iv_gate_ivr_high))
if req.iv_gate_ivr_extreme is not None:
set_config("iv_gate_ivr_extreme", str(req.iv_gate_ivr_extreme))
if req.iv_gate_skew_threshold is not None:
set_config("iv_gate_skew_threshold", str(req.iv_gate_skew_threshold))
return {"status": "ok"}

View File

@@ -459,6 +459,26 @@ export const useSaveExitDefaults = () => {
}) })
} }
export const useOptionsGate = () =>
useQuery({
queryKey: ['options-gate-config'],
queryFn: () => api.get('/config/options-gate').then(r => r.data),
staleTime: 60_000,
})
export const useSaveOptionsGate = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (body: {
iv_gate_enabled?: boolean
iv_gate_ivr_high?: number
iv_gate_ivr_extreme?: number
iv_gate_skew_threshold?: number
}) => api.put('/config/options-gate', body).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['options-gate-config'] }),
})
}
export const useSimPortfolioRisk = () => export const useSimPortfolioRisk = () =>
useQuery({ useQuery({
queryKey: ['journal-portfolio-risk'], queryKey: ['journal-portfolio-risk'],

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults } from '../hooks/useApi' import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate } from '../hooks/useApi'
import { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X, Lock, Gauge, DollarSign } from 'lucide-react' import { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X, Lock, Gauge, DollarSign, TrendingUp, ShieldAlert } from 'lucide-react'
import clsx from 'clsx' import clsx from 'clsx'
const API = '' const API = ''
@@ -343,6 +343,8 @@ export default function Config() {
const { mutate: triggerCycle, isPending: triggeringCycle } = useTriggerCycle() const { mutate: triggerCycle, isPending: triggeringCycle } = useTriggerCycle()
const { data: exitDefaultsData } = useExitDefaults() const { data: exitDefaultsData } = useExitDefaults()
const { mutate: saveExitDefaults, isPending: savingExitDefaults } = useSaveExitDefaults() const { mutate: saveExitDefaults, isPending: savingExitDefaults } = useSaveExitDefaults()
const { data: optionsGateData } = useOptionsGate()
const { mutate: saveOptionsGate, isPending: savingOptionsGate } = useSaveOptionsGate()
const [localSources, setLocalSources] = useState<Record<string, any> | null>(null) const [localSources, setLocalSources] = useState<Record<string, any> | null>(null)
const [openaiKey, setOpenaiKey] = useState('') const [openaiKey, setOpenaiKey] = useState('')
@@ -351,7 +353,13 @@ export default function Config() {
const [fredKey, setFredKey] = useState('') const [fredKey, setFredKey] = useState('')
const [showKeys, setShowKeys] = useState(false) const [showKeys, setShowKeys] = useState(false)
const [savedMsg, setSavedMsg] = useState('') const [savedMsg, setSavedMsg] = useState('')
const [configTab, setConfigTab] = useState<'ia' | 'cycle' | 'sources' | 'journal' | 'profiles'>('ia') const [configTab, setConfigTab] = useState<'ia' | 'cycle' | 'sources' | 'options' | 'profiles'>('ia')
// IV gate local state
const [ivGateEnabled, setIvGateEnabled] = useState(true)
const [ivGateHigh, setIvGateHigh] = useState(60)
const [ivGateExtreme, setIvGateExtreme] = useState(80)
const [ivGateSkew, setIvGateSkew] = useState(8)
// Analysis config local state // Analysis config local state
const [analysisTopN, setAnalysisTopN] = useState(10) const [analysisTopN, setAnalysisTopN] = useState(10)
@@ -437,6 +445,15 @@ export default function Config() {
} }
}, [analysisCfg]) }, [analysisCfg])
useEffect(() => {
if (optionsGateData) {
setIvGateEnabled(optionsGateData.iv_gate_enabled ?? true)
setIvGateHigh(optionsGateData.iv_gate_ivr_high ?? 60)
setIvGateExtreme(optionsGateData.iv_gate_ivr_extreme ?? 80)
setIvGateSkew(optionsGateData.iv_gate_skew_threshold ?? 8)
}
}, [optionsGateData])
const displaySources = localSources ?? sources ?? {} const displaySources = localSources ?? sources ?? {}
const toggleSource = (key: string) => { const toggleSource = (key: string) => {
@@ -471,7 +488,7 @@ export default function Config() {
{ key: 'ia', label: 'IA & Analyse', icon: Brain }, { key: 'ia', label: 'IA & Analyse', icon: Brain },
{ key: 'cycle', label: 'Auto-Cycle & Logging', icon: Zap }, { key: 'cycle', label: 'Auto-Cycle & Logging', icon: Zap },
{ key: 'sources', label: 'Sources', icon: Globe }, { key: 'sources', label: 'Sources', icon: Globe },
{ key: 'journal', label: 'Journal & Sortie', icon: Lock }, { key: 'options', label: 'Options — Paramètres', icon: TrendingUp },
{ key: 'profiles', label: 'Profils de risque', icon: SlidersHorizontal }, { key: 'profiles', label: 'Profils de risque', icon: SlidersHorizontal },
] as const ] as const
@@ -1038,88 +1055,226 @@ export default function Config() {
</div> </div>
)} )}
{/* ── Journal & Sortie ── */} {/* ── Options — Paramètres ── */}
{configTab === 'journal' && ( {configTab === 'options' && (
<div className="card"> <div className="space-y-5">
<h2 className="text-base font-bold text-white flex items-center gap-2 mb-4">
<Lock className="w-4 h-4 text-amber-400" /> Paramètres de sortie des trades {/* ── IV Gate — Paramètres d'entrée ── */}
</h2> <div className="card border-blue-700/30 bg-blue-900/5">
<p className="text-xs text-slate-500 mb-4"> <div className="flex items-center justify-between mb-1">
Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (Journal Ouverts). <h2 className="text-base font-bold text-white flex items-center gap-2">
</p> <ShieldAlert className="w-4 h-4 text-blue-400" /> IV Gate Filtres d'entrée
<div className="grid grid-cols-2 gap-6 mb-4"> </h2>
<div> <button
<label className="text-xs text-slate-500 mb-2 block"> onClick={() => setIvGateEnabled(!ivGateEnabled)}
Objectif de gain : <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span> className={clsx('text-xs px-3 py-1 rounded border font-semibold transition-all', {
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span> 'bg-blue-600 border-blue-500 text-white': ivGateEnabled,
</label> 'bg-dark-700 border-slate-700 text-slate-400': !ivGateEnabled,
<input type="range" min="5" max="150" step="5" })}>
value={exitTarget} {ivGateEnabled ? ' Gate actif' : ' Gate désactivé'}
onChange={e => setExitTarget(parseFloat(e.target.value))} </button>
className="w-full accent-emerald-500" /> </div>
<div className="flex justify-between text-[10px] text-slate-600 mt-1"> <p className="text-xs text-slate-500 mb-5">
<span>+5%</span><span>+150%</span> Bloque automatiquement les trades avec verdict <span className="text-red-400 font-semibold">ALERT</span> avant
qu'ils soient loggés dans le journal. Un trade ALERT est une stratégie incompatible avec le régime de volatilité actuel
(ex: Long Call à IVR 100%). Les trades bloqués apparaissent dans les trades skippés avec la raison <span className="text-slate-400 font-mono">[IV_GATE]</span>.
</p>
<div className={clsx('space-y-5 transition-opacity', !ivGateEnabled && 'opacity-40 pointer-events-none')}>
{/* IVR High — seuil vol chère */}
<div className="grid grid-cols-2 gap-6">
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">
Seuil vol chère (IVR High)
</label>
<span className={clsx('font-mono font-bold text-base', ivGateHigh >= 70 ? 'text-orange-400' : 'text-yellow-400')}>
{ivGateHigh}%
</span>
</div>
<input type="range" min="30" max="85" step="5"
value={ivGateHigh}
onChange={e => {
const v = parseInt(e.target.value)
setIvGateHigh(v)
if (ivGateExtreme <= v) setIvGateExtreme(Math.min(v + 10, 100))
}}
className="w-full accent-yellow-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>30% (strict)</span><span>85% (permissif)</span>
</div>
<div className="text-[10px] text-slate-500 mt-1.5 space-y-0.5">
<div>IVR &lt; {ivGateHigh}% <span className="text-emerald-400">Long Call / Long Put OK</span></div>
<div>IVR {ivGateHigh}{ivGateExtreme}% <span className="text-yellow-400">Spreads requis</span> naked long WARN</div>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">
Seuil vol extrême (IVR Extreme)
</label>
<span className="font-mono font-bold text-base text-red-400">{ivGateExtreme}%</span>
</div>
<input type="range" min="50" max="100" step="5"
value={ivGateExtreme}
onChange={e => {
const v = parseInt(e.target.value)
setIvGateExtreme(v)
if (ivGateHigh >= v) setIvGateHigh(Math.max(v - 10, 30))
}}
className="w-full accent-red-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>50%</span><span>100%</span>
</div>
<div className="text-[10px] text-slate-500 mt-1.5 space-y-0.5">
<div>IVR &gt; {ivGateExtreme}% <span className="text-red-400 font-semibold">BLOQUÉ</span> naked long vol ALERT</div>
<div>Straddle / Strangle ALERT dès IVR &gt; {ivGateExtreme}% (pénalité ×1.3)</div>
</div>
</div>
</div>
{/* Skew threshold */}
<div className="grid grid-cols-2 gap-6">
<div>
<div className="flex items-center justify-between mb-2">
<label className="text-xs text-slate-400 font-medium">
Seuil skew put (protection chère)
</label>
<span className="font-mono font-bold text-base text-orange-400">{ivGateSkew} pts</span>
</div>
<input type="range" min="2" max="20" step="1"
value={ivGateSkew}
onChange={e => setIvGateSkew(parseInt(e.target.value))}
className="w-full accent-orange-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>2pts (très sensible)</span><span>20pts (très tolérant)</span>
</div>
<div className="text-[10px] text-slate-500 mt-1.5">
Skew put &gt; {ivGateSkew}pts puts très chers, signal WARN sur Long Put
</div>
</div>
{/* Visual summary */}
<div className="bg-dark-700/60 rounded-lg px-4 py-3 text-xs space-y-2">
<div className="text-slate-500 font-semibold mb-2 text-[10px] uppercase tracking-wide">Résumé du gate actuel</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-emerald-500" />
<span className="text-slate-400">IVR &lt; <span className="text-emerald-400 font-mono">{ivGateHigh}%</span> Toute stratégie OK</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-yellow-500" />
<span className="text-slate-400">IVR <span className="text-yellow-400 font-mono">{ivGateHigh}{ivGateExtreme}%</span> Spreads préférés</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-red-500 animate-pulse" />
<span className="text-slate-400">IVR &gt; <span className="text-red-400 font-mono">{ivGateExtreme}%</span> Long naked <strong className="text-red-400">BLOQUÉ</strong></span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500" />
<span className="text-slate-400">Skew &gt; <span className="text-orange-400 font-mono">{ivGateSkew}pts</span> Puts chers (WARN)</span>
</div>
</div>
</div> </div>
</div> </div>
<div>
<label className="text-xs text-slate-500 mb-2 block"> <div className="mt-5 pt-4 border-t border-slate-700/30">
Stop-loss : <span className="text-red-400 font-mono font-bold">{exitStop}%</span> <button
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span> onClick={() => saveOptionsGate(
</label> { iv_gate_enabled: ivGateEnabled, iv_gate_ivr_high: ivGateHigh,
<input type="range" min="-90" max="-5" step="5" iv_gate_ivr_extreme: ivGateExtreme, iv_gate_skew_threshold: ivGateSkew },
value={exitStop} { onSuccess: () => { setSavedMsg('IV Gate sauvegardé'); setTimeout(() => setSavedMsg(''), 2000) } }
onChange={e => setExitStop(parseFloat(e.target.value))} )}
className="w-full accent-red-500" /> disabled={savingOptionsGate}
<div className="flex justify-between text-[10px] text-slate-600 mt-1"> className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<span>-90%</span><span>-5%</span> <Save className="w-4 h-4" />
</div> {savingOptionsGate ? 'Sauvegarde...' : 'Sauvegarder le gate'}
</button>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-6 mb-4">
<div> {/* ── Paramètres de sortie ── */}
<label className="text-xs text-slate-500 mb-2 block">Mode signal IA de retournement</label> <div className="card">
<div className="flex gap-2"> <h2 className="text-base font-bold text-white flex items-center gap-2 mb-1">
{[ <Lock className="w-4 h-4 text-amber-400" /> Paramètres de sortie des trades
{ value: 'badge_only', label: '🔔 Badge uniquement' }, </h2>
{ value: 'auto', label: '⚡ Auto (futur)' }, <p className="text-xs text-slate-500 mb-5">
].map(opt => ( Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (Journal Ouverts).
<button key={opt.value} onClick={() => setExitSignalMode(opt.value)} </p>
className={clsx('flex-1 py-2 rounded text-xs border transition-all', { <div className="grid grid-cols-2 gap-6 mb-4">
'bg-blue-600 border-blue-500 text-white': exitSignalMode === opt.value, <div>
'bg-dark-700 border-slate-700 text-slate-400 hover:text-slate-200': exitSignalMode !== opt.value, <label className="text-xs text-slate-500 mb-2 block">
})}> Objectif de gain : <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
{opt.label} <span className="text-slate-600 ml-1">(P&L sous-jacent)</span>
</button> </label>
))} <input type="range" min="5" max="150" step="5"
value={exitTarget}
onChange={e => setExitTarget(parseFloat(e.target.value))}
className="w-full accent-emerald-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>+5%</span><span>+150%</span>
</div>
</div> </div>
<div className="text-[10px] text-slate-600 mt-1"> <div>
Badge only : alerte visible, vous confirmez manuellement. Auto : fermeture proposée. <label className="text-xs text-slate-500 mb-2 block">
Stop-loss : <span className="text-red-400 font-mono font-bold">{exitStop}%</span>
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span>
</label>
<input type="range" min="-90" max="-5" step="5"
value={exitStop}
onChange={e => setExitStop(parseFloat(e.target.value))}
className="w-full accent-red-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>-90%</span><span>-5%</span>
</div>
</div> </div>
</div> </div>
<div> <div className="grid grid-cols-2 gap-6 mb-5">
<label className="text-xs text-slate-500 mb-2 block"> <div>
Seuil retournement signal : <span className="text-blue-400 font-mono font-bold">{exitSignalThreshold}pts</span> <label className="text-xs text-slate-500 mb-2 block">Mode signal IA de retournement</label>
</label> <div className="flex gap-2">
<input type="range" min="10" max="60" step="5" {[
value={exitSignalThreshold} { value: 'badge_only', label: '🔔 Badge uniquement' },
onChange={e => setExitSignalThreshold(parseFloat(e.target.value))} { value: 'auto', label: '⚡ Auto (futur)' },
className="w-full accent-blue-500" /> ].map(opt => (
<div className="flex justify-between text-[10px] text-slate-600 mt-1"> <button key={opt.value} onClick={() => setExitSignalMode(opt.value)}
<span>10pts (sensible)</span><span>60pts (tolérant)</span> className={clsx('flex-1 py-2 rounded text-xs border transition-all', {
'bg-blue-600 border-blue-500 text-white': exitSignalMode === opt.value,
'bg-dark-700 border-slate-700 text-slate-400 hover:text-slate-200': exitSignalMode !== opt.value,
})}>
{opt.label}
</button>
))}
</div>
<div className="text-[10px] text-slate-600 mt-1">
Badge only : alerte visible, vous confirmez manuellement. Auto : fermeture proposée.
</div>
</div>
<div>
<label className="text-xs text-slate-500 mb-2 block">
Seuil retournement signal : <span className="text-blue-400 font-mono font-bold">{exitSignalThreshold}pts</span>
</label>
<input type="range" min="10" max="60" step="5"
value={exitSignalThreshold}
onChange={e => setExitSignalThreshold(parseFloat(e.target.value))}
className="w-full accent-blue-500" />
<div className="flex justify-between text-[10px] text-slate-600 mt-1">
<span>10pts (sensible)</span><span>60pts (tolérant)</span>
</div>
</div> </div>
</div> </div>
<button
onClick={() => saveExitDefaults(
{ target_pct: exitTarget, stop_loss_pct: exitStop,
signal_reversal_mode: exitSignalMode, signal_reversal_threshold: exitSignalThreshold },
{ onSuccess: () => { setSavedMsg('Paramètres de sortie sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingExitDefaults}
className="flex items-center gap-1.5 bg-amber-600 hover:bg-amber-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingExitDefaults ? 'Sauvegarde...' : 'Sauvegarder les seuils'}
</button>
</div> </div>
<button
onClick={() => saveExitDefaults(
{ target_pct: exitTarget, stop_loss_pct: exitStop,
signal_reversal_mode: exitSignalMode, signal_reversal_threshold: exitSignalThreshold },
{ onSuccess: () => { setSavedMsg('Paramètres de sortie sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingExitDefaults}
className="flex items-center gap-1.5 bg-amber-600 hover:bg-amber-500 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingExitDefaults ? 'Sauvegarde...' : 'Sauvegarder les seuils'}
</button>
</div> </div>
)} )}