fix: 3 bugs — synthesis crash, stale running cycle, invalid tickers

- 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 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-17 12:15:32 +02:00
parent 27d6b598e8
commit dc3bc667eb
3 changed files with 37 additions and 3 deletions

View File

@@ -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

View File

@@ -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"""

View File

@@ -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):