fix: 4 cycle errors — NameError _log, WHEAT/EUR/USD ticker normalization, 429 serial scoring
- auto_cycle.py: replace _log with logger (NameError at lines 484/489) - auto_cycle.py: normalize underlying via _normalize_ticker before _resolve_ticker so WHEAT→ZW=F→WEAT and EUR/USD→EURUSD=X→FXE reach the IV watchlist correctly - iv_engine.py: _resolve_ticker now strips slash-format forex (EUR/USD→EURUSD=X) before _PROXY lookup, fixing yfinance 500/404 spam from get_atm_iv - database.py: _fetch in log_trade_entries uses _normalize_ticker (not _normalize_yf_ticker) so commodity aliases like WHEAT→ZW=F are applied at price-fetch time - ai_analyzer.py: max_workers=1 for batch scorer — parallel workers both slept and retried simultaneously after 429, causing repeated bursts; sequential fixes the pattern - journal.py + JournalDeBord.tsx: add price_warning field (no_price_data/no_entry_price/ no_live_price) with visible ⚠ badge and amber color on affected ticker/price cells Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -87,10 +87,19 @@ def trade_mtm(days: int = 30):
|
|||||||
elif pnl_pct <= stop:
|
elif pnl_pct <= stop:
|
||||||
alert_type = "stop_loss"
|
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({
|
result.append({
|
||||||
**e,
|
**e,
|
||||||
"current_price": current_price,
|
"current_price": current_price,
|
||||||
"pnl_pct": pnl_pct,
|
"pnl_pct": pnl_pct,
|
||||||
|
"price_warning": price_warning,
|
||||||
"days_held": days_held,
|
"days_held": days_held,
|
||||||
"direction": "bearish" if _is_bearish(e.get("strategy", "")) else "bullish",
|
"direction": "bearish" if _is_bearish(e.get("strategy", "")) else "bullish",
|
||||||
"maturity": maturity,
|
"maturity": maturity,
|
||||||
|
|||||||
@@ -760,13 +760,14 @@ TEMPLATE DE NOTATION:
|
|||||||
|
|
||||||
BATCH_SIZE = 4 # 4 patterns × ~800 tokens output = ~3200 tokens, safely within gpt-4o limits
|
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.
|
# Score batches sequentially (max_workers=1) — parallel workers both retry on 429
|
||||||
# 2 concurrent batches × ~6K tokens = ~12K tokens burst, well under 30K TPM.
|
# 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
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
batches = [pattern_blocks[i:i+BATCH_SIZE] for i in range(0, len(pattern_blocks), BATCH_SIZE)]
|
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}")
|
_scorer_log.info(f"[Scorer] Scoring {len(pattern_blocks)} patterns in {len(batches)} batches of max {BATCH_SIZE}")
|
||||||
all_scored = []
|
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]
|
futures = [executor.submit(_score_batch, b) for b in batches]
|
||||||
for future in as_completed(futures):
|
for future in as_completed(futures):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ def run_cycle_once(trigger: str = "auto") -> Dict[str, Any]:
|
|||||||
|
|
||||||
# Auto-add any new underlying tickers to the IV watchlist
|
# Auto-add any new underlying tickers to the IV watchlist
|
||||||
try:
|
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
|
from services.iv_engine import _resolve_ticker, bootstrap_iv_history
|
||||||
existing = set(get_watchlist_tickers())
|
existing = set(get_watchlist_tickers())
|
||||||
new_proxies = set()
|
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 []):
|
for trade in (sp.get("trade_rankings") or sp.get("suggested_trades") or []):
|
||||||
underlying = trade.get("underlying") or sp.get("underlying") or ""
|
underlying = trade.get("underlying") or sp.get("underlying") or ""
|
||||||
if underlying:
|
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:
|
if proxy not in existing:
|
||||||
new_proxies.add(proxy)
|
new_proxies.add(proxy)
|
||||||
for proxy in new_proxies:
|
for proxy in new_proxies:
|
||||||
if add_watchlist_ticker(proxy, added_by="cycle"):
|
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)
|
log_system_event("INFO", "auto_cycle", f"Nouveau ticker ajouté à la watchlist IV: {proxy}", cycle_id=scoring_run_id, ticker=proxy)
|
||||||
if new_proxies:
|
if new_proxies:
|
||||||
bootstrap_iv_history(tickers=list(new_proxies), min_existing=0)
|
bootstrap_iv_history(tickers=list(new_proxies), min_existing=0)
|
||||||
except Exception as _we:
|
except Exception as _we:
|
||||||
_log.warning(f"[Watchlist] Auto-add failed: {_we}")
|
logger.warning(f"[Watchlist] Auto-add failed: {_we}")
|
||||||
|
|
||||||
gauges_summary = {
|
gauges_summary = {
|
||||||
k: {"value": v.get("value"), "change_pct": v.get("change_pct"), "label": v.get("label")}
|
k: {"value": v.get("value"), "change_pct": v.get("change_pct"), "label": v.get("label")}
|
||||||
|
|||||||
@@ -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
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
def _fetch(ticker: str):
|
def _fetch(ticker: str):
|
||||||
normalized = _normalize_yf_ticker(ticker)
|
normalized = _normalize_ticker(ticker) # handles WHEAT→ZW=F, EUR/USD→EURUSD=X, etc.
|
||||||
try:
|
try:
|
||||||
# Use yf.download() — fresh HTTP request, no in-process Ticker cache
|
# Use yf.download() — fresh HTTP request, no in-process Ticker cache
|
||||||
for kwargs in [
|
for kwargs in [
|
||||||
|
|||||||
@@ -50,7 +50,13 @@ IV_WATCHLIST = [
|
|||||||
|
|
||||||
def _resolve_ticker(ticker: str) -> str:
|
def _resolve_ticker(ticker: str) -> str:
|
||||||
"""Return the optionable proxy ticker for a given symbol."""
|
"""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]:
|
def _get_current_price(t: yf.Ticker) -> Optional[float]:
|
||||||
|
|||||||
@@ -857,7 +857,19 @@ function TradeMtmSection({ days }: { days: number }) {
|
|||||||
<td className="px-3 py-2 text-right font-mono text-[11px] text-slate-400">
|
<td className="px-3 py-2 text-right font-mono text-[11px] text-slate-400">
|
||||||
{t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}j` : t.horizon_days != null ? `${t.horizon_days}j` : '—'}
|
{t.expiry_days_at_entry != null ? `${t.expiry_days_at_entry}j` : t.horizon_days != null ? `${t.horizon_days}j` : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 font-mono text-slate-300">{t.underlying}</td>
|
<td className="px-3 py-2 font-mono">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className={clsx(t.price_warning ? 'text-amber-300' : 'text-slate-300')}>{t.underlying}</span>
|
||||||
|
{t.price_warning && (
|
||||||
|
<span
|
||||||
|
className="text-[8px] font-bold bg-amber-900/40 border border-amber-600/40 text-amber-400 rounded px-1 py-0.5 leading-none shrink-0"
|
||||||
|
title={t.price_warning === 'no_price_data' ? 'Aucune donnée prix — monitoring impossible' : t.price_warning === 'no_entry_price' ? 'Prix d\'entrée manquant' : 'Prix live indisponible'}
|
||||||
|
>
|
||||||
|
⚠ {t.price_warning === 'no_price_data' ? 'NO DATA' : t.price_warning === 'no_entry_price' ? 'NO ENTRY' : 'NO LIVE'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="px-3 py-2 text-right">
|
<td className="px-3 py-2 text-right">
|
||||||
<ScoreDelta entry={t.score_at_entry} latest={t.latest_score ?? t.score_at_entry} />
|
<ScoreDelta entry={t.score_at_entry} latest={t.latest_score ?? t.score_at_entry} />
|
||||||
</td>
|
</td>
|
||||||
@@ -883,10 +895,10 @@ function TradeMtmSection({ days }: { days: number }) {
|
|||||||
) : <span className="text-slate-700 text-xs">—</span>}
|
) : <span className="text-slate-700 text-xs">—</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-right text-slate-600 whitespace-nowrap text-[11px]">{t.entry_date}</td>
|
<td className="px-3 py-2 text-right text-slate-600 whitespace-nowrap text-[11px]">{t.entry_date}</td>
|
||||||
<td className="px-3 py-2 text-right font-mono text-slate-400 text-[11px]">
|
<td className={clsx('px-3 py-2 text-right font-mono text-[11px]', t.price_warning === 'no_entry_price' || t.price_warning === 'no_price_data' ? 'text-amber-600' : 'text-slate-400')}>
|
||||||
{t.entry_price != null ? t.entry_price.toFixed(2) : '—'}
|
{t.entry_price != null ? t.entry_price.toFixed(2) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-right font-mono text-slate-300 text-[11px]">
|
<td className={clsx('px-3 py-2 text-right font-mono text-[11px]', t.price_warning === 'no_live_price' || t.price_warning === 'no_price_data' ? 'text-amber-600' : 'text-slate-300')}>
|
||||||
{t.current_price != null ? t.current_price.toFixed(2) : '—'}
|
{t.current_price != null ? t.current_price.toFixed(2) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-right">
|
<td className="px-3 py-2 text-right">
|
||||||
|
|||||||
Reference in New Issue
Block a user