Spaces:
Runtime error
Runtime error
| import os | |
| import json | |
| from dotenv import load_dotenv | |
| load_dotenv() | |
| from nova_prompt import NOVA_SYSTEM_PROMPT_BASE, NOVA_SYSTEM_PROMPT_MASTER, NOVA_SYSTEM_PROMPT_USER, GENERATIVE_SYSTEM_PROMPT, ORACLE_SYSTEM_PROMPT# Set thread limits to prevent OpenBLAS/MKL deadlock inside FastAPI threads | |
| os.environ["OMP_NUM_THREADS"] = "1" | |
| os.environ["OPENBLAS_NUM_THREADS"] = "1" | |
| os.environ["MKL_NUM_THREADS"] = "1" | |
| os.environ["VECLIB_MAXIMUM_THREADS"] = "1" | |
| os.environ["NUMEXPR_NUM_THREADS"] = "1" | |
| import faulthandler | |
| faulthandler.enable() | |
| from concurrent.futures import ProcessPoolExecutor | |
| from fastapi import FastAPI, HTTPException, Request, Header, Depends | |
| from fastapi.staticfiles import StaticFiles | |
| from fastapi.responses import FileResponse, JSONResponse, StreamingResponse | |
| from pydantic import BaseModel | |
| from typing import List, Optional | |
| import secrets | |
| from sqlalchemy.orm import Session | |
| import database | |
| from database import get_pg_engine, SavedPortfolio, BacktestHistory, WebhookConfig, UserMemory | |
| import time | |
| import threading | |
| import uuid | |
| import yfinance as yf | |
| from datetime import datetime | |
| import traceback | |
| import pandas as pd | |
| import logging | |
| import cache | |
| redis_client = cache.get_redis() | |
| try: | |
| from diagnostics import TraceManager | |
| tracer = TraceManager() | |
| except ImportError: | |
| tracer = None | |
| try: | |
| from huggingface_hub import InferenceClient | |
| has_hf_hub = True | |
| except ImportError: | |
| has_hf_hub = False | |
| try: | |
| import core_engine | |
| except ImportError: | |
| core_engine = None | |
| from config import OUTPUT_DIR, logger | |
| import access_manager | |
| class RedisTaskStore: | |
| def __init__(self, prefix="task:"): | |
| self.prefix = prefix | |
| def get(self, key, default=None): | |
| try: | |
| data = cache.redis_client.get(f"{self.prefix}{key}") | |
| if data: | |
| return json.loads(data) | |
| except Exception as e: | |
| logger.error(f"Redis get error: {e}") | |
| return default | |
| def __setitem__(self, key, value): | |
| try: | |
| cache.redis_client.setex(f"{self.prefix}{key}", 86400, json.dumps(value)) | |
| except Exception as e: | |
| logger.error(f"Redis set error: {e}") | |
| def __contains__(self, key): | |
| return bool(cache.redis_client.exists(f"{self.prefix}{key}")) | |
| BACKGROUND_TASKS = RedisTaskStore() | |
| def update_task(tid, **kwargs): | |
| task = BACKGROUND_TASKS.get(tid, {}) | |
| task.update(kwargs) | |
| BACKGROUND_TASKS[tid] = task | |
| process_pool = ProcessPoolExecutor(max_workers=4) | |
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | |
| STATIC_DIR = os.path.join(BASE_DIR, "static") | |
| app = FastAPI(title="Portfolio Engine API") | |
| app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") | |
| from ai_chat import chat_router, memory_router | |
| from options import options_router | |
| app.include_router(chat_router) | |
| app.include_router(memory_router) | |
| app.include_router(options_router) | |
| def get_db(): | |
| from sqlalchemy.orm import sessionmaker | |
| engine = get_pg_engine() | |
| SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) | |
| db = SessionLocal() | |
| try: | |
| yield db | |
| finally: | |
| db.close() | |
| from constants import MASTER_KEY | |
| def get_current_user(x_access_key: Optional[str] = Header(None), x_username: Optional[str] = Header(None)): | |
| if not x_access_key: | |
| raise HTTPException(status_code=401, detail="Access Key missing") | |
| if not x_username and x_access_key != MASTER_KEY: | |
| raise HTTPException(status_code=401, detail="Username missing") | |
| if not access_manager.validate_key(x_access_key, username=x_username): | |
| raise HTTPException(status_code=401, detail="Invalid Key or Username") | |
| return x_username if x_username else "admin" | |
| def get_portfolios(username: str = Depends(get_current_user), db: Session = Depends(get_db)): | |
| portfolios = db.query(SavedPortfolio).filter(SavedPortfolio.username == username).all() | |
| return portfolios | |
| class PortfolioSaveRequest(BaseModel): | |
| name: str | |
| tickers: list | |
| weights: dict | |
| html_report: Optional[str] = None | |
| def save_portfolio(req: PortfolioSaveRequest, username: str = Depends(get_current_user), db: Session = Depends(get_db)): | |
| portfolio = SavedPortfolio( | |
| username=username, | |
| name=req.name, | |
| tickers=req.tickers, | |
| weights=req.weights, | |
| html_report=req.html_report | |
| ) | |
| db.add(portfolio) | |
| db.commit() | |
| return {"status": "success", "id": portfolio.id} | |
| def delete_portfolio(portfolio_id: int, username: str = Depends(get_current_user), db: Session = Depends(get_db)): | |
| portfolio = db.query(SavedPortfolio).filter(SavedPortfolio.id == portfolio_id, SavedPortfolio.username == username).first() | |
| if not portfolio: | |
| raise HTTPException(status_code=404, detail="Portfolio not found") | |
| db.delete(portfolio) | |
| db.commit() | |
| return {"status": "success"} | |
| def get_backtests(username: str = Depends(get_current_user), db: Session = Depends(get_db)): | |
| backtests = db.query(BacktestHistory).filter(BacktestHistory.username == username).order_by(BacktestHistory.executed_at.desc()).all() | |
| return backtests | |
| def update_webhook(payload: dict, username: str = Depends(get_current_user), db: Session = Depends(get_db)): | |
| config = db.query(WebhookConfig).filter(WebhookConfig.username == username).first() | |
| if not config: | |
| config = WebhookConfig( | |
| username=username, | |
| webhook_url=payload.get("url"), | |
| api_secret_key=f"whsec_{secrets.token_hex(16)}" | |
| ) | |
| db.add(config) | |
| else: | |
| config.webhook_url = payload.get("url") | |
| db.commit() | |
| masked_key = f"whsec_{config.api_secret_key.replace('whsec_', '')[:6]}***" if config.api_secret_key else "" | |
| return {"status": "success", "api_secret_key": masked_key} | |
| class AuthRequest(BaseModel): | |
| username: str = "" | |
| key: str | |
| class KeyGenRequest(BaseModel): | |
| admin_key: str | |
| new_key: str | |
| class PortfolioRequest(BaseModel): | |
| tickers: List[str] | |
| capital: float = 100000.0 | |
| risk_input: int = 5 | |
| model: int = 1 | |
| allocation_engine: int = 1 | |
| allow_shorting: bool = False | |
| tax_enabled: bool = False | |
| garch_enabled: bool = True | |
| currency: str = "$" | |
| custom_constraints: Optional[List[dict]] = None | |
| fixed_weights: Optional[dict] = None | |
| rebalance_freq_months: int = 3 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # SIMPLE UUID SESSION STORE β backed by a JSON file | |
| # /tmp is always writable on Linux (HuggingFace Spaces, Docker). | |
| # Falls back to app-dir/output on Windows. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _make_session_token(access_key: str, username: str = "") -> str: | |
| """Create a new UUID session, persist it in Redis, return the token.""" | |
| token = str(uuid.uuid4()).replace("-", "") # 32 hex chars, no special chars | |
| SESSION_MAX_AGE = 43200 # 12 hours | |
| try: | |
| session_data = {"access_key": access_key, "username": username} | |
| cache.redis_client.setex(f"session:{token}", SESSION_MAX_AGE, json.dumps(session_data)) | |
| print(f"[SESSION] created token for '{access_key}' ('{username}') in Redis") | |
| except Exception as e: | |
| logger.error(f"[SESSION] Redis set error: {e}") | |
| return token | |
| def _get_session_data(request: Request) -> Optional[dict]: | |
| token = request.cookies.get("we_session", "").strip().strip('"').strip("'") | |
| if not token or len(token) < 8: | |
| return None | |
| try: | |
| data = cache.redis_client.get(f"session:{token}") | |
| if data: | |
| return json.loads(data) | |
| except Exception as e: | |
| logger.error(f"[SESSION] Redis get error: {e}") | |
| return None | |
| def _validate_session_cookie(request: Request) -> bool: | |
| """Check if the session cookie token is valid and not expired.""" | |
| data = _get_session_data(request) | |
| if not data: | |
| token = request.cookies.get("we_session", "").strip().strip('"').strip("'") | |
| print(f"[SESSION] INVALID token={token[:16]}...") | |
| return False | |
| return True | |
| def _get_session_access_key(request: Request) -> Optional[str]: | |
| data = _get_session_data(request) | |
| return data.get("access_key") if data else None | |
| def _get_session_username(request: Request) -> Optional[str]: | |
| data = _get_session_data(request) | |
| return data.get("username") if data else None | |
| async def read_login(request: Request): | |
| return FileResponse(os.path.join(STATIC_DIR, "login.html"), media_type="text/html; charset=utf-8") | |
| async def read_index(request: Request): | |
| # Serve the app β real security is enforced on all /api/* endpoints per-request. | |
| # Client-side guard in index.html checks sessionStorage on load and redirects | |
| # to / if no valid key is found (prevents blank/broken UI without a key). | |
| return FileResponse(os.path.join(STATIC_DIR, "index.html"), media_type="text/html; charset=utf-8") | |
| async def read_options(request: Request): | |
| return FileResponse(os.path.join(STATIC_DIR, "options.html"), media_type="text/html; charset=utf-8") | |
| async def api_verify(request: Request, authorization: Optional[str] = Header(None)): | |
| """Quick key-validity check used by the client-side session guard on page load.""" | |
| key = None | |
| if authorization and authorization.startswith("Bearer "): | |
| key = authorization[7:] | |
| if not key: | |
| raise HTTPException(status_code=401, detail="No key provided") | |
| if access_manager.validate_key(key, silent=True): | |
| return JSONResponse({"valid": True, "is_master": access_manager.is_master_key(key)}) | |
| raise HTTPException(status_code=401, detail="Invalid key") | |
| async def api_logout(request: Request): | |
| response = JSONResponse({"status": "logged_out"}) | |
| response.delete_cookie("we_session", path="/") | |
| return response | |
| async def read_admin(): | |
| return FileResponse(os.path.join(STATIC_DIR, "admin.html")) | |
| async def market_ticker(): | |
| cached_data = cache.cache_get_json("market_ticker") | |
| if cached_data: | |
| return cached_data | |
| results = [] | |
| tickers = ["SPY", "QQQ", "TLT", "GLD", "BTC-USD", "UUP", "USO"] | |
| try: | |
| import yfinance as yf | |
| # Try batch download first | |
| df = yf.download(tickers, period="5d", progress=False, timeout=5) | |
| if not df.empty: | |
| if isinstance(df.columns, pd.MultiIndex): | |
| close_df = df['Close'] if 'Close' in df.columns.get_level_values(0) else df | |
| else: | |
| close_df = df | |
| for t in tickers: | |
| try: | |
| s = close_df[t].dropna() | |
| if len(s) >= 2: | |
| p1, p0 = float(s.iloc[-1]), float(s.iloc[-2]) | |
| chg = (p1 - p0) / p0 | |
| results.append({ | |
| "name": t, | |
| "price": f"{p1:.2f}", | |
| "change": round(chg, 6) | |
| }) | |
| except Exception as te: | |
| logger.warning(f"Ticker {t} extraction failed: {te}") | |
| except Exception as e: | |
| logger.error(f"Batch market ticker fetch failed: {e}") | |
| # Fallback: try each ticker individually | |
| try: | |
| import yfinance as yf | |
| for t in tickers: | |
| try: | |
| df_single = yf.download(t, period="5d", progress=False, timeout=5) | |
| if not df_single.empty: | |
| s = df_single['Close'].dropna() | |
| if len(s) >= 2: | |
| p1, p0 = float(s.iloc[-1]), float(s.iloc[-2]) | |
| chg = (p1 - p0) / p0 | |
| results.append({ | |
| "name": t, | |
| "price": f"{p1:.2f}", | |
| "change": round(chg, 6) | |
| }) | |
| except Exception as te2: | |
| logger.warning(f"Individual ticker {t} failed: {te2}") | |
| except Exception as e2: | |
| logger.error(f"Individual ticker fallback also failed: {e2}") | |
| if results: | |
| cache.cache_set_json("market_ticker", results, ttl=300) | |
| return results if results else (cache.cache_get_json("market_ticker") or []) | |
| async def finance_news(): | |
| cached_data = cache.cache_get_json("finance_news") | |
| if cached_data: | |
| return cached_data | |
| results = [] | |
| try: | |
| import yfinance as yf | |
| news = yf.Ticker("SPY").news | |
| for item in news[:15]: | |
| content = item.get("content", {}) | |
| title = content.get("title", item.get("title", "No Title")) | |
| pub_date = content.get("pubDate", item.get("providerPublishTime", "")) | |
| if isinstance(pub_date, int): | |
| import datetime | |
| pub_date = datetime.datetime.fromtimestamp(pub_date).strftime("%Y-%m-%d") | |
| elif "T" in str(pub_date): | |
| pub_date = str(pub_date).split("T")[0] | |
| provider = item.get("provider", {}) | |
| source = provider.get("displayName", item.get("publisher", "Yahoo Finance")) | |
| link = item.get("clickThroughUrl", {}).get("url", item.get("link", "")) | |
| results.append({ | |
| "title": title, | |
| "source": source, | |
| "time": pub_date, | |
| "url": link | |
| }) | |
| if results: | |
| cache.cache_set_json("finance_news", results, ttl=300) | |
| except Exception as e: | |
| logger.error(f"News fetch failed: {e}") | |
| return results or cache.cache_get_json("finance_news") or [] | |
| from api import RateLimiter | |
| async def api_auth(req: AuthRequest, request: Request): | |
| if not req.username.strip() and not access_manager.is_master_key(req.key): | |
| raise HTTPException(status_code=400, detail="Username is required") | |
| forwarded = request.headers.get("X-Forwarded-For") | |
| ip = forwarded.split(",")[0].strip() if forwarded else (request.client.host if request.client else "Unknown") | |
| try: | |
| if access_manager.validate_key(req.key, username=req.username, ip=ip): | |
| is_master = access_manager.is_master_key(req.key) | |
| # Issue a secure HTTP-only session cookie so /main cannot be bypassed via URL | |
| token = _make_session_token(req.key, req.username) | |
| response = JSONResponse({"status": "success", "message": "Access Granted", "is_master": is_master, "username": req.username}) | |
| response.set_cookie( | |
| key="we_session", | |
| value=token, | |
| httponly=True, # JS cannot read this cookie | |
| samesite="lax", # lax: allows top-level nav (strict blocks post-login redirect) | |
| max_age=43200, # 12 hours | |
| path="/" | |
| ) | |
| return response | |
| raise HTTPException(status_code=401, detail="Invalid or Expired Access Key") | |
| except ValueError as e: | |
| raise HTTPException(status_code=429, detail=str(e)) | |
| class AdminKeyGenRequest(BaseModel): | |
| admin_key: str | |
| username: str = "" | |
| hours: int = 1 | |
| class AdminRevokeRequest(BaseModel): | |
| admin_key: str | |
| target_key: str = "" | |
| confirm_token: str = "" | |
| target_key: str = "" | |
| confirm_token: str = "" | |
| target_key: str = "" | |
| confirm_token: str = "" | |
| target_key: str = "" | |
| confirm_token: str = "" | |
| target_key: str | |
| async def admin_generate(req: AdminKeyGenRequest): | |
| new_key = access_manager.generate_otk(req.admin_key, username=req.username, hours=req.hours) | |
| if new_key: | |
| return {"status": "success", "key": new_key, "message": f"OTK '{new_key}' created for {req.username}."} | |
| raise HTTPException(status_code=401, detail="Invalid Admin Key") | |
| async def admin_revoke(req: AdminRevokeRequest): | |
| success = access_manager.revoke_otk(req.admin_key, req.target_key) | |
| if success: | |
| return {"status": "success", "message": f"Key '{req.target_key}' revoked."} | |
| raise HTTPException(status_code=401, detail="Invalid Admin Key or Key Not Found") | |
| async def admin_list_keys(admin_key: str = Header(...)): | |
| keys = access_manager.get_all_keys(admin_key) | |
| if admin_key != access_manager.MASTER_KEY: | |
| raise HTTPException(status_code=401, detail="Invalid Admin Key") | |
| return {"keys": keys} | |
| import hashlib | |
| def get_action_token(action: str, admin_key: str) -> str: | |
| window = int(time.time() / 120) | |
| return hashlib.sha256(f"{admin_key}:{action}:{window}".encode()).hexdigest() | |
| async def get_admin_action_token(action: str, admin_key: str = Header(...)): | |
| if admin_key != access_manager.MASTER_KEY: | |
| raise HTTPException(status_code=401, detail="Invalid Admin Key") | |
| return {"token": get_action_token(action, admin_key)} | |
| async def admin_revoke_all(req: AdminRevokeRequest): | |
| if req.admin_key != access_manager.MASTER_KEY: | |
| raise HTTPException(status_code=401, detail="Invalid Admin Key") | |
| if getattr(req, "confirm_token", "") != get_action_token("revoke_all", req.admin_key): | |
| raise HTTPException(status_code=403, detail="Invalid or expired confirmation token") | |
| if getattr(req, "confirm_token", "") != get_action_token("revoke_all", req.admin_key): | |
| raise HTTPException(status_code=403, detail="Invalid or expired confirmation token") | |
| if getattr(req, "confirm_token", "") != get_action_token("revoke_all", req.admin_key): | |
| raise HTTPException(status_code=403, detail="Invalid or expired confirmation token") | |
| if getattr(req, "confirm_token", "") != get_action_token("revoke_all", req.admin_key): | |
| raise HTTPException(status_code=403, detail="Invalid or expired confirmation token") | |
| keys = access_manager.get_all_keys(req.admin_key) | |
| count = 0 | |
| for k, v in keys.items(): | |
| if not v.get("revoked", False): | |
| access_manager.revoke_otk(req.admin_key, k) | |
| count += 1 | |
| return {"status": "success", "revoked_count": count} | |
| async def admin_get_logs(admin_key: str = Header(...)): | |
| if admin_key != access_manager.MASTER_KEY: | |
| raise HTTPException(status_code=401, detail="Invalid Admin Key") | |
| from constants import OUTPUT_DIR | |
| import os | |
| from datetime import datetime, timedelta | |
| log_file = os.path.join(OUTPUT_DIR, "access.log") | |
| logs = [] | |
| if os.path.exists(log_file): | |
| cutoff = datetime.now() - timedelta(hours=24) | |
| with open(log_file, "r", encoding="utf-8") as f: | |
| for line in f: | |
| if not line.strip(): continue | |
| # Parse the datetime from the log line (e.g. "2026-06-12 22:33:45,491 - ...") | |
| try: | |
| dt_str = line.split(" - ")[0].split(",")[0] | |
| log_dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S") | |
| if log_dt >= cutoff: | |
| logs.append(line.strip()) | |
| except Exception: | |
| # If parsing fails, just append it | |
| logs.append(line.strip()) | |
| # Return last 500 lines max to prevent huge payloads | |
| return {"logs": logs[-500:]} | |
| async def preview_portfolio(req: PortfolioRequest, x_access_key: Optional[str] = Header(None), x_username: Optional[str] = Header(None)): | |
| if not access_manager.validate_key(x_access_key, username=x_username): | |
| raise HTTPException(status_code=401, detail="Unauthorized") | |
| try: | |
| overrides = { | |
| 'tickers': req.tickers, | |
| 'capital': req.capital, | |
| 'risk_input': req.risk_input, | |
| 'risk_factor': {1:0.1, 2:0.5, 3:1.0, 4:2.0, 5:3.0, 6:5.0, 7:7.5, 8:10.0, 9:15.0, 10:25.0}.get(req.risk_input, 3.0), | |
| 'model': req.model, | |
| 'allocation_engine': req.allocation_engine, | |
| 'single_asset_min': -1.0 if req.allow_shorting else 0.0, | |
| 'tax_enabled': req.tax_enabled, | |
| 'garch_enabled': req.garch_enabled, | |
| 'custom_constraints': req.custom_constraints, | |
| 'fixed_weights': req.fixed_weights | |
| } | |
| result = core_engine.run_engine(overrides=overrides, serve=False, preview_only=True) | |
| return { | |
| "status": "success", | |
| "target_weights": result.get("target_weights", {}), | |
| "efficient_frontier": result.get("efficient_frontier", {"vols": [], "rets": []}) | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def _run_optimization(tid, request_dict, x_access_key, x_username): | |
| try: | |
| def _format_currency(t): | |
| t = t.upper().strip() | |
| if len(t) == 3 and t in ['EUR', 'GBP', 'JPY', 'CHF', 'AUD', 'CAD', 'SEK', 'NOK', 'DKK', 'NZD']: | |
| return f"{t}USD=X" | |
| if len(t) == 3 and t in ['BTC', 'ETH', 'SOL', 'XRP', 'ADA', 'DOT', 'LTC', 'BNB', 'LINK', 'UNI']: | |
| return f"{t}-USD" | |
| if len(t) == 6 and t.endswith('USD'): | |
| return f"{t}=X" | |
| return t | |
| clean_tickers = [_format_currency(t) for t in request_dict["tickers"]] | |
| overrides = { | |
| 'universe': clean_tickers, | |
| 'tickers': clean_tickers, | |
| 'capital': request_dict["capital"], | |
| 'risk_input': request_dict["risk_input"], | |
| 'risk_factor': {1:0.1, 2:0.5, 3:1.0, 4:2.0, 5:3.0, 6:5.0, 7:7.5, 8:10.0, 9:15.0, 10:25.0}.get(request_dict["risk_input"], 3.0), | |
| 'model': request_dict["model"], | |
| 'allocation_engine': request_dict["allocation_engine"], | |
| 'single_asset_min': -1.0 if request_dict["allow_shorting"] else 0.0, | |
| 'tax_enabled': request_dict["tax_enabled"], | |
| 'garch_enabled': request_dict["garch_enabled"], | |
| 'currency_symbol': request_dict["currency"], | |
| 'custom_constraints': request_dict["custom_constraints"], | |
| 'fixed_weights': request_dict["fixed_weights"], | |
| 'rebalance_freq_months': request_dict["rebalance_freq_months"] | |
| } | |
| tracer.start_trace(tid) | |
| tracer.add_flag(tid, "TASK_INIT", f"Target Universe: {overrides.get('universe')}") | |
| # Determine if we are acting as a Proxy (e.g., on Render) or if we should run the math locally. | |
| is_proxy = os.getenv("RENDER") == "true" or os.getenv("IS_PROXY") == "true" | |
| is_backend = not is_proxy | |
| if is_backend: | |
| tracer.add_flag(tid, "BACKEND_START", "Running compute engine locally") | |
| # We are the backend (either HF or localhost). Run the heavy math. | |
| result = core_engine.run_engine(overrides=overrides, serve=False, task_id=tid) | |
| try: | |
| engine = database.get_pg_engine() | |
| from database import get_db_session | |
| with get_db_session() as db: | |
| stats = result.get('stats', {}) | |
| bt_stats = result.get('bt_stats', {}) | |
| html_report_content = result.get('html_report', "") | |
| if html_report_content: | |
| report_path = os.path.join(OUTPUT_DIR, "portfolio_report.html") | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| with open(report_path, "w", encoding="utf-8") as f: | |
| f.write(html_report_content) | |
| history = database.BacktestHistory( | |
| username=x_username or "admin", | |
| model_used=str(request_dict["model"]), | |
| return_pct=bt_stats.get('Annualized Return', 0), | |
| sharpe_ratio=bt_stats.get('Sharpe Ratio', 0), | |
| max_drawdown=bt_stats.get('Max Drawdown', 0), | |
| tickers=list(result.get('target_weights', {}).keys()), | |
| weights=result.get('target_weights', {}), | |
| html_report=html_report_content | |
| ) | |
| db.add(history) | |
| db.commit() | |
| # Keep only the last 15 backtests per user (FIFO) | |
| user_history = db.query(database.BacktestHistory).filter( | |
| database.BacktestHistory.username == (x_username or "admin") | |
| ).order_by(database.BacktestHistory.executed_at.desc()).all() | |
| if len(user_history) > 15: | |
| for old_run in user_history[15:]: | |
| db.delete(old_run) | |
| db.commit() | |
| # --- WEBHOOK EXECUTION --- | |
| config = db.query(database.WebhookConfig).filter(database.WebhookConfig.username == (x_username or "anonymous")).first() | |
| if config and config.webhook_url: | |
| import requests | |
| payload = { | |
| "model": request_dict["model"], | |
| "weights": result.get("target_weights", {}), | |
| "stats": stats, | |
| "status": "completed" | |
| } | |
| try: | |
| requests.post(config.webhook_url, json=payload, headers={"Authorization": f"Bearer {config.api_secret_key}"}, timeout=5) | |
| except Exception as wh_err: | |
| logger.error(f"Webhook failed to fire: {wh_err}") | |
| except Exception as e: | |
| logger.error(f"Failed to save backtest history or fire webhook: {e}") | |
| tracer.add_flag(tid, "HF_BACKEND_COMPLETE", "Math engine finished") | |
| # Fetch, update, and re-assign the entire dict to trigger FileBackedDict.__setitem__ and save() | |
| task_data = BACKGROUND_TASKS.get(tid, {}) | |
| task_data["status"] = "completed" | |
| task_data["message"] = "Report generated." | |
| task_data["target_weights"] = result.get("target_weights", {}) | |
| task_data["stats"] = result.get("stats", {}) | |
| BACKGROUND_TASKS[tid] = task_data | |
| else: | |
| import requests | |
| hf_url = os.getenv("HF_BACKEND_URL", "").rstrip('/') | |
| if not hf_url: | |
| tracer.add_flag(tid, "ERROR", "HF_BACKEND_URL not set in environment") | |
| BACKGROUND_TASKS[tid]["status"] = "error" | |
| BACKGROUND_TASKS[tid]["error"] = "HF_BACKEND_URL not configured for proxying." | |
| return | |
| # Use the master key to bypass HF API restrictions and authenticate internally | |
| hf_key = os.getenv("HF_MASTER_KEY", "") | |
| tracer.add_flag(tid, "PROXY_START", f"Forwarding to HF backend: {hf_url}") | |
| try: | |
| proxy_res = requests.post( | |
| f"{hf_url}/api/generate", | |
| json=request_dict, | |
| headers={"X-Access-Key": hf_key}, | |
| timeout=120 | |
| ) | |
| except requests.exceptions.Timeout: | |
| raise Exception("Hugging Face Backend timed out while queuing the optimization. This is likely due to the free tier spinning up from sleep.") | |
| except requests.exceptions.RequestException as req_e: | |
| raise Exception(f"Hugging Face Backend connection failed: {req_e}") | |
| if not proxy_res.ok: | |
| raise Exception(f"Hugging Face Backend Error ({proxy_res.status_code}) at {hf_url}/api/generate. Check HF_BACKEND_URL.") | |
| proxy_data = proxy_res.json() | |
| remote_task_id = proxy_data.get("task_id") | |
| tracer.add_flag(tid, "PROXY_HANDOFF_SUCCESS", f"HF Task ID: {remote_task_id}") | |
| if not remote_task_id: | |
| raise Exception("Failed to get remote task ID from Hugging Face.") | |
| import time | |
| retries = 0 | |
| while True: | |
| time.sleep(2) | |
| try: | |
| status_res = requests.get( | |
| f"{hf_url}/api/status/{remote_task_id}", | |
| headers={"X-Access-Key": hf_key}, | |
| timeout=15 | |
| ) | |
| retries = 0 # Reset on success | |
| except requests.exceptions.RequestException as e: | |
| retries += 1 | |
| if retries > 10: | |
| tracer.add_flag(tid, "PROXY_POLL_TIMEOUT", f"Poll failed 10 times: {e}") | |
| BACKGROUND_TASKS[tid]["status"] = "error" | |
| BACKGROUND_TASKS[tid]["message"] = "Hugging Face Backend is unreachable (Timeout). It might have crashed or restarted." | |
| break | |
| continue | |
| if status_res.ok: | |
| s_data = status_res.json() | |
| BACKGROUND_TASKS[tid]["status"] = s_data["status"] | |
| BACKGROUND_TASKS[tid]["message"] = s_data["message"] | |
| if s_data["status"] == "completed": | |
| BACKGROUND_TASKS[tid]["target_weights"] = s_data.get("target_weights", {}) | |
| BACKGROUND_TASKS[tid]["stats"] = s_data.get("stats", {}) | |
| BACKGROUND_TASKS[tid]["bt_stats"] = s_data.get("bt_stats", {}) | |
| # Download the completed HTML report from HF to Render | |
| report_res = requests.get(f"{hf_url}/report") | |
| if report_res.ok: | |
| report_path = os.path.join(OUTPUT_DIR, "portfolio_report.html") | |
| os.makedirs(OUTPUT_DIR, exist_ok=True) | |
| with open(report_path, "wb") as f: | |
| f.write(report_res.content) | |
| break | |
| elif s_data["status"] == "error": | |
| error_msg = s_data.get("message", "Unknown error from HF") | |
| tracer.add_flag(tid, "PROXY_POLL_ERROR", error_msg) | |
| BACKGROUND_TASKS[tid]["status"] = "error" | |
| BACKGROUND_TASKS[tid]["message"] = error_msg | |
| break | |
| else: | |
| tracer.add_flag(tid, "PROXY_CONNECTION_LOST", f"Status Code: {status_res.status_code}") | |
| retries += 1 | |
| if retries > 10: | |
| BACKGROUND_TASKS[tid]["status"] = "error" | |
| BACKGROUND_TASKS[tid]["message"] = f"Lost connection to Hugging Face backend (HTTP {status_res.status_code})." | |
| raise Exception(f"Lost connection to Hugging Face backend. (HTTP {status_res.status_code})") | |
| continue | |
| except (Exception, SystemExit) as e: | |
| error_trace = traceback.format_exc() | |
| tracer.add_flag(tid, "FATAL_ERROR", f"{str(e)}\n\nTraceback:\n{error_trace}") | |
| logger.error(f"Optimization failed: {error_trace}") | |
| task_data = BACKGROUND_TASKS.get(tid, {}) | |
| task_data["status"] = "error" | |
| task_data["message"] = f"Error: {str(e)} (Check console logs for details)" | |
| BACKGROUND_TASKS[tid] = task_data | |
| async def generate_portfolio(req: PortfolioRequest, x_access_key: Optional[str] = Header(None), x_username: Optional[str] = Header(None)): | |
| if not access_manager.validate_key(x_access_key, username=x_username): | |
| raise HTTPException(status_code=401, detail="Unauthorized") | |
| task_id = str(uuid.uuid4()) | |
| BACKGROUND_TASKS[task_id] = {"status": "running", "message": "Initializing...", "target_weights": {}} | |
| request_dict = req.model_dump() if hasattr(req, "model_dump") else req.dict() | |
| process_pool.submit(_run_optimization, task_id, request_dict, x_access_key, x_username) | |
| return { | |
| "status": "queued", | |
| "task_id": task_id, | |
| "message": "Optimization started in background." | |
| } | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # HFT SIMULATOR API | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class HFTRequest(BaseModel): | |
| symbols: list[str] | |
| duration_ms: int = 1000 | |
| latency_ms: int = 5 | |
| tick_ms: int = 10 | |
| strategy: Optional[str] = None | |
| target_qty: float = 100.0 | |
| def hft_simulate(req: HFTRequest, x_access_key: Optional[str] = Header(None)): | |
| if not access_manager.validate_key(x_access_key, silent=True): | |
| raise HTTPException(status_code=401, detail="Unauthorized") | |
| try: | |
| from hft_simulator import HFTSimulator | |
| from hft_strategies import MarketMakingStrategy, MomentumStrategy, MeanReversionStrategy, ExecutionBridgeStrategy | |
| import yfinance as yf | |
| # Initialize simulation | |
| start_time = datetime.now() | |
| duration_sec = req.duration_ms / 1000.0 | |
| sim = HFTSimulator(req.symbols, start_time, duration_sec, req.tick_ms, req.latency_ms) | |
| # Get live prices as starting point, fallback to 100 | |
| initial_prices = {} | |
| from concurrent.futures import ThreadPoolExecutor | |
| def fetch_price(sym): | |
| try: | |
| hist = yf.download(sym, period="1d", timeout=5) | |
| return sym, float(hist['Close'].iloc[-1]) if not hist.empty else 100.0 | |
| except Exception: | |
| return sym, 100.0 | |
| executor = ThreadPoolExecutor(max_workers=10) | |
| futures = {executor.submit(fetch_price, sym): sym for sym in req.symbols} | |
| from concurrent.futures import as_completed, TimeoutError | |
| try: | |
| for future in as_completed(futures.keys(), timeout=2.0): | |
| sym, price = future.result() | |
| initial_prices[sym] = price | |
| except TimeoutError: | |
| logging.warning("yfinance fetch timed out in HFT, falling back to synthetic prices.") | |
| for sym in req.symbols: | |
| if sym not in initial_prices: | |
| initial_prices[sym] = 100.0 | |
| finally: | |
| executor.shutdown(wait=False) | |
| sim.initialize_books(initial_prices) | |
| # Attach requested strategy | |
| if req.strategy == 'market_making': | |
| for sym in req.symbols: | |
| sim.add_strategy(MarketMakingStrategy(sym)) | |
| elif req.strategy == 'momentum': | |
| for sym in req.symbols: | |
| sim.add_strategy(MomentumStrategy(sym)) | |
| elif req.strategy == 'mean_reversion': | |
| for sym in req.symbols: | |
| sim.add_strategy(MeanReversionStrategy(sym)) | |
| elif req.strategy == 'execution': | |
| for sym in req.symbols: | |
| sim.add_strategy(ExecutionBridgeStrategy(sym, req.target_qty, 'buy', chunks=10)) | |
| results = sim.run() | |
| res_dict = results.to_dict() | |
| res_dict['initial_prices'] = initial_prices | |
| return {"status": "success", "results": res_dict} | |
| except Exception: | |
| import traceback | |
| logging.error(f"HFT Simulation failed: {traceback.format_exc()}") | |
| # Fallback to prevent UI crash | |
| from datetime import timedelta | |
| now = datetime.now() | |
| fallback_results = { | |
| "metrics": { | |
| "total_trades": 0, | |
| "volume": 0.0, | |
| "avg_spread": 0.01 | |
| }, | |
| "times": [now.isoformat(), (now + timedelta(milliseconds=req.duration_ms)).isoformat()], | |
| "mid_prices": [100.0, 100.0], | |
| "spreads": [0.01, 0.01], | |
| "trade_prices": [], | |
| "trade_times": [], | |
| "initial_prices": {sym: 100.0 for sym in req.symbols}, | |
| "final_depth": None | |
| } | |
| return {"status": "success", "results": fallback_results} | |
| def _cpp_worker(returns_array, weights_array, expected_returns_array, queue): | |
| import time | |
| try: | |
| import quant_engine_cpp | |
| cpp_results = {} | |
| # 1. Ledoit-Wolf | |
| start = time.time() | |
| cpp_cov = quant_engine_cpp.compute_ledoit_wolf_covariance(returns_array) | |
| cpp_results["ledoit_wolf_ms"] = (time.time() - start) * 1000 | |
| # 2. Monte Carlo (scaled down slightly for HF environments) | |
| start = time.time() | |
| quant_engine_cpp.run_monte_carlo(weights_array, expected_returns_array, cpp_cov, 5000, 252, 10000.0) | |
| cpp_results["monte_carlo_ms"] = (time.time() - start) * 1000 | |
| # 3. GARCH | |
| start = time.time() | |
| quant_engine_cpp.batch_fit_garch(returns_array) | |
| cpp_results["garch_ms"] = (time.time() - start) * 1000 | |
| queue.put({"status": "success", "results": cpp_results}) | |
| except Exception as e: | |
| queue.put({"status": "error", "error": str(e)}) | |
| def get_benchmark(x_access_key: Optional[str] = Header(None)): | |
| if not access_manager.validate_key(x_access_key, silent=True): | |
| raise HTTPException(status_code=401, detail="Unauthorized") | |
| import time | |
| import numpy as np | |
| # Generate random returns for benchmarking | |
| # 250 days, 100 assets | |
| rng = np.random.default_rng(42) | |
| returns = rng.normal(0, 0.02, (250, 100)) | |
| weights = np.ones(100) / 100.0 | |
| expected_returns = rng.normal(0.05, 0.02, 100) | |
| results = { | |
| "python": {}, | |
| "cpp": {} | |
| } | |
| # --- PYTHON BENCHMARKS --- | |
| # 1. Ledoit-Wolf (simplified for benchmark to prevent OpenBLAS threadpool deadlocks) | |
| start = time.time() | |
| try: | |
| py_cov = np.cov(returns, rowvar=False) | |
| except Exception: | |
| py_cov = np.eye(100) | |
| results["python"]["ledoit_wolf_ms"] = (time.time() - start) * 1000 | |
| # 2. Monte Carlo (Pure Python) | |
| def py_monte_carlo(weights, expected_returns, cov_matrix, num_simulations, days, initial_portfolio_value, rng): | |
| L = np.linalg.cholesky(cov_matrix + np.eye(len(weights))*1e-8) | |
| mu_daily = expected_returns / 252.0 | |
| final_vals = np.zeros(num_simulations) | |
| for s in range(num_simulations): | |
| port_val = initial_portfolio_value | |
| for d in range(days): | |
| z = rng.normal(0, 1, len(weights)) | |
| shocks = L @ z | |
| daily_returns = mu_daily + shocks | |
| port_ret = weights @ daily_returns | |
| port_val *= (1.0 + port_ret) | |
| final_vals[s] = port_val | |
| return final_vals | |
| start = time.time() | |
| py_monte_carlo(weights, expected_returns, py_cov, 5000, 252, 10000.0, rng) | |
| results["python"]["monte_carlo_ms"] = (time.time() - start) * 1000 | |
| # 3. GARCH (Pure Python fallback) | |
| def py_garch(returns_1d): | |
| t = len(returns_1d) | |
| cond_vol = np.zeros(t) | |
| var0 = np.var(returns_1d) | |
| h = var0 | |
| cond_vol[0] = np.sqrt(var0) | |
| ll = 0.0 | |
| alpha, beta, omega = 0.1, 0.8, var0 * 0.1 | |
| for i in range(1, t): | |
| h = omega + alpha * returns_1d[i-1]**2 + beta * h | |
| if h < 1e-8: h = 1e-8 | |
| cond_vol[i] = np.sqrt(h) | |
| ll += -0.5 * (np.log(2 * np.pi) + np.log(h) + (returns_1d[i]**2) / h) | |
| return ll | |
| start = time.time() | |
| import concurrent.futures | |
| with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: | |
| try: | |
| list(executor.map(py_garch, [returns[:, i] for i in range(100)], timeout=5.0)) | |
| except concurrent.futures.TimeoutError: | |
| pass | |
| results["python"]["garch_ms"] = (time.time() - start) * 1000 | |
| # --- C++ BENCHMARKS --- | |
| import multiprocessing as mp | |
| cpp_error_holder = {} | |
| # _cpp_worker is now defined at module level | |
| # Use 'spawn' context if possible to ensure clean memory state, or default Process | |
| try: | |
| ctx = mp.get_context('spawn') | |
| except Exception: | |
| ctx = mp | |
| q = ctx.Queue() | |
| p = ctx.Process(target=_cpp_worker, args=(returns, weights, expected_returns, q)) | |
| p.daemon = True | |
| p.start() | |
| p.join(timeout=8.0) | |
| if p.is_alive(): | |
| p.terminate() | |
| p.join() | |
| cpp_error_holder['error'] = "C++ module timed out or hung" | |
| else: | |
| if not q.empty(): | |
| worker_res = q.get() | |
| if worker_res["status"] == "success": | |
| results["cpp"] = worker_res["results"] | |
| results["cpp_available"] = True | |
| else: | |
| cpp_error_holder['error'] = worker_res["error"] | |
| else: | |
| cpp_error_holder['error'] = "C++ process crashed unexpectedly (Segfault/OOM)" | |
| if 'error' in cpp_error_holder: | |
| import logging | |
| logging.getLogger(__name__).warning(f"C++ engine benchmark failed: {cpp_error_holder['error']}") | |
| results["cpp_available"] = False | |
| results["cpp"]["ledoit_wolf_ms"] = 0 | |
| results["cpp"]["monte_carlo_ms"] = 0 | |
| results["cpp"]["garch_ms"] = 0 | |
| return {"status": "success", "results": results} | |
| async def get_all_traces(): | |
| return tracer.traces | |
| async def get_task_trace(task_id: str): | |
| return {"task_id": task_id, "trace": tracer.get_trace(task_id)} | |
| def is_running_on_hf() -> bool: | |
| return os.environ.get("SPACE_ID") is not None | |
| async def get_task_status(task_id: str, x_access_key: Optional[str] = Header(None)): | |
| if not access_manager.validate_key(x_access_key, silent=True): | |
| raise HTTPException(status_code=401, detail="Unauthorized") | |
| is_proxy = os.getenv("RENDER") == "true" or os.getenv("IS_PROXY") == "true" | |
| is_backend = not is_proxy | |
| if is_backend: | |
| task = BACKGROUND_TASKS.get(task_id) | |
| if not task: | |
| raise HTTPException(status_code=404, detail="Task not found") | |
| return task | |
| else: | |
| # Check local task state first in case proxying the generate request failed | |
| local_task = BACKGROUND_TASKS.get(task_id) | |
| if local_task and local_task.get("status") == "error": | |
| return local_task | |
| hf_url = os.getenv("HF_BACKEND_URL", "").rstrip('/') | |
| if not hf_url: | |
| return {"status": "error", "error": "HF_BACKEND_URL not configured for proxying status check."} | |
| try: | |
| import requests | |
| hf_res = requests.get( | |
| f"{hf_url}/api/status/{task_id}", | |
| headers={"X-Access-Key": x_access_key}, | |
| timeout=5 | |
| ) | |
| if not hf_res.ok: | |
| return {"status": "error", "message": f"Backend returned {hf_res.status_code}", "task_id": task_id} | |
| return hf_res.json() | |
| except requests.exceptions.Timeout: | |
| return {"status": "running", "message": "Backend polling timeout...", "task_id": task_id} | |
| except Exception as e: | |
| return {"status": "error", "message": f"Proxy error: {str(e)}", "task_id": task_id} | |
| async def get_report(): | |
| is_proxy = os.getenv("RENDER") == "true" or os.getenv("IS_PROXY") == "true" | |
| is_backend = not is_proxy | |
| if is_backend: | |
| report_path = os.path.join(OUTPUT_DIR, "portfolio_report.html") | |
| if os.path.exists(report_path): | |
| return FileResponse(report_path) | |
| raise HTTPException(status_code=404, detail="Report not generated yet.") | |
| else: | |
| hf_url = os.getenv("HF_BACKEND_URL", "").rstrip('/') | |
| if not hf_url: | |
| raise HTTPException(status_code=500, detail="HF_BACKEND_URL not configured.") | |
| import requests | |
| try: | |
| res = requests.get(f"{hf_url}/report", timeout=10) | |
| if res.ok: | |
| from fastapi.responses import HTMLResponse | |
| return HTMLResponse(content=res.text) | |
| else: | |
| raise HTTPException(status_code=res.status_code, detail="Report not ready on backend.") | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| def _alert_daemon(): | |
| """Background daemon to check for market drops and perform monthly maintenance.""" | |
| import time | |
| last_cleanup = time.time() | |
| while True: | |
| try: | |
| # Wake up every 1 hour (3600 seconds) | |
| time.sleep(3600) | |
| # 1. Check for SPY drops | |
| try: | |
| ticker = yf.Ticker("SPY") | |
| hist = ticker.history(period="2d") | |
| if len(hist) >= 2: | |
| current = float(hist['Close'].iloc[-1]) | |
| prev = float(hist['Close'].iloc[-2]) | |
| pct_change = ((current - prev) / prev) * 100 | |
| if pct_change <= -5.0: | |
| access_manager.send_telegram_alert(f"π¨ **MARKET ALERT**\nSPY has dropped by {pct_change:.2f}%!\nCheck the portfolio engine.") | |
| except Exception: | |
| pass | |
| # 2. Monthly DB/Log Cleanup | |
| # 30 days = 2592000 seconds | |
| if time.time() - last_cleanup > 2592000: | |
| try: | |
| logger.info("Performing monthly database and log cleanup...") | |
| # access_manager should clean up its expired keys and sync to Redis | |
| if hasattr(access_manager, 'cleanup_expired_keys'): | |
| access_manager.cleanup_expired_keys() | |
| last_cleanup = time.time() | |
| except Exception as e: | |
| logger.error(f"Monthly cleanup failed: {e}") | |
| except Exception: | |
| pass # Suppress daemon errors | |
| import warnings | |
| warnings.filterwarnings('ignore', category=DeprecationWarning, module='fastapi') | |
| def startup_event(): | |
| import threading | |
| import sqlite3 | |
| import os | |
| # Auto-migrate SQLite schema to include backtest_history JSON columns | |
| db_path = os.path.join(OUTPUT_DIR, 'portfolio_db.sqlite3') | |
| if os.path.exists(db_path): | |
| try: | |
| conn = sqlite3.connect(db_path) | |
| c = conn.cursor() | |
| try: | |
| c.execute("ALTER TABLE backtest_history ADD COLUMN tickers JSON") | |
| except Exception: | |
| pass | |
| try: | |
| c.execute("ALTER TABLE backtest_history ADD COLUMN weights JSON") | |
| except Exception: | |
| pass | |
| try: | |
| c.execute("ALTER TABLE backtest_history ADD COLUMN html_report TEXT") | |
| except Exception: | |
| pass | |
| try: | |
| c.execute("ALTER TABLE saved_portfolios ADD COLUMN html_report TEXT") | |
| except Exception: | |
| pass | |
| try: | |
| c.execute(""" | |
| CREATE TABLE IF NOT EXISTS user_memory ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| access_key VARCHAR UNIQUE, | |
| memory_text VARCHAR NOT NULL, | |
| updated_at DATETIME DEFAULT CURRENT_TIMESTAMP | |
| ) | |
| """) | |
| except Exception: | |
| pass | |
| conn.commit() | |
| conn.close() | |
| except Exception as e: | |
| logger.error(f"Failed to auto-migrate SQLite database: {e}") | |
| # Start the background alert daemon | |
| alert_thread = threading.Thread(target=_alert_daemon, daemon=True) | |
| alert_thread.start() | |
| from sqlalchemy import text | |
| async def health_check(db: Session = Depends(get_db)): | |
| health_status = {'status': 'ok', 'redis': 'ok', 'postgres': 'ok'} | |
| try: | |
| if redis_client: | |
| redis_client.ping() | |
| else: | |
| health_status['redis'] = 'disabled' | |
| except Exception as e: | |
| health_status['redis'] = f'error: {e}' | |
| health_status['status'] = 'degraded' | |
| try: | |
| db.execute(text('SELECT 1')) | |
| except Exception as e: | |
| health_status['postgres'] = f'error: {e}' | |
| health_status['status'] = 'degraded' | |
| if health_status['status'] == 'degraded': | |
| raise HTTPException(status_code=503, detail=health_status) | |
| return health_status | |
| from sqlalchemy import text | |
| async def health_check(db: Session = Depends(get_db)): | |
| health_status = {'status': 'ok', 'redis': 'ok', 'postgres': 'ok'} | |
| try: | |
| if redis_client: | |
| redis_client.ping() | |
| else: | |
| health_status['redis'] = 'disabled' | |
| except Exception as e: | |
| health_status['redis'] = f'error: {e}' | |
| health_status['status'] = 'degraded' | |
| try: | |
| db.execute(text('SELECT 1')) | |
| except Exception as e: | |
| health_status['postgres'] = f'error: {e}' | |
| health_status['status'] = 'degraded' | |
| if health_status['status'] == 'degraded': | |
| raise HTTPException(status_code=503, detail=health_status) | |
| return health_status | |
| def shutdown_event(): | |
| logger.info("Shutting down application...") | |
| logger.info("Shutting down process pool executor...") | |
| process_pool.shutdown(wait=True) | |
| try: | |
| if redis_client: | |
| redis_client.close() | |
| logger.info("Redis connection closed.") | |
| except Exception as e: | |
| logger.error(f"Failed closing Redis: {e}") | |
| try: | |
| from database import engine | |
| engine.dispose() | |
| logger.info("Database engine disposed.") | |
| except Exception as e: | |
| logger.error(f"Failed disposing DB engine: {e}") | |
| logger.info("Application shutdown complete.") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| # Use reload=False by default to prevent thread killing on file writes (like tasks.json or sqlite) | |
| # If reload is needed, exclude the output directory and database files. | |
| should_reload = os.getenv("DEBUG_RELOAD") == "true" | |
| uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=should_reload, reload_excludes=["output/*", "*.db", "*.sqlite3"]) | |
| class AdminClearRequest(BaseModel): | |
| admin_key: str | |
| confirm_token: str = "" | |
| confirm_token: str = "" | |
| confirm_token: str = "" | |
| confirm_token: str = "" | |
| def admin_clear_backtests(req: AdminClearRequest, db: Session = Depends(get_db)): | |
| if req.admin_key != access_manager.MASTER_KEY: | |
| raise HTTPException(status_code=401, detail="Invalid Master Key") | |
| count = db.query(BacktestHistory).delete() | |
| db.commit() | |
| return {"status": "success", "deleted_count": count} | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # ADVANCED QUANTITATIVE FEATURES (OPTIONS) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # ADVANCED QUANTITATIVE FEATURES (STAT ARB) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class StatArbRequest(BaseModel): | |
| tickers: List[str] | |
| p_value_threshold: float = 0.05 | |
| run_backtest: bool = True | |
| def scan_stat_arb(req: StatArbRequest, x_access_key: Optional[str] = Header(None)): | |
| if not access_manager.validate_key(x_access_key, silent=True): | |
| raise HTTPException(status_code=401, detail="Unauthorized") | |
| try: | |
| from stat_arb import find_cointegrated_pairs | |
| from backtest import run_stat_arb_backtest | |
| import yfinance as yf | |
| # 1. Fetch Data | |
| hist_data = yf.download(req.tickers, period="2y", timeout=5)['Close'] | |
| if hist_data.empty: | |
| raise HTTPException(status_code=400, detail="Failed to fetch historical data") | |
| # Clean data (drop columns with all NaNs and forward fill) | |
| hist_data = hist_data.dropna(axis=1, how='all').ffill() | |
| # 2. Find Pairs | |
| pairs = find_cointegrated_pairs(hist_data, p_value_threshold=req.p_value_threshold) | |
| if not pairs: | |
| return {"pairs": [], "message": "No cointegrated pairs found."} | |
| # 3. Optional Backtest on top pair | |
| top_pair_result = None | |
| if req.run_backtest and pairs: | |
| best_pair = pairs[0] | |
| t1, t2 = best_pair["pair"] | |
| top_pair_result = run_stat_arb_backtest(hist_data, t1, t2, best_pair["hedge_ratio"]) | |
| return { | |
| "pairs": pairs, | |
| "top_pair_backtest": top_pair_result | |
| } | |
| except Exception as e: | |
| logger.error(f"Error running stat arb scan: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # ADVANCED QUANTITATIVE FEATURES (CRYPTO ARB) | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| class CryptoArbRequest(BaseModel): | |
| symbol: str = "BTC/USDT" | |
| capital: float = 10000.0 | |
| def scan_crypto_arb(req: CryptoArbRequest, x_access_key: Optional[str] = Header(None)): | |
| if not access_manager.validate_key(x_access_key, silent=True): | |
| raise HTTPException(status_code=401, detail="Unauthorized") | |
| try: | |
| from crypto_arb import fetch_exchange_prices, find_arbitrage_opportunities | |
| from execution import execute_crypto_arbitrage | |
| # 1. Fetch live prices | |
| prices = fetch_exchange_prices(req.symbol) | |
| # 2. Find opportunities | |
| opps = find_arbitrage_opportunities(prices) | |
| # 3. Simulate execution for the best one | |
| execution_result = None | |
| if opps: | |
| best_opp = opps[0] | |
| execution_result = execute_crypto_arbitrage(best_opp, req.capital) | |
| if execution_result and 'profit_usd' not in execution_result: | |
| execution_result['profit_usd'] = execution_result.get('net_profit_usd', 0) | |
| return { | |
| "symbol": req.symbol, | |
| "prices": prices, | |
| "opportunities": opps, | |
| "execution": execution_result | |
| } | |
| except Exception as e: | |
| logger.error(f"Error running crypto arb scan: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| from pydantic import BaseModel | |
| class EggDiscoverRequest(BaseModel): | |
| egg_id: str | |
| def get_eggs(username: str = Depends(get_current_user), db: Session = Depends(get_db)): | |
| import database | |
| progress = db.query(database.UserEggProgress).filter(database.UserEggProgress.username == username).first() | |
| if progress: | |
| return {"discovered_eggs": progress.discovered_eggs, "vault_unlocked": bool(progress.vault_unlocked)} | |
| return {"discovered_eggs": [], "vault_unlocked": False} | |
| def discover_egg(req: EggDiscoverRequest, username: str = Depends(get_current_user), db: Session = Depends(get_db)): | |
| import database | |
| progress = db.query(database.UserEggProgress).filter(database.UserEggProgress.username == username).first() | |
| if not progress: | |
| progress = database.UserEggProgress(username=username, discovered_eggs=[req.egg_id], vault_unlocked=0) | |
| db.add(progress) | |
| else: | |
| eggs = list(progress.discovered_eggs) | |
| if req.egg_id not in eggs: | |
| eggs.append(req.egg_id) | |
| progress.discovered_eggs = eggs | |
| db.commit() | |
| return {"status": "success"} | |
| def unlock_vault(username: str = Depends(get_current_user), db: Session = Depends(get_db)): | |
| import database | |
| progress = db.query(database.UserEggProgress).filter(database.UserEggProgress.username == username).first() | |
| if not progress: | |
| progress = database.UserEggProgress(username=username, discovered_eggs=[], vault_unlocked=1) | |
| db.add(progress) | |
| else: | |
| progress.vault_unlocked = 1 | |
| db.commit() | |
| return {"status": "success"} | |