feat: add initial frontend setup with styles, types, and testing framework
- Created a global CSS file for styling the frontend with responsive design. - Introduced TypeScript types for various entities including Research, Alpha, and Account. - Implemented Playwright tests for account management and data synchronization workflows. - Configured TypeScript with strict settings and included necessary libraries. - Set up Vite as the build tool with React plugin and API proxy configuration. - Added a Python script to initialize environment variables securely.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"""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
|
||||
|
||||
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],
|
||||
"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(days=index)).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
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)]
|
||||
|
||||
def upstream(request):
|
||||
path = request.url.path
|
||||
if path == "/authentication" and request.method == "POST":
|
||||
return httpx.Response(
|
||||
201, json={"user": {"id": "TEST_USER"}}, headers={"Set-Cookie": "mock=only; Path=/"}
|
||||
)
|
||||
if request.method != "GET":
|
||||
raise AssertionError("Browser acceptance attempted an upstream mutation")
|
||||
if path == "/users/self":
|
||||
return httpx.Response(
|
||||
200, json={"id": "TEST_USER", "name": "模拟研究员", "email": "test@example.com"}
|
||||
)
|
||||
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
|
||||
]
|
||||
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)))
|
||||
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
|
||||
Reference in New Issue
Block a user