feat: Implement Alpha list and metrics enhancements
Deploy production / deploy (push) Successful in 1m12s
Deploy production / deploy (push) Successful in 1m12s
- Added new metrics fields: sub_universe_sharpe, robust_universe_sharpe, two_year_sharpe, prod_correlation, pnl, check_type, and neutralization to the Alpha model. - Updated snapshot_columns function to derive new metrics and check types from platform snapshots. - Enhanced API to include failed checks and check types in responses. - Created migration script to backfill existing Alpha records with new metrics and check types. - Updated frontend components to display new metrics and allow editing of custom tags. - Improved filtering and sorting capabilities for new metrics in the Alpha list. - Added tests for new functionality including checks classification and metrics filtering.
This commit is contained in:
+61
-2
@@ -9,6 +9,55 @@ from sqlalchemy import or_, select, update
|
||||
from .models import Alpha, Research, ResearchTag, SelfCorrelation, now
|
||||
from .research.provenance import source_alpha_ids
|
||||
|
||||
METRIC_FIELDS = (
|
||||
"sharpe", "fitness", "returns", "turnover", "margin", "drawdown",
|
||||
"sub_universe_sharpe", "robust_universe_sharpe", "two_year_sharpe", "prod_correlation", "pnl",
|
||||
)
|
||||
|
||||
|
||||
def failed_checks(checks):
|
||||
"""Return failed platform check names; local correlation never changes this list."""
|
||||
return [
|
||||
check.get("name") if isinstance(check.get("name"), str) else "未命名检查"
|
||||
for check in checks if isinstance(check, dict) and check.get("result") == "FAIL"
|
||||
] if isinstance(checks, list) else []
|
||||
|
||||
|
||||
def snapshot_columns(settings, metrics, checks):
|
||||
"""Derive list fields from a platform snapshot, preserving missing metrics as null.
|
||||
|
||||
Only explicit FAIL results count. Empty, malformed and unfinished checks are
|
||||
pending; all known checks passing without PROD_CORRELATION is only a pre-check.
|
||||
No submission eligibility or activity eligibility is inferred here.
|
||||
"""
|
||||
settings = settings if isinstance(settings, dict) else {}
|
||||
metrics = metrics if isinstance(metrics, dict) else {}
|
||||
checks = checks if isinstance(checks, list) else []
|
||||
valid = [check for check in checks if isinstance(check, dict)]
|
||||
failures = len(failed_checks(checks))
|
||||
by_name = {check["name"]: check for check in valid if isinstance(check.get("name"), str)}
|
||||
if failures:
|
||||
check_type = "FAIL_1" if failures == 1 else "FAIL_2"
|
||||
elif not checks or len(valid) != len(checks) or any(check.get("result") != "PASS" for check in valid):
|
||||
check_type = "PENDING"
|
||||
else:
|
||||
check_type = "PASS" if "PROD_CORRELATION" in by_name else "PRE_CHECK"
|
||||
neutralization = settings.get("neutralization")
|
||||
return {
|
||||
"check_type": check_type,
|
||||
"neutralization": neutralization if isinstance(neutralization, str) else None,
|
||||
"pnl": number(metrics.get("pnl")),
|
||||
**{
|
||||
field: number(by_name.get(name, {}).get("value"))
|
||||
for field, name in (
|
||||
("sub_universe_sharpe", "LOW_SUB_UNIVERSE_SHARPE"),
|
||||
("robust_universe_sharpe", "LOW_ROBUST_UNIVERSE_SHARPE"),
|
||||
("two_year_sharpe", "LOW_2Y_SHARPE"),
|
||||
("prod_correlation", "PROD_CORRELATION"),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def submission_condition(submission):
|
||||
"""Match the platform list contract; a missing status is never assumed submitted."""
|
||||
@@ -106,6 +155,8 @@ async def upsert_alpha(db, raw: dict):
|
||||
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, value in snapshot_columns(item.settings, item.is_metrics, item.checks).items():
|
||||
setattr(item, key, value)
|
||||
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"))
|
||||
@@ -134,7 +185,7 @@ def list_statement(filters):
|
||||
)
|
||||
)
|
||||
)
|
||||
for name in ("region", "universe", "alpha_type", "language", "status", "stage", "hidden"):
|
||||
for name in ("region", "universe", "alpha_type", "language", "status", "stage", "hidden", "check_type", "neutralization"):
|
||||
value = getattr(filters, name)
|
||||
if value is not None:
|
||||
query = query.where(getattr(Alpha, name) == value)
|
||||
@@ -148,7 +199,7 @@ def list_statement(filters):
|
||||
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 name in METRIC_FIELDS:
|
||||
for suffix, compare in (("min", "ge"), ("max", "le")):
|
||||
value = getattr(filters, f"{name}_{suffix}")
|
||||
if value is not None:
|
||||
@@ -183,8 +234,16 @@ def summary(item: Alpha, research: Research):
|
||||
"date_created",
|
||||
"date_submitted",
|
||||
"synced_at",
|
||||
"check_type",
|
||||
"neutralization",
|
||||
"sub_universe_sharpe",
|
||||
"robust_universe_sharpe",
|
||||
"two_year_sharpe",
|
||||
"prod_correlation",
|
||||
"pnl",
|
||||
)
|
||||
result = {k: getattr(item, k) for k in keys}
|
||||
result["failed_checks"] = failed_checks(item.checks)
|
||||
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", "version")
|
||||
|
||||
+10
-3
@@ -16,7 +16,7 @@ from sqlalchemy import delete, select, text
|
||||
|
||||
from .ai.routes import router as ai_router
|
||||
from .ai.runtime import AIRuntime
|
||||
from .alphas import list_statement, sorted_statement
|
||||
from .alphas import failed_checks, list_statement, sorted_statement
|
||||
from .backtests.routes import router as backtest_router
|
||||
from .business import Business, notify_job
|
||||
from .catalog.research_routes import router as research_catalog_router
|
||||
@@ -375,11 +375,18 @@ def create_app(settings=None, wq_client=None, ai_model_factory=None):
|
||||
"turnover",
|
||||
"margin",
|
||||
"drawdown",
|
||||
"sub_universe_sharpe",
|
||||
"robust_universe_sharpe",
|
||||
"two_year_sharpe",
|
||||
"prod_correlation",
|
||||
"pnl",
|
||||
"neutralization",
|
||||
"check_type",
|
||||
"date_created",
|
||||
"date_submitted",
|
||||
"synced_at",
|
||||
]
|
||||
writer.writerow(columns + ["research_state", "favorite", "tags", "note"])
|
||||
writer.writerow(columns + ["failed_checks", "research_state", "favorite", "tags", "note"])
|
||||
yield buffer.getvalue()
|
||||
buffer.seek(0)
|
||||
buffer.truncate(0)
|
||||
@@ -388,7 +395,7 @@ def create_app(settings=None, wq_client=None, ai_model_factory=None):
|
||||
async for a, r in rows:
|
||||
writer.writerow(
|
||||
[csv_cell(getattr(a, key)) for key in columns]
|
||||
+ [r.state, r.favorite, csv_cell(";".join(r.tags)), csv_cell(r.note)]
|
||||
+ [csv_cell(";".join(failed_checks(a.checks))), r.state, r.favorite, csv_cell(";".join(r.tags)), csv_cell(r.note)]
|
||||
)
|
||||
yield buffer.getvalue()
|
||||
buffer.seek(0)
|
||||
|
||||
@@ -79,6 +79,13 @@ class Alpha(Base):
|
||||
turnover: Mapped[float | None] = mapped_column(Float)
|
||||
margin: Mapped[float | None] = mapped_column(Float)
|
||||
drawdown: Mapped[float | None] = mapped_column(Float)
|
||||
sub_universe_sharpe: Mapped[float | None] = mapped_column(Float)
|
||||
robust_universe_sharpe: Mapped[float | None] = mapped_column(Float)
|
||||
two_year_sharpe: Mapped[float | None] = mapped_column(Float)
|
||||
prod_correlation: Mapped[float | None] = mapped_column(Float)
|
||||
pnl: Mapped[float | None] = mapped_column(Float)
|
||||
neutralization: Mapped[str | None] = mapped_column(Text)
|
||||
check_type: Mapped[str] = mapped_column(String(20), default="PENDING", server_default="PENDING", index=True)
|
||||
date_created: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), index=True)
|
||||
date_submitted: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
synced_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now)
|
||||
|
||||
+30
-1
@@ -9,6 +9,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_valida
|
||||
|
||||
ResearchState = Literal["inbox", "candidate", "optimizing", "archived"]
|
||||
Submission = Literal["UNSUBMITTED", "SUBMITTED"]
|
||||
CheckType = Literal["PENDING", "PRE_CHECK", "PASS", "FAIL_1", "FAIL_2"]
|
||||
SortField = Literal[
|
||||
"id",
|
||||
"name",
|
||||
@@ -18,6 +19,11 @@ SortField = Literal[
|
||||
"turnover",
|
||||
"margin",
|
||||
"drawdown",
|
||||
"sub_universe_sharpe",
|
||||
"robust_universe_sharpe",
|
||||
"two_year_sharpe",
|
||||
"prod_correlation",
|
||||
"pnl",
|
||||
"date_created",
|
||||
"date_submitted",
|
||||
"synced_at",
|
||||
@@ -76,6 +82,8 @@ class AlphaFilters(Contract):
|
||||
status: str | None = None
|
||||
stage: str | None = None
|
||||
hidden: bool | None = None
|
||||
check_type: CheckType | None = None
|
||||
neutralization: str | None = None
|
||||
research_state: ResearchState | None = None
|
||||
favorite: bool | None = None
|
||||
tag: str | None = Field(default=None, max_length=60)
|
||||
@@ -93,6 +101,16 @@ class AlphaFilters(Contract):
|
||||
margin_max: float | None = None
|
||||
drawdown_min: float | None = None
|
||||
drawdown_max: float | None = None
|
||||
sub_universe_sharpe_min: float | None = None
|
||||
sub_universe_sharpe_max: float | None = None
|
||||
robust_universe_sharpe_min: float | None = None
|
||||
robust_universe_sharpe_max: float | None = None
|
||||
two_year_sharpe_min: float | None = None
|
||||
two_year_sharpe_max: float | None = None
|
||||
prod_correlation_min: float | None = None
|
||||
prod_correlation_max: float | None = None
|
||||
pnl_min: float | None = None
|
||||
pnl_max: float | None = None
|
||||
sort: SortField = "date_created"
|
||||
direction: Literal["asc", "desc"] = "desc"
|
||||
limit: int = Field(default=25, ge=1, le=100)
|
||||
@@ -105,7 +123,10 @@ class AlphaFilters(Contract):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def range_order(self):
|
||||
for key in ("sharpe", "fitness", "returns", "turnover", "margin", "drawdown"):
|
||||
for key in (
|
||||
"sharpe", "fitness", "returns", "turnover", "margin", "drawdown",
|
||||
"sub_universe_sharpe", "robust_universe_sharpe", "two_year_sharpe", "prod_correlation", "pnl",
|
||||
):
|
||||
lo, hi = getattr(self, f"{key}_min"), getattr(self, f"{key}_max")
|
||||
if lo is not None and hi is not None and lo > hi:
|
||||
raise ValueError(f"{key} 最小值不能大于最大值")
|
||||
@@ -222,6 +243,14 @@ class AlphaSummary(BaseModel):
|
||||
date_created: datetime | None
|
||||
date_submitted: datetime | None
|
||||
synced_at: datetime
|
||||
check_type: CheckType = "PENDING"
|
||||
failed_checks: list[str] = Field(default_factory=list)
|
||||
neutralization: str | None = None
|
||||
sub_universe_sharpe: float | None = None
|
||||
robust_universe_sharpe: float | None = None
|
||||
two_year_sharpe: float | None = None
|
||||
prod_correlation: float | None = None
|
||||
pnl: float | None = None
|
||||
research: ResearchOutput
|
||||
local_correlation: dict | None = None
|
||||
source_kinds: list[str] = Field(default_factory=list)
|
||||
|
||||
Reference in New Issue
Block a user