diff --git a/dashboard/server.py b/dashboard/server.py index c672a4c..3f27e46 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -29,6 +29,35 @@ import sys sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from config.fee_tiers import get_perp_fees, PERPS_TIERS, STAKING_TIERS, STRATEGY_FEE_MODELS from common.risk import risk_summary +from strategies.quant_report import compute_quant_report + +# ═══════════════════════════════════════════════════════════ +# Memory guard: cap RSS at 512MB, GC-aggressive at 256MB +# ═══════════════════════════════════════════════════════════ +import resource, gc, signal + +MEM_SOFT_LIMIT = 256 * 1024 * 1024 # 256 MB — force GC +MEM_HARD_LIMIT = 512 * 1024 * 1024 # 512 MB — terminate + +resource.setrlimit(resource.RLIMIT_AS, (MEM_HARD_LIMIT, MEM_HARD_LIMIT)) + +def check_memory(): + """Check RSS, force GC if over soft limit, raise if over hard limit.""" + try: + with open("/proc/self/status") as f: + for line in f: + if line.startswith("VmRSS:"): + rss_kb = int(line.split()[1]) + rss = rss_kb * 1024 + if rss > MEM_HARD_LIMIT: + print(f"[CRIT] RSS {rss_kb // 1024}MB > 512MB — exiting", flush=True) + os._exit(1) + if rss > MEM_SOFT_LIMIT: + gc.collect() + gc.collect() + return + except Exception: + pass import uvicorn # ═══════════════════════════════════════════════════════════ @@ -109,6 +138,7 @@ def broadcast_loop(): """Continuously read metrics and broadcast to all clients.""" while True: time.sleep(1) + check_memory() data = read_metrics() payload = json.dumps(data, default=str) for ws in list(connected_clients): @@ -437,6 +467,63 @@ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") # Main # ═══════════════════════════════════════════════════════════ +@app.get("/api/quant-report/{name}") +async def get_quant_report(name: str): + """Compute full QF-Lib quant report from a backtest file. + Accepts strategy name and auto-maps to filename prefix. + """ + # Strategy name → file prefix mapping + NAME_MAP = { + "order book imbalance": "ofi", + "avellaneda-stoikov": "avellaneda", + "funding rate arb": "funding_arb", + "iceberg detection": "iceberg", + "momentum breakout": "momentum", + "mean reversion": "mean_rev", + "kalman pairs": "kalman_pairs", + "pairs trading": "pairs", + } + + name_lower = name.lower() + prefix = NAME_MAP.get(name_lower, name_lower.replace(" ", "_")) + + # Build candidate paths + candidates = [] + exact_path = os.path.join(BACKTEST_DIR, name) + hist_exact = os.path.join(HISTORICAL_DIR, name) + candidates.extend([exact_path, hist_exact]) + + # Try exact match + for path in candidates: + if os.path.exists(path): + backtest_path = path + break + else: + # Fuzzy match: find files starting with the mapped prefix + fuzzy = [] + for d in [BACKTEST_DIR, HISTORICAL_DIR]: + if not os.path.exists(d): continue + for f in os.listdir(d): + f_clean = f.lower() + # Match by prefix, then prefer BTC/ETH files + if f_clean.startswith(f"{prefix}_"): + fuzzy.append(os.path.join(d, f)) + if fuzzy: + backtest_path = fuzzy[0] + else: + return JSONResponse({"error": f"Backtest '{name}' not found"}, status_code=404) + try: + with open(backtest_path) as f: + data = json.load(f) + trades = data.get("trades", data.get("trade_history", [])) + strategy_name = data.get("name", data.get("strategy", name)) + strategy_id = data.get("id", name) + report = compute_quant_report(strategy_name, strategy_id, trades, 100.0) + return JSONResponse(report) + except Exception as e: + return JSONResponse({"error": str(e)}, status_code=500) + + def main(): import argparse parser = argparse.ArgumentParser()