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:
@@ -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"}
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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 < {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 > {ivGateExtreme}% → <span className="text-red-400 font-semibold">BLOQUÉ</span> — naked long vol → ALERT</div>
|
||||||
|
<div>Straddle / Strangle → ALERT dès IVR > {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 > {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 < <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 > <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 > <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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user