From dc3bc667ebb14cda2d50be018891fd1e92745a70 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Wed, 17 Jun 2026 12:15:32 +0200 Subject: [PATCH] =?UTF-8?q?fix:=203=20bugs=20=E2=80=94=20synthesis=20crash?= =?UTF-8?q?,=20stale=20running=20cycle,=20invalid=20tickers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - knowledge.py: trade_line crash when pnl_pct is None in dict (t.get('pnl_pct',0) returns None if key exists with None value — use 'or 0') - database.py: cleanup_stale_running_cycles() marks any 'running' cycle as 'error' on startup (uvicorn reload mid-cycle left status stuck) - main.py: call cleanup_stale_running_cycles() at startup with warning log - database.py: _normalize_ticker() converts GPT-4o exchange:symbol format (NSE:RELIANCE → RELIANCE.NS, BSE:X → X.BO, etc.) so yfinance stops spamming 404 errors for every MTM request Co-Authored-By: Claude Sonnet 4.6 --- backend/main.py | 9 ++++++++- backend/routers/knowledge.py | 3 ++- backend/services/database.py | 28 +++++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/backend/main.py b/backend/main.py index f5568e6..33404c7 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from routers import market_data, geopolitical, options, backtest, ai, portfolio, config, patterns, journal, cycle as cycle_router, profiles as profiles_router, reasoning as reasoning_router, knowledge as knowledge_router -from services.database import init_db, get_config +from services.database import init_db, get_config, cleanup_stale_running_cycles import os import uvicorn @@ -23,6 +23,13 @@ app.add_middleware( @app.on_event("startup") def startup(): init_db() + # Clean up cycles that were left in 'running' state by a previous crashed process + stale = cleanup_stale_running_cycles() + if stale: + import logging + logging.getLogger(__name__).warning( + f"[Startup] Cleaned up {stale} stale 'running' cycle(s) — server likely reloaded mid-cycle" + ) key = get_config("openai_api_key") or "" if key: os.environ["OPENAI_API_KEY"] = key diff --git a/backend/routers/knowledge.py b/backend/routers/knowledge.py index aadd4bd..668bb8e 100644 --- a/backend/routers/knowledge.py +++ b/backend/routers/knowledge.py @@ -64,8 +64,9 @@ Note timing: {timing[:150]} def trade_line(t): mat = t.get("maturity", {}) + pnl = t.get("pnl_pct") or 0 return (f"{t.get('underlying','?')} {t.get('strategy','?')} " - f"P&L={t.get('pnl_pct',0):+.1f}% score={t.get('latest_score','?')} " + f"P&L={pnl:+.1f}% score={t.get('latest_score','?')} " f"regime={t.get('macro_regime','?')} [{mat.get('readable','')}]") trades_block = f""" diff --git a/backend/services/database.py b/backend/services/database.py index 4e4823d..bbc35e7 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -950,6 +950,17 @@ def add_cycle_run(run_id: str, trigger: str = "auto") -> None: conn.close() +def cleanup_stale_running_cycles() -> int: + """Mark any cycle_runs still in 'running' state as 'error' (stale from a crashed process).""" + conn = get_conn() + cur = conn.execute( + "UPDATE cycle_runs SET status='error', completed_at=datetime('now') WHERE status='running'" + ) + conn.commit() + conn.close() + return cur.rowcount + + def update_cycle_run(run_id: str, **fields) -> None: if not fields: return @@ -1125,6 +1136,20 @@ def get_pattern_scoring_history(pattern_id: str, limit: int = 10) -> List[Dict[s _BEARISH_KEYWORDS = {"bear", "put", "short", "sell", "vente", "baissier"} +_EXCHANGE_PREFIX_MAP = { + "NSE": ".NS", "BSE": ".BO", "TSX": ".TO", "LSE": ".L", + "HKG": ".HK", "SHA": ".SS", "SHE": ".SZ", "TYO": ".T", +} + +def _normalize_ticker(ticker: str) -> str: + """Convert exchange:symbol formats (from GPT-4o) to yfinance-compatible tickers.""" + if ":" in ticker: + exchange, symbol = ticker.split(":", 1) + suffix = _EXCHANGE_PREFIX_MAP.get(exchange.upper(), "") + return symbol + suffix if suffix else symbol + return ticker + + def _fetch_live_prices(tickers: List[str], timeout: int = 20) -> Dict[str, Optional[float]]: """ Fetch current prices for a list of tickers using yf.download(). @@ -1139,12 +1164,13 @@ def _fetch_live_prices(tickers: List[str], timeout: int = 20) -> Dict[str, Optio from concurrent.futures import ThreadPoolExecutor, as_completed def _one(ticker: str) -> tuple: + yf_ticker = _normalize_ticker(ticker) for kwargs in [ {"period": "1d", "interval": "5m"}, {"period": "5d", "interval": "1d"}, ]: try: - df = yf.download(ticker, progress=False, auto_adjust=True, **kwargs) + df = yf.download(yf_ticker, progress=False, auto_adjust=True, **kwargs) if df.empty: continue if isinstance(df.columns, pd.MultiIndex):