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:
@@ -119,3 +119,31 @@ def save_options_gate(req: OptionsGateRequest):
|
|||||||
if req.iv_gate_skew_threshold is not None:
|
if req.iv_gate_skew_threshold is not None:
|
||||||
set_config("iv_gate_skew_threshold", str(req.iv_gate_skew_threshold))
|
set_config("iv_gate_skew_threshold", str(req.iv_gate_skew_threshold))
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Tech Indicators config ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class TechIndicatorsRequest(BaseModel):
|
||||||
|
tech_indicators_enabled: Optional[bool] = None
|
||||||
|
tech_indicators_list: Optional[str] = None # comma-separated: "rsi,ma,bollinger,atr"
|
||||||
|
tech_indicators_auto_calibrate: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tech-indicators")
|
||||||
|
def get_tech_indicators():
|
||||||
|
return {
|
||||||
|
"tech_indicators_enabled": (get_config("tech_indicators_enabled") or "true").lower() == "true",
|
||||||
|
"tech_indicators_list": get_config("tech_indicators_list") or "rsi,ma,bollinger,atr",
|
||||||
|
"tech_indicators_auto_calibrate": (get_config("tech_indicators_auto_calibrate") or "true").lower() == "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/tech-indicators")
|
||||||
|
def save_tech_indicators(req: TechIndicatorsRequest):
|
||||||
|
if req.tech_indicators_enabled is not None:
|
||||||
|
set_config("tech_indicators_enabled", "true" if req.tech_indicators_enabled else "false")
|
||||||
|
if req.tech_indicators_list is not None:
|
||||||
|
set_config("tech_indicators_list", req.tech_indicators_list)
|
||||||
|
if req.tech_indicators_auto_calibrate is not None:
|
||||||
|
set_config("tech_indicators_auto_calibrate", "true" if req.tech_indicators_auto_calibrate else "false")
|
||||||
|
return {"status": "ok"}
|
||||||
|
|||||||
@@ -353,6 +353,7 @@ def score_patterns_with_context(
|
|||||||
iv_context: str = "",
|
iv_context: str = "",
|
||||||
risk_context: str = "",
|
risk_context: str = "",
|
||||||
cycle_meta: Optional[Dict] = None,
|
cycle_meta: Optional[Dict] = None,
|
||||||
|
tech_indicators_block: str = "",
|
||||||
) -> List[Dict[str, Any]]:
|
) -> List[Dict[str, Any]]:
|
||||||
"""Score all patterns with rich context (news, prices, IV, risk clusters) using GPT-4o."""
|
"""Score all patterns with rich context (news, prices, IV, risk clusters) using GPT-4o."""
|
||||||
if not get_client():
|
if not get_client():
|
||||||
@@ -571,11 +572,14 @@ Instructions de notation:
|
|||||||
f"⚠️ Tiens compte du délai depuis le dernier cycle pour évaluer si les news sont déjà intégrées.\n"
|
f"⚠️ Tiens compte du délai depuis le dernier cycle pour évaluer si les news sont déjà intégrées.\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_tech_sc_section = f"\n{tech_indicators_block}\n" if tech_indicators_block else ""
|
||||||
|
|
||||||
user = f"""CONTEXTE GLOBAL:
|
user = f"""CONTEXTE GLOBAL:
|
||||||
- Score risque géopolitique: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')})
|
- Score risque géopolitique: {geo_score.get('score', 50)}/100 ({geo_score.get('level', 'medium')})
|
||||||
- Top risques: {geo_score.get('top_risks', [])}
|
- Top risques: {geo_score.get('top_risks', [])}
|
||||||
{temporal_section_sc}
|
{temporal_section_sc}
|
||||||
{macro_section}
|
{macro_section}
|
||||||
|
{_tech_sc_section}
|
||||||
TEMPLATE DE NOTATION:
|
TEMPLATE DE NOTATION:
|
||||||
{scoring_template}
|
{scoring_template}
|
||||||
|
|
||||||
@@ -964,6 +968,7 @@ def suggest_patterns_from_market_context(
|
|||||||
reliability_map: Optional[Dict] = None,
|
reliability_map: Optional[Dict] = None,
|
||||||
iv_context: str = "",
|
iv_context: str = "",
|
||||||
cycle_meta: Optional[Dict] = None,
|
cycle_meta: Optional[Dict] = None,
|
||||||
|
tech_indicators_block: str = "",
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""Ask GPT-4o to propose new patterns based on current geo/market + macro regime context."""
|
"""Ask GPT-4o to propose new patterns based on current geo/market + macro regime context."""
|
||||||
_cycle_meta = cycle_meta or {}
|
_cycle_meta = cycle_meta or {}
|
||||||
@@ -1109,12 +1114,14 @@ Règles supplémentaires:
|
|||||||
"## Actualités géopolitiques du moment (triées par impact)\n" + news_block
|
"## Actualités géopolitiques du moment (triées par impact)\n" + news_block
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tech_block_section = f"\n{tech_indicators_block}\n" if tech_indicators_block else ""
|
||||||
|
|
||||||
user = f"""Tu es un stratège géopolitique et financier senior, expert en options.
|
user = f"""Tu es un stratège géopolitique et financier senior, expert en options.
|
||||||
{macro_block}{geo_block}{lessons_block}{reliability_block}{iv_block}
|
{macro_block}{geo_block}{lessons_block}{reliability_block}{iv_block}
|
||||||
{temporal_news_block}
|
{temporal_news_block}
|
||||||
## Prix des marchés (variation J-1)
|
## Prix des marchés (variation J-1)
|
||||||
{market_block}
|
{market_block}
|
||||||
|
{tech_block_section}
|
||||||
## Calendrier économique à venir
|
## Calendrier économique à venir
|
||||||
{cal_block}
|
{cal_block}
|
||||||
|
|
||||||
|
|||||||
@@ -376,12 +376,43 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
|||||||
# Apply decay to news before suggestion (adds decayed_score + age_hours)
|
# Apply decay to news before suggestion (adds decayed_score + age_hours)
|
||||||
from services.ai_analyzer import apply_news_decay as _apply_decay
|
from services.ai_analyzer import apply_news_decay as _apply_decay
|
||||||
news = _apply_decay(news)
|
news = _apply_decay(news)
|
||||||
|
|
||||||
|
# ── Tech indicators for top tickers ──────────────────────────────
|
||||||
|
_tech_block = ""
|
||||||
|
try:
|
||||||
|
_ti_enabled = (get_config("tech_indicators_enabled") or "true").lower() == "true"
|
||||||
|
if _ti_enabled:
|
||||||
|
from services.technical_indicators import compute_indicators, format_indicators_for_prompt
|
||||||
|
_ti_list = [s.strip() for s in (get_config("tech_indicators_list") or "rsi,ma,bollinger,atr").split(",") if s.strip()]
|
||||||
|
_horizon_days = 45 # default mid-range if no trade horizon context yet
|
||||||
|
_ti_lines = []
|
||||||
|
# Pick up to 5 tickers from quotes (one per asset class)
|
||||||
|
_seen_tickers: set = set()
|
||||||
|
for _cls, _qs in quotes.items():
|
||||||
|
for _q in _qs[:1]:
|
||||||
|
_sym = _q.get("symbol", "")
|
||||||
|
if _sym and _sym not in _seen_tickers:
|
||||||
|
_seen_tickers.add(_sym)
|
||||||
|
_ind = compute_indicators(_sym, _horizon_days, enabled_indicators=_ti_list)
|
||||||
|
_blk = format_indicators_for_prompt(_ind)
|
||||||
|
if _blk:
|
||||||
|
_ti_lines.append(_blk)
|
||||||
|
if len(_seen_tickers) >= 5:
|
||||||
|
break
|
||||||
|
if len(_seen_tickers) >= 5:
|
||||||
|
break
|
||||||
|
if _ti_lines:
|
||||||
|
_tech_block = "\n".join(_ti_lines)
|
||||||
|
except Exception as _te:
|
||||||
|
logger.warning(f"[Cycle] Tech indicators failed (non-blocking): {_te}")
|
||||||
|
|
||||||
suggestions = suggest_patterns_from_market_context(
|
suggestions = suggest_patterns_from_market_context(
|
||||||
news, quotes, calendar, macro_regime=macro_regime, geo_score=geo_score_obj,
|
news, quotes, calendar, macro_regime=macro_regime, geo_score=geo_score_obj,
|
||||||
portfolio_lessons=portfolio_lessons,
|
portfolio_lessons=portfolio_lessons,
|
||||||
reliability_map=_reliability_map or None,
|
reliability_map=_reliability_map or None,
|
||||||
iv_context=iv_context,
|
iv_context=iv_context,
|
||||||
cycle_meta=cycle_meta,
|
cycle_meta=cycle_meta,
|
||||||
|
tech_indicators_block=_tech_block,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[Cycle] Suggestion step failed: {e}")
|
logger.warning(f"[Cycle] Suggestion step failed: {e}")
|
||||||
@@ -505,6 +536,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
|||||||
iv_context=iv_context,
|
iv_context=iv_context,
|
||||||
risk_context=risk_cluster_context,
|
risk_context=risk_cluster_context,
|
||||||
cycle_meta=cycle_meta,
|
cycle_meta=cycle_meta,
|
||||||
|
tech_indicators_block=_tech_block,
|
||||||
)
|
)
|
||||||
scored_with_id = [s for s in scored if s.get("pattern_id")]
|
scored_with_id = [s for s in scored if s.get("pattern_id")]
|
||||||
scored_without_id = [s for s in scored if not s.get("pattern_id")]
|
scored_without_id = [s for s in scored if not s.get("pattern_id")]
|
||||||
|
|||||||
174
backend/services/technical_indicators.py
Normal file
174
backend/services/technical_indicators.py
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
"""
|
||||||
|
Technical indicators computed from OHLCV data, calibrated to option horizon.
|
||||||
|
|
||||||
|
Horizon calibration:
|
||||||
|
<= 30 days : RSI(14), MA(20/50), BB(20), ATR(14)
|
||||||
|
<= 90 days : RSI(21), MA(50/100), BB(50), ATR(21)
|
||||||
|
> 90 days : RSI(28), MA(100/200),BB(100),ATR(28)
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yfinance as yf
|
||||||
|
import pandas as pd
|
||||||
|
_YF_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
_YF_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
def _calibration(horizon_days: int) -> dict:
|
||||||
|
if horizon_days <= 30:
|
||||||
|
return {"rsi": 14, "ma_fast": 20, "ma_slow": 50, "bb": 20, "atr": 14, "label": "Court terme"}
|
||||||
|
elif horizon_days <= 90:
|
||||||
|
return {"rsi": 21, "ma_fast": 50, "ma_slow": 100, "bb": 50, "atr": 21, "label": "Moyen terme"}
|
||||||
|
else:
|
||||||
|
return {"rsi": 28, "ma_fast": 100, "ma_slow": 200, "bb": 100, "atr": 28, "label": "Long terme"}
|
||||||
|
|
||||||
|
|
||||||
|
def _rsi(closes: "pd.Series", period: int) -> Optional[float]:
|
||||||
|
delta = closes.diff()
|
||||||
|
gain = delta.clip(lower=0).rolling(period).mean()
|
||||||
|
loss = (-delta.clip(upper=0)).rolling(period).mean()
|
||||||
|
rs = gain / loss.replace(0, float("nan"))
|
||||||
|
rsi_series = 100 - (100 / (1 + rs))
|
||||||
|
val = rsi_series.iloc[-1]
|
||||||
|
return float(val) if not math.isnan(val) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _bollinger(closes: "pd.Series", period: int) -> dict:
|
||||||
|
ma = closes.rolling(period).mean()
|
||||||
|
std = closes.rolling(period).std()
|
||||||
|
upper = ma + 2 * std
|
||||||
|
lower = ma - 2 * std
|
||||||
|
price = closes.iloc[-1]
|
||||||
|
u, l, m = float(upper.iloc[-1]), float(lower.iloc[-1]), float(ma.iloc[-1])
|
||||||
|
band_width = u - l
|
||||||
|
bb_pct = ((price - l) / band_width * 100) if band_width > 0 else 50.0
|
||||||
|
return {"upper": round(u, 4), "lower": round(l, 4), "mid": round(m, 4), "bb_pct": round(bb_pct, 1)}
|
||||||
|
|
||||||
|
|
||||||
|
def _atr(df: "pd.DataFrame", period: int) -> Optional[float]:
|
||||||
|
high, low, close = df["High"], df["Low"], df["Close"]
|
||||||
|
prev_close = close.shift(1)
|
||||||
|
tr = pd.concat([
|
||||||
|
high - low,
|
||||||
|
(high - prev_close).abs(),
|
||||||
|
(low - prev_close).abs(),
|
||||||
|
], axis=1).max(axis=1)
|
||||||
|
atr = tr.rolling(period).mean().iloc[-1]
|
||||||
|
return float(atr) if not math.isnan(atr) else None
|
||||||
|
|
||||||
|
|
||||||
|
def _trend_signal(price: float, ma_fast: float, ma_slow: float) -> str:
|
||||||
|
if price > ma_fast > ma_slow:
|
||||||
|
return "uptrend"
|
||||||
|
elif price < ma_fast < ma_slow:
|
||||||
|
return "downtrend"
|
||||||
|
elif ma_fast > ma_slow:
|
||||||
|
return "bullish_bias"
|
||||||
|
elif ma_fast < ma_slow:
|
||||||
|
return "bearish_bias"
|
||||||
|
return "sideways"
|
||||||
|
|
||||||
|
|
||||||
|
def _rsi_label(rsi_val: float) -> str:
|
||||||
|
if rsi_val >= 70:
|
||||||
|
return "SURACHETÉ ⚠"
|
||||||
|
elif rsi_val <= 30:
|
||||||
|
return "SURVENDU 🔥"
|
||||||
|
return "neutre"
|
||||||
|
|
||||||
|
|
||||||
|
def _bb_label(bb_pct: float) -> str:
|
||||||
|
if bb_pct >= 80:
|
||||||
|
return "proche bande haute — potentiel retournement"
|
||||||
|
elif bb_pct <= 20:
|
||||||
|
return "proche bande basse — potentiel rebond"
|
||||||
|
return "dans les bandes"
|
||||||
|
|
||||||
|
|
||||||
|
def compute_indicators(ticker: str, horizon_days: int, enabled_indicators: Optional[list] = None) -> dict:
|
||||||
|
"""
|
||||||
|
Compute technical indicators for *ticker* calibrated to *horizon_days*.
|
||||||
|
|
||||||
|
Returns a dict with computed values + a pre-formatted prompt_block string.
|
||||||
|
Returns {"error": "..."} if data unavailable.
|
||||||
|
"""
|
||||||
|
if not _YF_AVAILABLE:
|
||||||
|
return {"error": "yfinance not installed"}
|
||||||
|
|
||||||
|
cal = _calibration(horizon_days)
|
||||||
|
# Fetch enough history: need at least ma_slow + some buffer
|
||||||
|
lookback = cal["ma_slow"] * 2 + 50
|
||||||
|
try:
|
||||||
|
df = yf.download(ticker, period=f"{lookback}d", interval="1d", progress=False, auto_adjust=True)
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": f"yfinance download failed: {e}"}
|
||||||
|
|
||||||
|
if df is None or len(df) < cal["ma_slow"]:
|
||||||
|
return {"error": f"Not enough data for {ticker} (got {len(df) if df is not None else 0} rows)"}
|
||||||
|
|
||||||
|
closes = df["Close"].dropna()
|
||||||
|
price = float(closes.iloc[-1])
|
||||||
|
enabled = set(enabled_indicators) if enabled_indicators else {"rsi", "ma", "bollinger", "atr"}
|
||||||
|
|
||||||
|
result: dict = {
|
||||||
|
"ticker": ticker,
|
||||||
|
"horizon_days": horizon_days,
|
||||||
|
"calibration": cal["label"],
|
||||||
|
"price": round(price, 4),
|
||||||
|
"periods": cal,
|
||||||
|
}
|
||||||
|
|
||||||
|
if "rsi" in enabled:
|
||||||
|
rsi_val = _rsi(closes, cal["rsi"])
|
||||||
|
result["rsi"] = round(rsi_val, 1) if rsi_val is not None else None
|
||||||
|
result["rsi_label"] = _rsi_label(rsi_val) if rsi_val is not None else "N/A"
|
||||||
|
|
||||||
|
ma_fast_val = ma_slow_val = None
|
||||||
|
if "ma" in enabled:
|
||||||
|
if len(closes) >= cal["ma_fast"]:
|
||||||
|
ma_fast_val = float(closes.rolling(cal["ma_fast"]).mean().iloc[-1])
|
||||||
|
result["ma_fast"] = round(ma_fast_val, 4)
|
||||||
|
if len(closes) >= cal["ma_slow"]:
|
||||||
|
ma_slow_val = float(closes.rolling(cal["ma_slow"]).mean().iloc[-1])
|
||||||
|
result["ma_slow"] = round(ma_slow_val, 4)
|
||||||
|
if ma_fast_val and ma_slow_val:
|
||||||
|
result["trend"] = _trend_signal(price, ma_fast_val, ma_slow_val)
|
||||||
|
|
||||||
|
if "bollinger" in enabled and len(closes) >= cal["bb"]:
|
||||||
|
bb = _bollinger(closes, cal["bb"])
|
||||||
|
result["bollinger"] = bb
|
||||||
|
result["bb_label"] = _bb_label(bb["bb_pct"])
|
||||||
|
|
||||||
|
if "atr" in enabled and len(df) >= cal["atr"]:
|
||||||
|
atr_val = _atr(df, cal["atr"])
|
||||||
|
if atr_val is not None:
|
||||||
|
result["atr"] = round(atr_val, 4)
|
||||||
|
result["atr_pct"] = round(atr_val / price * 100, 2)
|
||||||
|
|
||||||
|
# Build a human-readable prompt block
|
||||||
|
lines = [f"📊 INDICATEURS TECHNIQUES — {ticker} (horizon {horizon_days}j, calibration {cal['label']}) :"]
|
||||||
|
if "rsi" in result:
|
||||||
|
lines.append(f" - RSI({cal['rsi']}): {result['rsi']} → {result['rsi_label']}")
|
||||||
|
if "ma_fast" in result and "ma_slow" in result:
|
||||||
|
above = "au-dessus ▲" if price > result["ma_slow"] else "en-dessous ▼"
|
||||||
|
lines.append(f" - MA{cal['ma_fast']}/{cal['ma_slow']}: prix {above} MA{cal['ma_slow']} → trend {result.get('trend','N/A')}")
|
||||||
|
if "bollinger" in result:
|
||||||
|
bb = result["bollinger"]
|
||||||
|
lines.append(f" - Bollinger({cal['bb']}): prix à {bb['bb_pct']}% des bandes → {result['bb_label']}")
|
||||||
|
if "atr" in result:
|
||||||
|
lines.append(f" - ATR({cal['atr']}): {result['atr']} ({result['atr_pct']}% du prix)")
|
||||||
|
|
||||||
|
result["prompt_block"] = "\n".join(lines)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def format_indicators_for_prompt(indicators: dict) -> str:
|
||||||
|
"""Return the pre-formatted prompt block, or empty string on error."""
|
||||||
|
if "error" in indicators:
|
||||||
|
return ""
|
||||||
|
return indicators.get("prompt_block", "")
|
||||||
@@ -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 = () =>
|
export const useSimPortfolioRisk = () =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: ['journal-portfolio-risk'],
|
queryKey: ['journal-portfolio-risk'],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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, 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 { 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'
|
||||||
|
|
||||||
@@ -361,6 +361,13 @@ export default function Config() {
|
|||||||
const [ivGateExtreme, setIvGateExtreme] = useState(80)
|
const [ivGateExtreme, setIvGateExtreme] = useState(80)
|
||||||
const [ivGateSkew, setIvGateSkew] = useState(8)
|
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
|
// Analysis config local state
|
||||||
const [analysisTopN, setAnalysisTopN] = useState(10)
|
const [analysisTopN, setAnalysisTopN] = useState(10)
|
||||||
const [analysisCategoryDefault, setAnalysisCategoryDefault] = useState('all')
|
const [analysisCategoryDefault, setAnalysisCategoryDefault] = useState('all')
|
||||||
@@ -454,6 +461,15 @@ export default function Config() {
|
|||||||
}
|
}
|
||||||
}, [optionsGateData])
|
}, [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 displaySources = localSources ?? sources ?? {}
|
||||||
|
|
||||||
const toggleSource = (key: string) => {
|
const toggleSource = (key: string) => {
|
||||||
@@ -1275,6 +1291,85 @@ export default function Config() {
|
|||||||
{savingExitDefaults ? 'Sauvegarde...' : 'Sauvegarder les seuils'}
|
{savingExitDefaults ? 'Sauvegarde...' : 'Sauvegarder les seuils'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 | >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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user