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 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"}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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<Record<string, any> | 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() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Journal & Sortie ── */}
|
||||
{configTab === 'journal' && (
|
||||
<div className="card">
|
||||
<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
|
||||
</h2>
|
||||
<p className="text-xs text-slate-500 mb-4">
|
||||
Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (Journal → Ouverts).
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-6 mb-4">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Objectif de gain : <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
|
||||
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span>
|
||||
</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>
|
||||
{/* ── Options — Paramètres ── */}
|
||||
{configTab === 'options' && (
|
||||
<div className="space-y-5">
|
||||
|
||||
{/* ── IV Gate — Paramètres d'entrée ── */}
|
||||
<div className="card border-blue-700/30 bg-blue-900/5">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h2 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<ShieldAlert className="w-4 h-4 text-blue-400" /> IV Gate — Filtres d'entrée
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIvGateEnabled(!ivGateEnabled)}
|
||||
className={clsx('text-xs px-3 py-1 rounded border font-semibold transition-all', {
|
||||
'bg-blue-600 border-blue-500 text-white': ivGateEnabled,
|
||||
'bg-dark-700 border-slate-700 text-slate-400': !ivGateEnabled,
|
||||
})}>
|
||||
{ivGateEnabled ? '✓ Gate actif' : '○ Gate désactivé'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mb-5">
|
||||
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>
|
||||
<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 className="mt-5 pt-4 border-t border-slate-700/30">
|
||||
<button
|
||||
onClick={() => saveOptionsGate(
|
||||
{ iv_gate_enabled: ivGateEnabled, iv_gate_ivr_high: ivGateHigh,
|
||||
iv_gate_ivr_extreme: ivGateExtreme, iv_gate_skew_threshold: ivGateSkew },
|
||||
{ onSuccess: () => { setSavedMsg('IV Gate sauvegardé'); setTimeout(() => setSavedMsg(''), 2000) } }
|
||||
)}
|
||||
disabled={savingOptionsGate}
|
||||
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">
|
||||
<Save className="w-4 h-4" />
|
||||
{savingOptionsGate ? 'Sauvegarde...' : 'Sauvegarder le gate'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-6 mb-4">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Mode signal IA de retournement</label>
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ value: 'badge_only', label: '🔔 Badge uniquement' },
|
||||
{ value: 'auto', label: '⚡ Auto (futur)' },
|
||||
].map(opt => (
|
||||
<button key={opt.value} onClick={() => setExitSignalMode(opt.value)}
|
||||
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>
|
||||
))}
|
||||
|
||||
{/* ── Paramètres de sortie ── */}
|
||||
<div className="card">
|
||||
<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
|
||||
</h2>
|
||||
<p className="text-xs text-slate-500 mb-5">
|
||||
Valeurs par défaut pour les objectifs et stop-loss. Chaque trade peut avoir ses propres seuils (Journal → Ouverts).
|
||||
</p>
|
||||
<div className="grid grid-cols-2 gap-6 mb-4">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">
|
||||
Objectif de gain : <span className="text-emerald-400 font-mono font-bold">+{exitTarget}%</span>
|
||||
<span className="text-slate-600 ml-1">(P&L sous-jacent)</span>
|
||||
</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 className="text-[10px] text-slate-600 mt-1">
|
||||
Badge only : alerte visible, vous confirmez manuellement. Auto : fermeture proposée.
|
||||
<div>
|
||||
<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>
|
||||
<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 className="grid grid-cols-2 gap-6 mb-5">
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 mb-2 block">Mode signal IA de retournement</label>
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ value: 'badge_only', label: '🔔 Badge uniquement' },
|
||||
{ value: 'auto', label: '⚡ Auto (futur)' },
|
||||
].map(opt => (
|
||||
<button key={opt.value} onClick={() => setExitSignalMode(opt.value)}
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user