feat: Phase 3 — indicateurs techniques calibrés par horizon option

- technical_indicators.py (nouveau) : compute_indicators() calcule RSI, MA fast/slow,
  Bollinger Bands, ATR — périodes calibrées automatiquement selon horizon_days
- config.py : endpoints GET/PUT /config/tech-indicators (activé, liste, auto-calibration)
- useApi.ts : useTechIndicatorsConfig + useSaveTechIndicatorsConfig hooks
- Config.tsx : carte "Indicateurs techniques" dans Options—Paramètres avec toggles
- auto_cycle.py : compute top-5 tickers à chaque cycle si tech_indicators_enabled=true
- ai_analyzer.py : tech_indicators_block injecté dans suggestion + scoring prompts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-20 16:41:42 +02:00
parent d5e31bc897
commit 50ba75e468
6 changed files with 357 additions and 2 deletions

View File

@@ -491,6 +491,25 @@ export const useSaveOptionsGate = () => {
})
}
export const useTechIndicatorsConfig = () =>
useQuery({
queryKey: ['tech-indicators-config'],
queryFn: () => api.get('/config/tech-indicators').then(r => r.data),
staleTime: 60_000,
})
export const useSaveTechIndicatorsConfig = () => {
const qc = useQueryClient()
return useMutation({
mutationFn: (body: {
tech_indicators_enabled?: boolean
tech_indicators_list?: string
tech_indicators_auto_calibrate?: boolean
}) => api.put('/config/tech-indicators', body).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: ['tech-indicators-config'] }),
})
}
export const useSimPortfolioRisk = () =>
useQuery({
queryKey: ['journal-portfolio-risk'],

View File

@@ -1,6 +1,6 @@
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, useOptionsGate, useSaveOptionsGate } from '../hooks/useApi'
import { useSources, useUpdateSources, useUpdateApiKeys, useConfig, useAiStatus, useAnalysisConfig, useSaveAnalysisConfig, useCycleStatus, useUpdateCycleConfig, useTriggerCycle, useRiskProfiles, useUpsertProfile, useDeleteProfile, useExitDefaults, useSaveExitDefaults, useOptionsGate, useSaveOptionsGate, useTechIndicatorsConfig, useSaveTechIndicatorsConfig } 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'
@@ -361,6 +361,13 @@ export default function Config() {
const [ivGateExtreme, setIvGateExtreme] = useState(80)
const [ivGateSkew, setIvGateSkew] = useState(8)
// Tech indicators local state
const { data: techIndicatorsData } = useTechIndicatorsConfig()
const { mutate: saveTechIndicators, isPending: savingTechIndicators } = useSaveTechIndicatorsConfig()
const [techEnabled, setTechEnabled] = useState(true)
const [techAutoCalibrate, setTechAutoCalibrate] = useState(true)
const [techList, setTechList] = useState<string[]>(['rsi', 'ma', 'bollinger', 'atr'])
// Analysis config local state
const [analysisTopN, setAnalysisTopN] = useState(10)
const [analysisCategoryDefault, setAnalysisCategoryDefault] = useState('all')
@@ -454,6 +461,15 @@ export default function Config() {
}
}, [optionsGateData])
useEffect(() => {
if (techIndicatorsData) {
setTechEnabled(techIndicatorsData.tech_indicators_enabled ?? true)
setTechAutoCalibrate(techIndicatorsData.tech_indicators_auto_calibrate ?? true)
const list = (techIndicatorsData.tech_indicators_list || 'rsi,ma,bollinger,atr').split(',').map((s: string) => s.trim()).filter(Boolean)
setTechList(list)
}
}, [techIndicatorsData])
const displaySources = localSources ?? sources ?? {}
const toggleSource = (key: string) => {
@@ -1275,6 +1291,85 @@ export default function Config() {
{savingExitDefaults ? 'Sauvegarde...' : 'Sauvegarder les seuils'}
</button>
</div>
{/* ── Indicateurs techniques du sous-jacent ── */}
<div className="card border-purple-700/30 bg-purple-900/5">
<div className="flex items-center justify-between mb-1">
<h2 className="text-base font-bold text-white flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-purple-400" /> Indicateurs techniques du sous-jacent
</h2>
<button
onClick={() => setTechEnabled(!techEnabled)}
className={clsx('text-xs px-3 py-1 rounded border font-semibold transition-all', {
'bg-purple-600 border-purple-500 text-white': techEnabled,
'bg-dark-700 border-slate-700 text-slate-400': !techEnabled,
})}>
{techEnabled ? '✓ Activé' : '○ Désactivé'}
</button>
</div>
<p className="text-xs text-slate-500 mb-4">
Injecte RSI, MA, Bollinger et ATR dans le contexte IA à chaque cycle. Les périodes sont calibrées automatiquement selon l'horizon de l'option analysée.
</p>
<div className={clsx('space-y-4 transition-opacity', !techEnabled && 'opacity-40 pointer-events-none')}>
{/* Indicateurs à activer */}
<div>
<label className="text-xs text-slate-400 font-medium block mb-2">Indicateurs actifs</label>
<div className="flex flex-wrap gap-2">
{[
{ key: 'rsi', label: 'RSI', desc: 'Momentum / surachat-survente' },
{ key: 'ma', label: 'MA fast/slow', desc: 'Tendance & golden/death cross' },
{ key: 'bollinger', label: 'Bollinger', desc: 'Position dans les bandes de vol' },
{ key: 'atr', label: 'ATR', desc: 'Volatilité réalisée récente' },
].map(({ key, label, desc }) => {
const active = techList.includes(key)
return (
<button
key={key}
title={desc}
onClick={() => setTechList(prev =>
active ? prev.filter(k => k !== key) : [...prev, key]
)}
className={clsx('text-xs px-3 py-1.5 rounded border font-semibold transition-all', {
'bg-purple-700 border-purple-500 text-white': active,
'bg-dark-700 border-slate-700 text-slate-400 hover:border-purple-600': !active,
})}>
{active ? '✓ ' : ''}{label}
</button>
)
})}
</div>
<p className="text-[10px] text-slate-600 mt-1.5">Survolez pour voir la description de chaque indicateur.</p>
</div>
{/* Calibration auto */}
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-300 font-medium">Calibration automatique selon horizon</p>
<p className="text-[10px] text-slate-500">30j RSI14, MA20/50 | 90j RSI21, MA50/100 | &gt;90j RSI28, MA100/200</p>
</div>
<button
onClick={() => setTechAutoCalibrate(!techAutoCalibrate)}
className={clsx('text-xs px-3 py-1 rounded border font-semibold transition-all flex-shrink-0 ml-4', {
'bg-purple-600 border-purple-500 text-white': techAutoCalibrate,
'bg-dark-700 border-slate-700 text-slate-400': !techAutoCalibrate,
})}>
{techAutoCalibrate ? '✓ Auto' : '○ Fixe'}
</button>
</div>
</div>
<button
onClick={() => saveTechIndicators(
{ tech_indicators_enabled: techEnabled, tech_indicators_list: techList.join(','), tech_indicators_auto_calibrate: techAutoCalibrate },
{ onSuccess: () => { setSavedMsg('Indicateurs techniques sauvegardés'); setTimeout(() => setSavedMsg(''), 2000) } }
)}
disabled={savingTechIndicators}
className="mt-4 flex items-center gap-1.5 bg-purple-700 hover:bg-purple-600 disabled:opacity-40 text-white px-4 py-2 rounded text-sm font-semibold">
<Save className="w-4 h-4" />
{savingTechIndicators ? 'Sauvegarde...' : 'Sauvegarder les indicateurs'}
</button>
</div>
</div>
)}