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,199 @@
|
||||
"""Normalize upstream business data, without making platform or research decisions."""
|
||||
|
||||
import math
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import or_, select
|
||||
|
||||
from .models import Alpha, Research, ResearchTag, now
|
||||
|
||||
SENSITIVE_KEYS = {
|
||||
"password",
|
||||
"token",
|
||||
"cookies",
|
||||
"cookie",
|
||||
"authorization",
|
||||
"credentials",
|
||||
"secret",
|
||||
"accesstoken",
|
||||
"refreshtoken",
|
||||
"sessiontoken",
|
||||
"clientsecret",
|
||||
"authorizationheader",
|
||||
"setcookie",
|
||||
"apikey",
|
||||
"csrftoken",
|
||||
"xsrftoken",
|
||||
"authentication",
|
||||
}
|
||||
|
||||
|
||||
def sanitize(value):
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
k: sanitize(v) for k, v in value.items() if re.sub(r"[^a-z]", "", k.lower()) not in SENSITIVE_KEYS
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [sanitize(v) for v in value]
|
||||
if isinstance(value, float) and not math.isfinite(value):
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
def number(value):
|
||||
if value is None or isinstance(value, bool):
|
||||
return None
|
||||
try:
|
||||
result = float(value)
|
||||
return result if math.isfinite(result) else None
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def date(value):
|
||||
try:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00")) if value else None
|
||||
except (ValueError, TypeError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
def code(value):
|
||||
return value.get("code") if isinstance(value, dict) else value if isinstance(value, str) else None
|
||||
|
||||
|
||||
async def upsert_alpha(db, raw: dict):
|
||||
alpha_id = raw.get("id")
|
||||
if not isinstance(alpha_id, str) or not alpha_id:
|
||||
raise ValueError("Alpha 数据缺少 ID")
|
||||
item = await db.get(Alpha, alpha_id)
|
||||
if item is None:
|
||||
item = Alpha(id=alpha_id)
|
||||
db.add(item)
|
||||
settings = raw.get("settings") or {}
|
||||
metrics = raw.get("is") if isinstance(raw.get("is"), dict) else {}
|
||||
item.name = raw.get("name")
|
||||
item.expression = code(raw.get("regular"))
|
||||
item.selection, item.combo = code(raw.get("selection")), code(raw.get("combo"))
|
||||
item.alpha_type, item.language = raw.get("type"), settings.get("language")
|
||||
item.stage, item.status, item.hidden = raw.get("stage"), raw.get("status"), raw.get("hidden") is True
|
||||
item.region, item.universe = settings.get("region"), settings.get("universe")
|
||||
item.settings, item.is_metrics = sanitize(settings), sanitize(metrics)
|
||||
item.os_metrics = sanitize(raw.get("os")) if isinstance(raw.get("os"), dict) else {}
|
||||
item.checks = sanitize(metrics.get("checks") or raw.get("checks") or [])
|
||||
for key in ("sharpe", "fitness", "returns", "turnover", "margin", "drawdown"):
|
||||
setattr(item, key, number(metrics.get(key)))
|
||||
item.date_created, item.date_submitted = date(raw.get("dateCreated")), date(raw.get("dateSubmitted"))
|
||||
item.synced_at, item.raw = now(), sanitize(raw)
|
||||
await db.flush()
|
||||
if await db.get(Research, alpha_id) is None:
|
||||
db.add(Research(alpha_id=alpha_id))
|
||||
return item
|
||||
|
||||
|
||||
def list_statement(filters):
|
||||
query = select(Alpha, Research).join(Research, Research.alpha_id == Alpha.id)
|
||||
q = filters.q
|
||||
if q:
|
||||
pattern = "%" + q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%"
|
||||
query = query.where(
|
||||
or_(
|
||||
*(
|
||||
getattr(Alpha, f).ilike(pattern, escape="\\")
|
||||
for f in ("id", "name", "expression", "selection", "combo")
|
||||
)
|
||||
)
|
||||
)
|
||||
for name in ("region", "universe", "alpha_type", "language", "status", "stage", "hidden"):
|
||||
value = getattr(filters, name)
|
||||
if value is not None:
|
||||
query = query.where(getattr(Alpha, name) == value)
|
||||
if filters.research_state:
|
||||
query = query.where(Research.state == filters.research_state)
|
||||
if filters.favorite is not None:
|
||||
query = query.where(Research.favorite == filters.favorite)
|
||||
if filters.tag:
|
||||
query = query.where(Alpha.id.in_(select(ResearchTag.alpha_id).where(ResearchTag.tag == filters.tag)))
|
||||
if filters.created_from:
|
||||
query = query.where(Alpha.date_created >= filters.created_from)
|
||||
if filters.created_to:
|
||||
query = query.where(Alpha.date_created <= filters.created_to)
|
||||
for name in ("sharpe", "fitness", "returns", "turnover", "margin", "drawdown"):
|
||||
for suffix, compare in (("min", "ge"), ("max", "le")):
|
||||
value = getattr(filters, f"{name}_{suffix}")
|
||||
if value is not None:
|
||||
column = getattr(Alpha, name)
|
||||
query = query.where(column >= value if compare == "ge" else column <= value)
|
||||
return query
|
||||
|
||||
|
||||
def sorted_statement(query, sort, direction):
|
||||
column = getattr(Alpha, sort)
|
||||
order = column.desc() if direction == "desc" else column.asc()
|
||||
return query.order_by(order.nullslast(), Alpha.id.asc())
|
||||
|
||||
|
||||
def summary(item: Alpha, research: Research):
|
||||
keys = (
|
||||
"id",
|
||||
"name",
|
||||
"alpha_type",
|
||||
"language",
|
||||
"stage",
|
||||
"status",
|
||||
"hidden",
|
||||
"region",
|
||||
"universe",
|
||||
"sharpe",
|
||||
"fitness",
|
||||
"returns",
|
||||
"turnover",
|
||||
"margin",
|
||||
"drawdown",
|
||||
"date_created",
|
||||
"date_submitted",
|
||||
"synced_at",
|
||||
)
|
||||
result = {k: getattr(item, k) for k in keys}
|
||||
result["expression_preview"] = (item.expression or item.selection or "")[:240]
|
||||
result["research"] = {
|
||||
k: getattr(research, k) for k in ("note", "tags", "favorite", "state", "updated_at")
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def pnl_points(raw):
|
||||
"""Use schema column names, preserving missing values rather than creating zero PnL."""
|
||||
records = raw.get("records")
|
||||
schema = raw.get("schema") or {}
|
||||
properties = schema.get("properties", []) if isinstance(schema, dict) else schema
|
||||
if isinstance(properties, dict):
|
||||
names = list(properties)
|
||||
else:
|
||||
names = [p.get("name", "") if isinstance(p, dict) else str(p) for p in properties]
|
||||
normalized = [name.lower() for name in names]
|
||||
if not isinstance(records, list):
|
||||
raise ValueError("PnL 缺少 records")
|
||||
points = []
|
||||
for row in records:
|
||||
if isinstance(row, dict):
|
||||
row = {str(k).lower(): v for k, v in row.items()}
|
||||
timestamp = next((row[k] for k in ("date", "datetime", "timestamp") if k in row), None)
|
||||
value = next((row[k] for k in ("pnl", "value") if k in row), None)
|
||||
else:
|
||||
date_i = next(
|
||||
(i for i, n in enumerate(normalized) if n in ("date", "datetime", "timestamp")), None
|
||||
)
|
||||
pnl_i = next((i for i, n in enumerate(normalized) if n in ("pnl", "value")), None)
|
||||
if date_i is None or pnl_i is None or not isinstance(row, list) or len(row) <= max(date_i, pnl_i):
|
||||
raise ValueError("PnL schema 无法识别日期或数值列")
|
||||
timestamp, value = row[date_i], row[pnl_i]
|
||||
if timestamp is not None:
|
||||
if isinstance(timestamp, (int, float)):
|
||||
from datetime import timezone
|
||||
|
||||
timestamp = datetime.fromtimestamp(
|
||||
timestamp / 1000 if timestamp > 1e11 else timestamp, tz=timezone.utc
|
||||
).isoformat()
|
||||
points.append({"date": str(timestamp), "value": number(value)})
|
||||
return sorted(points, key=lambda p: p["date"])
|
||||
Reference in New Issue
Block a user