Files
worldquant-alpha-system/backend/tests/browser_server.py
T

212 lines
8.3 KiB
Python

"""Isolated browser acceptance server: temporary database, synthetic upstream, no real network."""
import tempfile
from contextlib import asynccontextmanager
from datetime import datetime, timedelta, timezone
import httpx
from cryptography.fernet import Fernet
from app.config import Settings
from app.main import create_app
from app.models import Base
from app.worldquant import WqClient
from tests.ai_fake import fake_model
from tests.backtest_fake import Platform
from tests.catalog_fake import catalog_response
TEST_PASSWORD = "browser-test-password"
def sample(index):
super_alpha = index % 7 == 0
language = "PYTHON" if index % 11 == 0 and not super_alpha else "FASTEXPR"
names = [
"短期价格反转",
"成交量与价格背离",
"分析师预期修正",
"行业中性动量",
"低波动质量信号",
"资金流向偏离",
"组合选择实验",
]
return {
"id": f"TEST{index:04}",
"name": names[index % len(names)] + f" · {index:03}",
"type": "SUPER" if super_alpha else "REGULAR",
"stage": "OS" if index % 3 == 0 else "IS",
"status": "ACTIVE" if index % 3 == 0 else "UNSUBMITTED",
"hidden": index % 5 == 0,
"regular": None
if super_alpha
else {
"code": "def alpha(data):\n return -data.close.rank()"
if language == "PYTHON"
else "group_neutralize(-rank(ts_delta(close, 5)), industry)"
},
"selection": {"code": "self_correlation < 0.5"} if super_alpha else None,
"combo": {"code": "alpha"} if super_alpha else None,
"settings": {
"region": ["USA", "CHN", "EUR"][(index // 3) % 3],
"universe": "TOP3000",
"language": language,
"delay": 1,
"decay": 4,
"neutralization": "INDUSTRY",
"truncation": 0.08,
"pasteurization": "ON",
},
"is": {
"sharpe": None if index % 13 == 0 else round(0.8 + (index % 25) * 0.1, 2),
"fitness": round(0.3 + (index % 20) * 0.06, 2),
"turnover": round(0.08 + (index % 12) * 0.015, 3),
"returns": 0.09,
"margin": 0.0002,
"drawdown": 0.11,
"checks": [{"name": "LOW_SHARPE", "result": "PASS", "value": 2.1, "limit": 1.58}],
},
"os": {"sharpe": 1.1} if index % 3 == 0 else None,
"dateCreated": (datetime(2025, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=index)).isoformat(),
"dateSubmitted": (datetime(2025, 2, 1, tzinfo=timezone.utc) + timedelta(days=index % 2)).isoformat()
if index % 3 == 0
else None,
}
def create_test_app():
temporary = tempfile.TemporaryDirectory(prefix="wq-browser-test-")
settings = Settings(
_env_file=None,
database_url=f"sqlite+aiosqlite:///{temporary.name}/test.db",
admin_password=TEST_PASSWORD,
encryption_key=Fernet.generate_key().decode(),
public_origin="http://127.0.0.1:5179",
)
records = [sample(i) for i in range(620)]
simulations = Platform()
simulations.existing_alpha_ids = [f"TEST{i:04}" for i in range(1, 100)]
def upstream(request):
path = request.url.path
if path == "/authentication" and request.method == "POST":
return httpx.Response(
201,
json={
"user": {"id": "TEST_USER"},
"token": {"expiry": 14400},
"permissions": [
"CONSULTANT",
"SUPER_ALPHA",
"MULTI_SIMULATION",
"PROD_ALPHAS",
"BRAIN_LABS",
"BRAIN_LABS_JUPYTER_LAB",
"REFERRAL",
"VISUALIZATION",
"WORKDAY",
"BEFORE_AND_AFTER_PERFORMANCE_V2",
],
},
headers={"Set-Cookie": "mock=only; Path=/"},
)
if path.startswith("/simulations") or (
path.startswith("/alphas/") and path.rsplit("/", 1)[-1] in simulations.alphas
):
return simulations(request)
if request.method != "GET":
raise AssertionError("Browser acceptance attempted an upstream mutation")
catalog = catalog_response(request)
if catalog is not None:
return catalog
if path == "/users/self":
return httpx.Response(
200,
json={
"id": "TEST_USER",
"fullName": "模拟研究员",
"email": "test@example.com",
"level": "CONSULTANT",
"geniusLevel": "GOLD",
"verified": True,
"approved": True,
"dateCreated": "2025-01-01T00:00:00Z",
},
)
if path == "/users/self/alphas/summary":
return httpx.Response(200, json={"unsubmitted": 413, "active": 207, "decommissioned": 0})
if path in ("/users/self/activities/simulations", "/users/self/activities/submissions"):
from zoneinfo import ZoneInfo
today = datetime.now(ZoneInfo("America/New_York")).date()
return httpx.Response(
200,
json={
"yesterday": {"end": (today - timedelta(days=1)).isoformat(), "value": 2},
"total": {"value": 207},
"records": {
"schema": {"properties": [{"name": "date"}, {"name": "value"}]},
"records": [[today.isoformat(), 3]],
},
},
)
if path == "/users/self/alphas":
unsubmitted = "status" in request.url.params
hidden = request.url.params.get("hidden") == "true"
matched = [
r for r in records if (r["status"] == "UNSUBMITTED") == unsubmitted and r["hidden"] == hidden
]
for field in ("dateCreated", "dateSubmitted"):
for suffix in (">=", "<"):
boundary = request.url.params.get(field + suffix)
if boundary:
bound = datetime.fromisoformat(boundary).replace(tzinfo=timezone.utc)
matched = [
r
for r in matched
if r.get(field)
and (
datetime.fromisoformat(r[field]) >= bound
if suffix == ">="
else datetime.fromisoformat(r[field]) < bound
)
]
offset, limit = (
int(request.url.params.get("offset", 0)),
int(request.url.params.get("limit", 100)),
)
return httpx.Response(
200, json={"count": len(matched), "results": matched[offset : offset + limit]}
)
if path.endswith("/recordsets/pnl"):
return httpx.Response(
200,
json={
"schema": {"properties": [{"name": "date"}, {"name": "pnl"}]},
"records": [
[
(datetime(2025, 1, 1) + timedelta(days=i)).date().isoformat(),
i * 250 + (i % 13) * 85,
]
for i in range(180)
],
},
)
record = next((r for r in records if path == f"/alphas/{r['id']}"), None)
return httpx.Response(200, json=record) if record else httpx.Response(404)
application = create_app(
settings, WqClient(settings, transport=httpx.MockTransport(upstream)), ai_model_factory=fake_model
)
original_lifespan = application.router.lifespan_context
@asynccontextmanager
async def lifespan(app):
async with app.state.engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
async with original_lifespan(app):
yield
temporary.cleanup()
application.router.lifespan_context = lifespan
return application