diff --git a/backend/routers/journal.py b/backend/routers/journal.py index 08c429c..8fc7b70 100644 --- a/backend/routers/journal.py +++ b/backend/routers/journal.py @@ -87,10 +87,19 @@ def trade_mtm(days: int = 30): elif pnl_pct <= stop: alert_type = "stop_loss" + price_warning = None + if entry_price is None and current_price is None: + price_warning = "no_price_data" + elif entry_price is None: + price_warning = "no_entry_price" + elif current_price is None: + price_warning = "no_live_price" + result.append({ **e, "current_price": current_price, "pnl_pct": pnl_pct, + "price_warning": price_warning, "days_held": days_held, "direction": "bearish" if _is_bearish(e.get("strategy", "")) else "bullish", "maturity": maturity, diff --git a/backend/services/ai_analyzer.py b/backend/services/ai_analyzer.py index 5f9079c..70bb0bd 100644 --- a/backend/services/ai_analyzer.py +++ b/backend/services/ai_analyzer.py @@ -760,13 +760,14 @@ TEMPLATE DE NOTATION: BATCH_SIZE = 4 # 4 patterns × ~800 tokens output = ~3200 tokens, safely within gpt-4o limits - # Score batches with limited parallelism to stay under TPM limits. - # 2 concurrent batches × ~6K tokens = ~12K tokens burst, well under 30K TPM. + # Score batches sequentially (max_workers=1) — parallel workers both retry on 429 + # simultaneously, defeating the sleep backoff. Sequential ensures the retry window + # is fully cleared before the next batch fires. from concurrent.futures import ThreadPoolExecutor, as_completed batches = [pattern_blocks[i:i+BATCH_SIZE] for i in range(0, len(pattern_blocks), BATCH_SIZE)] _scorer_log.info(f"[Scorer] Scoring {len(pattern_blocks)} patterns in {len(batches)} batches of max {BATCH_SIZE}") all_scored = [] - with ThreadPoolExecutor(max_workers=min(len(batches), 2)) as executor: + with ThreadPoolExecutor(max_workers=1) as executor: futures = [executor.submit(_score_batch, b) for b in batches] for future in as_completed(futures): try: diff --git a/backend/services/auto_cycle.py b/backend/services/auto_cycle.py index fff6acf..c89d9f1 100644 --- a/backend/services/auto_cycle.py +++ b/backend/services/auto_cycle.py @@ -468,7 +468,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]: # Auto-add any new underlying tickers to the IV watchlist try: - from services.database import add_watchlist_ticker, get_watchlist_tickers + from services.database import _normalize_ticker, add_watchlist_ticker, get_watchlist_tickers from services.iv_engine import _resolve_ticker, bootstrap_iv_history existing = set(get_watchlist_tickers()) new_proxies = set() @@ -476,17 +476,18 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]: for trade in (sp.get("trade_rankings") or sp.get("suggested_trades") or []): underlying = trade.get("underlying") or sp.get("underlying") or "" if underlying: - proxy = _resolve_ticker(underlying) + normalized = _normalize_ticker(underlying.upper()) # WHEAT→ZW=F, EUR/USD→EURUSD=X + proxy = _resolve_ticker(normalized) # ZW=F→WEAT, EURUSD=X→FXE if proxy not in existing: new_proxies.add(proxy) for proxy in new_proxies: if add_watchlist_ticker(proxy, added_by="cycle"): - _log.info(f"[Watchlist] Auto-added new ticker: {proxy}") + logger.info(f"[Watchlist] Auto-added new ticker: {proxy}") log_system_event("INFO", "auto_cycle", f"Nouveau ticker ajouté à la watchlist IV: {proxy}", cycle_id=scoring_run_id, ticker=proxy) if new_proxies: bootstrap_iv_history(tickers=list(new_proxies), min_existing=0) except Exception as _we: - _log.warning(f"[Watchlist] Auto-add failed: {_we}") + logger.warning(f"[Watchlist] Auto-add failed: {_we}") gauges_summary = { k: {"value": v.get("value"), "change_pct": v.get("change_pct"), "label": v.get("label")} diff --git a/backend/services/database.py b/backend/services/database.py index b060dbb..d7c8a74 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -940,7 +940,7 @@ def log_trade_entries(run_id: str, scored_patterns: List[Dict[str, Any]], quotes from concurrent.futures import ThreadPoolExecutor, as_completed def _fetch(ticker: str): - normalized = _normalize_yf_ticker(ticker) + normalized = _normalize_ticker(ticker) # handles WHEAT→ZW=F, EUR/USD→EURUSD=X, etc. try: # Use yf.download() — fresh HTTP request, no in-process Ticker cache for kwargs in [ diff --git a/backend/services/iv_engine.py b/backend/services/iv_engine.py index 9be3818..bec3bea 100644 --- a/backend/services/iv_engine.py +++ b/backend/services/iv_engine.py @@ -50,7 +50,13 @@ IV_WATCHLIST = [ def _resolve_ticker(ticker: str) -> str: """Return the optionable proxy ticker for a given symbol.""" - return _PROXY.get(ticker.upper(), ticker.upper()) + t = ticker.upper().strip() + # Normalize slash-format forex (EUR/USD → EURUSD=X) before proxy lookup + if '/' in t: + parts = t.split('/') + if len(parts) == 2 and all(p.isalpha() for p in parts): + t = parts[0] + parts[1] + '=X' + return _PROXY.get(t, t) def _get_current_price(t: yf.Ticker) -> Optional[float]: diff --git a/frontend/src/pages/JournalDeBord.tsx b/frontend/src/pages/JournalDeBord.tsx index a7e082a..61a096f 100644 --- a/frontend/src/pages/JournalDeBord.tsx +++ b/frontend/src/pages/JournalDeBord.tsx @@ -857,7 +857,19 @@ function TradeMtmSection({ days }: { days: number }) { {t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}j` : t.horizon_days != null ? `${t.horizon_days}j` : '—'} - {t.underlying} + +
+ {t.underlying} + {t.price_warning && ( + + ⚠ {t.price_warning === 'no_price_data' ? 'NO DATA' : t.price_warning === 'no_entry_price' ? 'NO ENTRY' : 'NO LIVE'} + + )} +
+ @@ -883,10 +895,10 @@ function TradeMtmSection({ days }: { days: number }) { ) : } {t.entry_date} - + {t.entry_price != null ? t.entry_price.toFixed(2) : '—'} - + {t.current_price != null ? t.current_price.toFixed(2) : '—'}