Memory guard: 512MB hard cap, GC at 256MB, +swap
This commit is contained in:
@@ -29,6 +29,35 @@ import sys
|
|||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
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 config.fee_tiers import get_perp_fees, PERPS_TIERS, STAKING_TIERS, STRATEGY_FEE_MODELS
|
||||||
from common.risk import risk_summary
|
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
|
import uvicorn
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
@@ -109,6 +138,7 @@ def broadcast_loop():
|
|||||||
"""Continuously read metrics and broadcast to all clients."""
|
"""Continuously read metrics and broadcast to all clients."""
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
check_memory()
|
||||||
data = read_metrics()
|
data = read_metrics()
|
||||||
payload = json.dumps(data, default=str)
|
payload = json.dumps(data, default=str)
|
||||||
for ws in list(connected_clients):
|
for ws in list(connected_clients):
|
||||||
@@ -437,6 +467,63 @@ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|||||||
# Main
|
# 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():
|
def main():
|
||||||
import argparse
|
import argparse
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
|||||||
Reference in New Issue
Block a user