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:
@@ -1,7 +1,7 @@
|
|||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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 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 os
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
@@ -23,6 +23,13 @@ app.add_middleware(
|
|||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
def startup():
|
def startup():
|
||||||
init_db()
|
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 ""
|
key = get_config("openai_api_key") or ""
|
||||||
if key:
|
if key:
|
||||||
os.environ["OPENAI_API_KEY"] = key
|
os.environ["OPENAI_API_KEY"] = key
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ Note timing: {timing[:150]}
|
|||||||
|
|
||||||
def trade_line(t):
|
def trade_line(t):
|
||||||
mat = t.get("maturity", {})
|
mat = t.get("maturity", {})
|
||||||
|
pnl = t.get("pnl_pct") or 0
|
||||||
return (f"{t.get('underlying','?')} {t.get('strategy','?')} "
|
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','')}]")
|
f"regime={t.get('macro_regime','?')} [{mat.get('readable','')}]")
|
||||||
|
|
||||||
trades_block = f"""
|
trades_block = f"""
|
||||||
|
|||||||
@@ -950,6 +950,17 @@ def add_cycle_run(run_id: str, trigger: str = "auto") -> None:
|
|||||||
conn.close()
|
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:
|
def update_cycle_run(run_id: str, **fields) -> None:
|
||||||
if not fields:
|
if not fields:
|
||||||
return
|
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"}
|
_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]]:
|
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().
|
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
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
def _one(ticker: str) -> tuple:
|
def _one(ticker: str) -> tuple:
|
||||||
|
yf_ticker = _normalize_ticker(ticker)
|
||||||
for kwargs in [
|
for kwargs in [
|
||||||
{"period": "1d", "interval": "5m"},
|
{"period": "1d", "interval": "5m"},
|
||||||
{"period": "5d", "interval": "1d"},
|
{"period": "5d", "interval": "1d"},
|
||||||
]:
|
]:
|
||||||
try:
|
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:
|
if df.empty:
|
||||||
continue
|
continue
|
||||||
if isinstance(df.columns, pd.MultiIndex):
|
if isinstance(df.columns, pd.MultiIndex):
|
||||||
|
|||||||
Reference in New Issue
Block a user