From 39da3b8945302b6bb912d2fee0e6021a681057f5 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Sat, 20 Jun 2026 10:23:31 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Config=20tab=20"Options=20=E2=80=94=20P?= =?UTF-8?q?aram=C3=A8tres"=20with=20IV=20gate=20controls=20+=20exit=20para?= =?UTF-8?q?ms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backend/routers/config.py | 32 +++- frontend/src/hooks/useApi.ts | 20 +++ frontend/src/pages/Config.tsx | 307 +++++++++++++++++++++++++--------- 3 files changed, 282 insertions(+), 77 deletions(-) diff --git a/backend/routers/config.py b/backend/routers/config.py index f4d2c50..56d0e24 100644 --- a/backend/routers/config.py +++ b/backend/routers/config.py @@ -2,7 +2,7 @@ from fastapi import APIRouter from pydantic import BaseModel from typing import Dict, Any, Optional 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"]) @@ -89,3 +89,33 @@ def save_analysis_config_endpoint(req: AnalysisConfigRequest): current["template"] = req.template save_analysis_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"} diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index e0e8921..e18dd16 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -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 = () => useQuery({ queryKey: ['journal-portfolio-risk'], diff --git a/frontend/src/pages/Config.tsx b/frontend/src/pages/Config.tsx index 4d97372..2465a84 100644 --- a/frontend/src/pages/Config.tsx +++ b/frontend/src/pages/Config.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react' 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 { Settings, Key, Globe, CheckCircle, XCircle, AlertCircle, Save, Eye, EyeOff, Brain, SlidersHorizontal, RefreshCw, Zap, Plus, Trash2, Pencil, X, Lock, Gauge, DollarSign } from 'lucide-react' +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, TrendingUp, ShieldAlert } from 'lucide-react' import clsx from 'clsx' const API = '' @@ -343,6 +343,8 @@ export default function Config() { const { mutate: triggerCycle, isPending: triggeringCycle } = useTriggerCycle() const { data: exitDefaultsData } = useExitDefaults() const { mutate: saveExitDefaults, isPending: savingExitDefaults } = useSaveExitDefaults() + const { data: optionsGateData } = useOptionsGate() + const { mutate: saveOptionsGate, isPending: savingOptionsGate } = useSaveOptionsGate() const [localSources, setLocalSources] = useState | null>(null) const [openaiKey, setOpenaiKey] = useState('') @@ -351,7 +353,13 @@ export default function Config() { const [fredKey, setFredKey] = useState('') const [showKeys, setShowKeys] = useState(false) 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 const [analysisTopN, setAnalysisTopN] = useState(10) @@ -437,6 +445,15 @@ export default function Config() { } }, [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 toggleSource = (key: string) => { @@ -471,7 +488,7 @@ export default function Config() { { key: 'ia', label: 'IA & Analyse', icon: Brain }, { key: 'cycle', label: 'Auto-Cycle & Logging', icon: Zap }, { 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 }, ] as const @@ -1038,88 +1055,226 @@ export default function Config() { )} - {/* ── Journal & Sortie ── */} - {configTab === 'journal' && ( -
-

- Paramètres de sortie des trades -

-

- Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (Journal → Ouverts). -

-
-
- - setExitTarget(parseFloat(e.target.value))} - className="w-full accent-emerald-500" /> -
- +5%+150% + {/* ── Options — Paramètres ── */} + {configTab === 'options' && ( +
+ + {/* ── IV Gate — Paramètres d'entrée ── */} +
+
+

+ IV Gate — Filtres d'entrée +

+ +
+

+ Bloque automatiquement les trades avec verdict ALERT 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 [IV_GATE]. +

+ +
+ {/* IVR High — seuil vol chère */} +
+
+
+ + = 70 ? 'text-orange-400' : 'text-yellow-400')}> + {ivGateHigh}% + +
+ { + const v = parseInt(e.target.value) + setIvGateHigh(v) + if (ivGateExtreme <= v) setIvGateExtreme(Math.min(v + 10, 100)) + }} + className="w-full accent-yellow-500" /> +
+ 30% (strict)85% (permissif) +
+
+
IVR < {ivGateHigh}% → Long Call / Long Put OK
+
IVR {ivGateHigh}–{ivGateExtreme}% → Spreads requis — naked long → WARN
+
+
+ +
+
+ + {ivGateExtreme}% +
+ { + const v = parseInt(e.target.value) + setIvGateExtreme(v) + if (ivGateHigh >= v) setIvGateHigh(Math.max(v - 10, 30)) + }} + className="w-full accent-red-500" /> +
+ 50%100% +
+
+
IVR > {ivGateExtreme}% → BLOQUÉ — naked long vol → ALERT
+
Straddle / Strangle → ALERT dès IVR > {ivGateExtreme}% (pénalité ×1.3)
+
+
+
+ + {/* Skew threshold */} +
+
+
+ + {ivGateSkew} pts +
+ setIvGateSkew(parseInt(e.target.value))} + className="w-full accent-orange-500" /> +
+ 2pts (très sensible)20pts (très tolérant) +
+
+ Skew put > {ivGateSkew}pts → puts très chers, signal WARN sur Long Put +
+
+ + {/* Visual summary */} +
+
Résumé du gate actuel
+
+
+ IVR < {ivGateHigh}% → Toute stratégie OK +
+
+
+ IVR {ivGateHigh}–{ivGateExtreme}% → Spreads préférés +
+
+
+ IVR > {ivGateExtreme}% → Long naked → BLOQUÉ +
+
+
+ Skew > {ivGateSkew}pts → Puts chers (WARN) +
+
-
- - setExitStop(parseFloat(e.target.value))} - className="w-full accent-red-500" /> -
- -90%-5% -
+ +
+
-
-
- -
- {[ - { value: 'badge_only', label: '🔔 Badge uniquement' }, - { value: 'auto', label: '⚡ Auto (futur)' }, - ].map(opt => ( - - ))} + + {/* ── Paramètres de sortie ── */} +
+

+ Paramètres de sortie des trades +

+

+ Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (Journal → Ouverts). +

+
+
+ + setExitTarget(parseFloat(e.target.value))} + className="w-full accent-emerald-500" /> +
+ +5%+150% +
-
- Badge only : alerte visible, vous confirmez manuellement. Auto : fermeture proposée. +
+ + setExitStop(parseFloat(e.target.value))} + className="w-full accent-red-500" /> +
+ -90%-5% +
-
- - setExitSignalThreshold(parseFloat(e.target.value))} - className="w-full accent-blue-500" /> -
- 10pts (sensible)60pts (tolérant) +
+
+ +
+ {[ + { value: 'badge_only', label: '🔔 Badge uniquement' }, + { value: 'auto', label: '⚡ Auto (futur)' }, + ].map(opt => ( + + ))} +
+
+ Badge only : alerte visible, vous confirmez manuellement. Auto : fermeture proposée. +
+
+
+ + setExitSignalThreshold(parseFloat(e.target.value))} + className="w-full accent-blue-500" /> +
+ 10pts (sensible)60pts (tolérant) +
+
-
)}