DevWizard-Vandan
Implement wake_event synchronization to instantly resume recovery loop upon HTTP cookie updates
f168c51
Raw
History Blame Contribute Delete
33.5 kB
import asyncio
import core.backup # Restore DB from HF before database session is initialized!
import logging
from typing import List, Dict, Any, Optional
from datetime import datetime, timezone, timedelta
import random
import os
from fastapi import FastAPI, Request, Form, Depends, HTTPException, status
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.middleware.cors import CORSMiddleware
from database import SessionLocal, AlphaRecord
from wq_client import BrainClient
import config
import main
import requests
from requests.auth import HTTPBasicAuth
logger = logging.getLogger("App")
app = FastAPI(title="QuantForge Dashboard")
# Allow Chrome extensions (chrome-extension://*) and any origin to POST /login-cookie
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["POST", "GET", "OPTIONS"],
allow_headers=["*"],
)
@app.on_event("startup")
async def startup_event():
# Start background database backup loop
core.backup.start_backup()
# Automatically start the miner loop task on application startup
logger.info("Application startup: auto-starting miner loop...")
main.MINER_RUNNING = True
main.MINER_TASK = asyncio.create_task(main.miner_loop())
os.makedirs("templates", exist_ok=True)
app.mount("/static", StaticFiles(directory="templates"), name="static")
templates = Jinja2Templates(directory="templates")
import time
# Simple mock session management for the demo
SESSION_TOKEN = "quantforge_secure_session_token"
def is_system_logged_in() -> bool:
"""
Checks if there is a valid WQ session cookie or credentials configured globally
and successfully authenticated with the WorldQuant Brain API.
Caches the validation status for 15 seconds to avoid flooding the API.
"""
cookie = os.getenv("WQ_SESSION_COOKIE")
username = getattr(config, "WQ_USERNAME", None)
password = getattr(config, "WQ_PASSWORD", None)
if not username:
return False
now = time.time()
# Cache key: WQ credentials and cookie hash to auto-invalidate if updated
cache_key = (username, password, cookie)
if (
hasattr(is_system_logged_in, "_cached_time")
and now - is_system_logged_in._cached_time < 15
and getattr(is_system_logged_in, "_cached_key", None) == cache_key
):
return is_system_logged_in._cached_val
val = False
if cookie:
if "session=" in cookie or "t=" in cookie or ";" in cookie:
cookie_header = cookie
else:
cookie_header = f"t={cookie}; session={cookie}"
try:
r = requests.get("https://api.worldquantbrain.com/users/self", headers={"Cookie": cookie_header}, timeout=5)
val = (r.status_code == 200)
except Exception:
val = False
elif password:
try:
r = requests.post("https://api.worldquantbrain.com/authentication", auth=HTTPBasicAuth(username, password), timeout=5)
val = (r.status_code == 201)
except Exception:
val = False
is_system_logged_in._cached_val = val
is_system_logged_in._cached_time = now
is_system_logged_in._cached_key = cache_key
return val
def is_authenticated(request: Request) -> bool:
# Rule 3: If the session isn't logged in anywhere, everyone needs to log in
if not is_system_logged_in():
return False
# Rule 1: If anyone on the same wifi/ip address tries to open the link,
# they can see everything without logging in, if the session is logged in somewhere.
if is_home_wifi(request):
return True
# Rule 2: If another person tries with different ip, they must enter correct credentials
token = request.cookies.get("session_token")
return token == SESSION_TOKEN
def get_client_ip(request: Request) -> str:
x_forwarded_for = request.headers.get("x-forwarded-for")
if x_forwarded_for:
return x_forwarded_for.split(",")[0].strip()
return request.client.host
def is_home_wifi(request: Request) -> bool:
client_ip = get_client_ip(request)
whitelist = config.get_home_ip_whitelist()
logger.debug(f"is_home_wifi check: client_ip={client_ip}, whitelist={whitelist}")
if client_ip in ("127.0.0.1", "localhost", "::1", "testclient"):
return True
if not whitelist:
return False
return client_ip in whitelist
def check_auth(request: Request):
if not is_authenticated(request):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
def update_env_file(username, password):
env_path = os.path.join(os.path.dirname(__file__), ".env")
if os.path.exists(env_path):
with open(env_path, "r") as f:
lines = f.readlines()
new_lines = []
has_username = False
has_password = False
for line in lines:
if line.startswith("WQ_USERNAME="):
new_lines.append(f"WQ_USERNAME={username}\n")
has_username = True
elif line.startswith("WQ_PASSWORD="):
new_lines.append(f"WQ_PASSWORD={password}\n")
has_password = True
else:
new_lines.append(line)
if not has_username:
new_lines.append(f"WQ_USERNAME={username}\n")
if not has_password:
new_lines.append(f"WQ_PASSWORD={password}\n")
with open(env_path, "w") as f:
f.writelines(new_lines)
# Also persist to miner_settings.json for persistence across Hugging Face Space restarts
try:
config.save_wq_credentials(username=username, password=password)
except Exception as e:
logger.error(f"Failed to persist credentials to miner_settings.json: {e}")
def update_env_cookie(username, cookie):
env_path = os.path.join(os.path.dirname(__file__), ".env")
if os.path.exists(env_path):
with open(env_path, "r") as f:
lines = f.readlines()
new_lines = []
has_username = False
has_cookie = False
for line in lines:
if line.startswith("WQ_USERNAME="):
new_lines.append(f"WQ_USERNAME={username}\n")
has_username = True
elif line.startswith("WQ_SESSION_COOKIE="):
new_lines.append(f"WQ_SESSION_COOKIE={cookie}\n")
has_cookie = True
else:
new_lines.append(line)
if not has_username:
new_lines.append(f"WQ_USERNAME={username}\n")
if not has_cookie:
new_lines.append(f"WQ_SESSION_COOKIE={cookie}\n")
with open(env_path, "w") as f:
f.writelines(new_lines)
# Also persist to miner_settings.json for persistence across Hugging Face Space restarts
try:
config.save_wq_credentials(username=username, session_cookie=cookie)
except Exception as e:
logger.error(f"Failed to persist session cookie to miner_settings.json: {e}")
def test_wq_credentials(username, password):
url = "https://api.worldquantbrain.com/authentication"
try:
response = requests.post(url, auth=HTTPBasicAuth(username, password), timeout=10)
return response.status_code == 201
except Exception as e:
logger.error(f"Error authenticating with WQ: {e}")
return False
@app.get("/health")
async def health():
return {"status": "healthy"}
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
authenticated = is_authenticated(request)
client_ip = get_client_ip(request)
return templates.TemplateResponse(
request=request,
name="index.html",
context={
"authenticated": authenticated,
"logged_in": authenticated,
"client_ip": client_ip
}
)
@app.post("/login")
async def login(request: Request, username: str = Form(...), password: str = Form(...)):
# Validate locally against current memory state first
if username == config.WQ_USERNAME and password == config.WQ_PASSWORD:
try:
client = BrainClient()
except Exception as e:
return JSONResponse(status_code=400, content={"message": f"WorldQuant Auth Failed: {e}"})
response = JSONResponse(content={"message": "Success"})
response.set_cookie(
key="session_token",
value=SESSION_TOKEN,
httponly=True,
max_age=2592000, # 30 days in seconds
samesite="none",
secure=True
)
return response
# If not matching or placeholder, test against WQ Brain API directly
if test_wq_credentials(username, password):
# Update .env
update_env_file(username, password)
# Update memory
config.WQ_USERNAME = username
config.WQ_PASSWORD = password
response = JSONResponse(content={"message": "Success"})
response.set_cookie(
key="session_token",
value=SESSION_TOKEN,
httponly=True,
max_age=2592000, # 30 days in seconds
samesite="none",
secure=True
)
return response
return JSONResponse(status_code=401, content={"message": "Invalid Credentials"})
@app.post("/logout")
async def logout():
response = JSONResponse(content={"message": "Logged out"})
response.delete_cookie("session_token", samesite="none", secure=True)
return response
@app.get("/logout")
async def logout_get():
response = RedirectResponse(url="/", status_code=302)
response.delete_cookie("session_token", samesite="none", secure=True)
return response
# JSON variant of login-cookie used by the dashboard JS fetch calls
@app.post("/login-cookie")
async def login_cookie_json(request: Request):
"""Accepts both JSON body {cookie: '...'} and Form data."""
content_type = request.headers.get("content-type", "")
if "application/json" in content_type:
body = await request.json()
cookie = body.get("cookie", "").strip()
username = body.get("username", config.WQ_USERNAME or "").strip()
else:
form = await request.form()
cookie = str(form.get("cookie", "")).strip()
username = str(form.get("username", config.WQ_USERNAME or "")).strip()
if not cookie:
return JSONResponse(status_code=400, content={"status": "error", "detail": "Cookie is required"})
if "session=" in cookie or "t=" in cookie or ";" in cookie:
cookie_header = cookie
else:
cookie_header = f"t={cookie}; session={cookie}"
headers = {"Cookie": cookie_header}
url = "https://api.worldquantbrain.com/users/self"
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
update_env_cookie(username, cookie)
config.WQ_USERNAME = username
os.environ["WQ_SESSION_COOKIE"] = cookie
# Signal the Telegram recovery loop to wake up and resume the miner
try:
main.COOKIE_UPDATED_EVENT.set()
logger.info("COOKIE_UPDATED_EVENT set — waking up recovery loop.")
except Exception as e:
logger.warning(f"Could not set COOKIE_UPDATED_EVENT: {e}")
resp = JSONResponse(content={"status": "success"})
resp.set_cookie(key="session_token", value=SESSION_TOKEN, httponly=True,
max_age=2592000, samesite="none", secure=True)
resp.headers["Access-Control-Allow-Origin"] = "*"
return resp
else:
return JSONResponse(status_code=401, content={"status": "error",
"detail": f"Session cookie validation failed (HTTP {response.status_code})"})
except Exception as e:
logger.error(f"Error validating session cookie: {e}")
return JSONResponse(status_code=400, content={"status": "error", "detail": f"Connection error: {e}"})
# Simple pause/resume for miner (used by dashboard toggle button)
@app.post("/pause")
async def pause_miner(request: Request):
main.MINER_RUNNING = False
return {"status": "paused"}
@app.post("/resume")
async def resume_miner(request: Request):
if not main.MINER_RUNNING:
main.MINER_RUNNING = True
main.MINER_TASK = asyncio.create_task(main.miner_loop())
return {"status": "running"}
# MINER CONTROL
@app.post("/api/miner/{action}")
async def control_miner(action: str, request: Request):
check_auth(request)
if action == "start":
if main.MINER_RUNNING:
return {"status": "Already running"}
main.MINER_RUNNING = True
main.MINER_TASK = asyncio.create_task(main.miner_loop())
return {"status": "Started"}
elif action == "stop":
main.MINER_RUNNING = False
return {"status": "Stopped"}
elif action == "status":
return {"running": main.MINER_RUNNING}
return {"error": "Invalid action"}
# DATA APIs
def categorize_alpha(expression: str) -> str:
"""Derive category from expression syntax."""
expr = expression.lower()
if any(x in expr for x in ["volume", "vwap", "close", "open", "high", "low"]):
return "PV"
if any(x in expr for x in ["ebit", "capex", "sales", "assets", "debt", "income"]):
return "Fundamental"
if any(x in expr for x in ["sentiment", "news", "relevance"]):
return "Sentiment"
if any(x in expr for x in ["implied_vol", "put_call", "strike"]):
return "Options"
if any(x in expr for x in ["analyst", "eps", "recommendation"]):
return "Analyst"
return "Other"
def generate_mock_pnl(final_return: float) -> List[float]:
"""Generates a synthetic timeseries for the line chart."""
days = 60
pnl = [0.0]
drift = final_return / days
volatility = 0.005 # 0.5% daily volatility mock
for _ in range(days):
step = drift + random.gauss(0, volatility)
pnl.append(pnl[-1] + step)
return pnl
@app.get("/api/stats")
async def get_stats(request: Request):
from database.db import SessionLocal as SyncSessionLocal
from database.models import Alpha as AlphaV2
from sqlalchemy import func
# Try to get the active LLM model from main module
active_llm = "Unknown"
try:
import main as main_module
active_llm = getattr(main_module, "ACTIVE_LLM_MODEL", "Unknown")
except Exception:
pass
# Legacy DB stats (AlphaRecord from old app.py DB)
db = SessionLocal()
try:
legacy_generated = db.query(AlphaRecord).count()
legacy_simulating = db.query(AlphaRecord).filter_by(status="SIMULATING").count()
legacy_submitted = db.query(AlphaRecord).filter_by(status="SUBMITTED").count()
finally:
db.close()
# V2 DB stats (Alpha from database.models, used by core pipeline)
try:
v2_db = SyncSessionLocal()
try:
total_generated = v2_db.query(AlphaV2).count()
pending_gen = v2_db.query(AlphaV2).filter(AlphaV2.status == "PENDING_GEN").count()
simulating = v2_db.query(AlphaV2).filter(AlphaV2.status == "SIMULATING").count()
pending_submit = v2_db.query(AlphaV2).filter(AlphaV2.status == "PENDING_SUBMIT").count()
submitted = v2_db.query(AlphaV2).filter(AlphaV2.status == "SUBMITTED").count()
total_trashed = v2_db.query(AlphaV2).filter(AlphaV2.status == "TRASHED").count()
# Trash reason breakdown
trash_rows = (
v2_db.query(AlphaV2.trash_reason, func.count(AlphaV2.id))
.filter(AlphaV2.status == "TRASHED")
.group_by(AlphaV2.trash_reason)
.all()
)
trash_breakdown = {
"No Trades / Null Metrics": 0,
"Low Sharpe / Fitness": 0,
"High Correlation": 0,
"Syntax / Schema Error": 0,
}
for reason, cnt in trash_rows:
if reason in trash_breakdown:
trash_breakdown[reason] = cnt
else:
trash_breakdown["Syntax / Schema Error"] += cnt # bucket unknowns
# Avg metrics for passed alphas
avg_row = v2_db.query(
func.avg(AlphaV2.sharpe),
func.avg(AlphaV2.fitness),
func.avg(AlphaV2.turnover),
).filter(AlphaV2.status.in_(["PENDING_SUBMIT", "SUBMITTED"])).first()
avg_sharpe = round(avg_row[0] or 0, 4)
avg_fitness = round(avg_row[1] or 0, 4)
avg_turnover = round((avg_row[2] or 0) * 100, 2) # as %
total_evaluated = total_trashed + submitted + pending_submit
success_rate = round((submitted + pending_submit) / total_evaluated * 100, 1) if total_evaluated > 0 else 0.0
finally:
v2_db.close()
except Exception as e:
# Fallback if V2 DB not accessible
total_generated = legacy_generated
pending_gen = 0
simulating = legacy_simulating
pending_submit = 0
submitted = legacy_submitted
total_trashed = 0
trash_breakdown = {"No Trades / Null Metrics": 0, "Low Sharpe / Fitness": 0, "High Correlation": 0, "Syntax / Schema Error": 0}
avg_sharpe = 0.0
avg_fitness = 0.0
avg_turnover = 0.0
success_rate = 0.0
return {
"total_generated": total_generated,
"pending_gen": pending_gen,
"simulating": simulating,
"pending_submit": pending_submit,
"submitted": submitted,
"total_trashed": total_trashed,
"trash_breakdown": trash_breakdown,
"avg_sharpe": avg_sharpe,
"avg_fitness": avg_fitness,
"avg_turnover": avg_turnover,
"success_rate": success_rate,
"active_llm_model": active_llm,
}
@app.get("/api/queue")
async def get_queue(request: Request):
db = SessionLocal()
try:
pending = db.query(AlphaRecord).filter(
AlphaRecord.status.in_(["PENDING_CORRELATION", "PASSED_CORRELATION"])
).all()
authenticated = is_authenticated(request)
return [
{
"id": a.id,
"expression": a.expression if authenticated else "[REDACTED (Home WiFi Only)]",
"sharpe": a.sharpe,
"fitness": a.fitness,
"turnover": a.turnover,
"returns": a.returns,
"score": (a.fitness or 0.0) * (a.sharpe or 0.0),
"status": a.status,
"region": a.region,
"universe": a.universe,
"delay": a.delay,
"decay": a.decay,
"truncation": a.truncation,
"neutralization": a.neutralization,
"alpha_id": a.alpha_id
}
for a in pending
]
finally:
db.close()
@app.get("/api/failed")
async def get_failed(request: Request):
db = SessionLocal()
try:
failed = db.query(AlphaRecord).filter(AlphaRecord.status.like("%FAIL%")).order_by(AlphaRecord.id.desc()).limit(10).all()
authenticated = is_authenticated(request)
return [
{
"id": a.id,
"expression": a.expression if authenticated else "[REDACTED (Home WiFi Only)]",
"error_log": a.error_log if authenticated else "[REDACTED (Home WiFi Only)]"
}
for a in failed
]
finally:
db.close()
@app.get("/api/logs")
async def get_logs(request: Request):
check_auth(request)
log_file = config.DATA_DIR / "miner_production.log"
if not log_file.exists():
return {"logs": "No logs yet."}
try:
with open(log_file, "r") as f:
lines = f.readlines()
return {"logs": "".join(lines[-100:])}
except Exception as e:
return {"logs": f"Error reading logs: {e}"}
@app.get("/api/system_status")
async def get_system_status(request: Request):
import agent
authenticated = is_authenticated(request)
current_model = agent.CURRENT_MODEL if authenticated else "[REDACTED]"
running = main.MINER_RUNNING
db = SessionLocal()
try:
cutoff = datetime.utcnow() - timedelta(hours=24)
daily_submissions = db.query(AlphaRecord).filter(
AlphaRecord.status == "SUBMITTED",
AlphaRecord.date_submitted >= cutoff
).count()
finally:
db.close()
res = {
"running": running,
"current_model": current_model,
"daily_submissions": daily_submissions,
"daily_limit": 20,
"concurrent_limit": config.get_concurrent_limit() if authenticated else 0
}
if authenticated:
res["client_ip"] = get_client_ip(request)
res["home_ip_whitelist"] = ",".join(config.get_home_ip_whitelist())
return res
@app.post("/api/settings")
async def update_settings(request: Request):
check_auth(request)
try:
data = await request.json()
# 1. Update concurrent limit
if "concurrent_limit" in data:
limit = int(data.get("concurrent_limit", 3))
if limit < 1 or limit > 50:
raise HTTPException(status_code=400, detail="Invalid limit value")
config.set_concurrent_limit(limit)
# 2. Update home IP whitelist
if "home_ip_whitelist" in data:
whitelist_str = data.get("home_ip_whitelist", "")
ips = [ip.strip() for ip in whitelist_str.split(",") if ip.strip()]
config.set_home_ip_whitelist(ips)
# 3. Update Telegram settings
telegram_token = data.get("telegram_bot_token")
telegram_chat = data.get("telegram_chat_id")
telegram_proxy = data.get("telegram_api_proxy")
if telegram_token is not None or telegram_chat is not None or telegram_proxy is not None:
config.save_wq_credentials(
telegram_bot_token=telegram_token,
telegram_chat_id=telegram_chat,
telegram_api_proxy=telegram_proxy
)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/quick-auth", response_class=HTMLResponse)
async def quick_auth():
return """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QuantForge &mdash; Cookie Sync</title>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap" rel="stylesheet">
<style>
}
.step {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.step-number {
width: 28px;
height: 28px;
background: var(--primary-accent);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
font-size: 14px;
flex-shrink: 0;
box-shadow: 0 0 10px var(--primary-glow);
}
.step-text {
font-size: 15px;
line-height: 1.5;
color: #d1d5db;
}
.code-box {
background: rgba(0,0,0,0.2);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
font-family: monospace;
font-size: 12px;
word-break: break-all;
color: var(--text-muted);
margin: 16px 0;
max-height: 100px;
overflow-y: auto;
}
.btn {
background: var(--primary-accent);
color: white;
border: none;
border-radius: 12px;
padding: 14px 24px;
font-size: 15px;
font-weight: 600;
width: 100%;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px var(--primary-glow);
}
.btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn:active {
transform: translateY(0);
}
</style>
</head>
<body>
<div class="container">
<h1>One-Tap Cookie Sync</h1>
<div class="subtitle">Sync your biometric WorldQuant session cookie to the miner in one tap, even on mobile!</div>
<div class="step">
<div class="step-number">1</div>
<div class="step-text">
Click below to copy the Bookmarklet JS code to your clipboard:
<div class="code-box" id="codeText">javascript:(function(){let t=null;const ck=document.cookie.split('; ').find(r=>r.startsWith('t=')||r.startsWith('session='));if(ck){t=ck.split('=')[1];}if(!t){for(let i=0;i<localStorage.length;i++){const k=localStorage.key(i);const v=localStorage.getItem(k);if(v){const m=v.match(/eyJ[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+/);if(m){t=m[0];break;}}}}if(!t){for(let i=0;i<sessionStorage.length;i++){const k=sessionStorage.key(i);const v=sessionStorage.getItem(k);if(v){const m=v.match(/eyJ[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+/);if(m){t=m[0];break;}}}}if(!t){alert('❌ Active session cookie or token not found. Please log in first!');return;}fetch('https://vanam56-quantforge-miner.hf.space/login-cookie',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cookie:t})}).then(r=>{if(r.ok){alert('🚀 QuantForge: Cookie synced successfully! Miner is resuming.');}else{alert('❌ Failed to sync cookie: '+r.status);}}).catch(e=>alert('❌ Network error: '+e));})();</div>
<button class="btn" onclick="copyCode()">Copy Sync Script</button>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-text">
Bookmark <b>any</b> page in your browser (e.g. this page), edit the bookmark, rename it to <b>"Sync Cookie"</b>, and paste the copied code as the bookmark URL.
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-text">
Go to <a href="https://worldquantbrain.com" target="_blank" style="color: #818cf8; text-decoration: none;">WorldQuant BRAIN</a> and log in. Complete your face/fingerprint scan.
</div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-text">
Tap your browser address bar, type <b>"Sync Cookie"</b>, and select the bookmarklet you created. Your session will sync instantly!
</div>
</div>
</div>
<script>
function copyCode() {
const code = document.getElementById('codeText').innerText;
navigator.clipboard.writeText(code).then(() => {
alert('Copied to clipboard! Ready to save as bookmark.');
});
}
</script>
</body>
</html>
"""
@app.post("/api/whitelist/add")
async def add_whitelist_ip(request: Request):
check_auth(request)
try:
data = await request.json()
ip = data.get("ip", "").strip()
if not ip:
ip = get_client_ip(request)
current_whitelist = config.get_home_ip_whitelist()
if ip not in current_whitelist:
current_whitelist.append(ip)
config.set_home_ip_whitelist(current_whitelist)
logger.info(f"Added IP {ip} to whitelist")
return {"status": "success", "ip": ip, "whitelist": current_whitelist}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.post("/api/whitelist/remove")
async def remove_whitelist_ip(request: Request):
check_auth(request)
try:
data = await request.json()
ip = data.get("ip", "").strip()
if not ip:
raise HTTPException(status_code=400, detail="IP address is required")
current_whitelist = config.get_home_ip_whitelist()
if ip in current_whitelist:
current_whitelist.remove(ip)
config.set_home_ip_whitelist(current_whitelist)
logger.info(f"Removed IP {ip} from whitelist")
return {"status": "success", "ip": ip, "whitelist": current_whitelist}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/api/archive")
async def get_archive(request: Request):
db = SessionLocal()
try:
alphas = db.query(AlphaRecord).filter(
AlphaRecord.status.notin_(["GENERATED", "SIMULATING"])
).order_by(AlphaRecord.id.desc()).all()
authenticated = is_authenticated(request)
return [
{
"id": a.id,
"expression": a.expression if authenticated else "[REDACTED (Home WiFi Only)]",
"sharpe": a.sharpe,
"fitness": a.fitness,
"turnover": a.turnover,
"returns": a.returns,
"status": a.status,
"error_log": a.error_log if authenticated else "[REDACTED (Home WiFi Only)]",
"date_created": a.date_created.replace(tzinfo=timezone.utc).isoformat() if a.date_created else None,
"region": a.region,
"universe": a.universe,
"delay": a.delay,
"decay": a.decay,
"truncation": a.truncation,
"neutralization": a.neutralization,
"alpha_id": a.alpha_id
}
for a in alphas
]
finally:
db.close()
@app.get("/api/submission_queue")
async def get_submission_queue(request: Request):
db = SessionLocal()
try:
alphas = db.query(AlphaRecord).filter(
AlphaRecord.status.in_(["PENDING_CORRELATION", "PASSED_CORRELATION"])
).all()
authenticated = is_authenticated(request)
queue_list = []
for a in alphas:
score = (a.fitness or 0.0) * (a.sharpe or 0.0)
queue_list.append({
"id": a.id,
"expression": a.expression if authenticated else "[REDACTED (Home WiFi Only)]",
"sharpe": a.sharpe,
"fitness": a.fitness,
"turnover": a.turnover,
"returns": a.returns,
"score": score,
"status": a.status,
"region": a.region,
"universe": a.universe,
"delay": a.delay,
"decay": a.decay,
"truncation": a.truncation,
"neutralization": a.neutralization,
"alpha_id": a.alpha_id
})
queue_list.sort(key=lambda x: x["score"], reverse=True)
return queue_list
finally:
db.close()
@app.get("/api/submittable_archive")
async def get_submittable_archive(request: Request):
db = SessionLocal()
try:
alphas = db.query(AlphaRecord).filter(
AlphaRecord.status.in_(["PASSED_CORRELATION", "SUBMITTED"])
).order_by(AlphaRecord.id.desc()).all()
authenticated = is_authenticated(request)
archive_list = []
for a in alphas:
score = (a.fitness or 0.0) * (a.sharpe or 0.0)
archive_list.append({
"id": a.id,
"expression": a.expression if authenticated else "[REDACTED (Home WiFi Only)]",
"sharpe": a.sharpe,
"fitness": a.fitness,
"turnover": a.turnover,
"returns": a.returns,
"score": score,
"status": a.status,
"region": a.region,
"universe": a.universe,
"delay": a.delay,
"decay": a.decay,
"truncation": a.truncation,
"neutralization": a.neutralization,
"alpha_id": a.alpha_id,
"date_created": a.date_created.replace(tzinfo=timezone.utc).isoformat() if a.date_created else None,
"date_submitted": a.date_submitted.replace(tzinfo=timezone.utc).isoformat() if a.date_submitted else None
})
archive_list.sort(key=lambda x: x["score"], reverse=True)
return archive_list
finally:
db.close()
@app.get("/api/alpha/{alpha_id}/pnl")
async def get_alpha_pnl(alpha_id: int, request: Request):
authenticated = is_authenticated(request)
if not authenticated:
return {"pnl": []}
db = SessionLocal()
try:
alpha = db.query(AlphaRecord).filter_by(id=alpha_id).first()
if not alpha or alpha.returns is None:
return {"pnl": []}
return {"pnl": generate_mock_pnl(alpha.returns)}
finally:
db.close()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)