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:
@@ -0,0 +1,125 @@
|
||||
"""Index Alpha checks and metrics; backfill existing snapshots without upstream calls."""
|
||||
|
||||
import math
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0011"
|
||||
down_revision = "0010"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
# Frozen normalization for historical snapshots; do not import mutable app code.
|
||||
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 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"),
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
METRICS = ("sub_universe_sharpe", "robust_universe_sharpe", "two_year_sharpe", "prod_correlation", "pnl")
|
||||
|
||||
|
||||
def upgrade():
|
||||
columns = [sa.Column(name, sa.Float(), nullable=True) for name in METRICS]
|
||||
columns += [
|
||||
sa.Column("neutralization", sa.Text(), nullable=True),
|
||||
sa.Column("check_type", sa.String(20), nullable=False, server_default="PENDING"),
|
||||
]
|
||||
for column in columns:
|
||||
op.add_column("alphas", column)
|
||||
op.create_index("ix_alphas_check_type", "alphas", ["check_type"])
|
||||
table = sa.table(
|
||||
"alphas",
|
||||
sa.column("id", sa.String()),
|
||||
sa.column("settings", sa.JSON()),
|
||||
sa.column("is_metrics", sa.JSON()),
|
||||
sa.column("checks", sa.JSON()),
|
||||
*(sa.column(column.name, column.type) for column in columns),
|
||||
)
|
||||
connection = op.get_bind()
|
||||
last_id = None
|
||||
while True:
|
||||
query = (
|
||||
sa.select(table.c.id, table.c.settings, table.c.is_metrics, table.c.checks)
|
||||
.order_by(table.c.id)
|
||||
.limit(500)
|
||||
)
|
||||
if last_id is not None:
|
||||
query = query.where(table.c.id > last_id)
|
||||
rows = connection.execute(query).mappings().all()
|
||||
if not rows:
|
||||
break
|
||||
connection.execute(
|
||||
table.update()
|
||||
.where(table.c.id == sa.bindparam("snapshot_id"))
|
||||
.values({column.name: sa.bindparam(column.name) for column in columns}),
|
||||
[
|
||||
{
|
||||
"snapshot_id": row["id"],
|
||||
**snapshot_columns(row["settings"], row["is_metrics"], row["checks"]),
|
||||
}
|
||||
for row in rows
|
||||
],
|
||||
)
|
||||
last_id = rows[-1]["id"]
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index("ix_alphas_check_type", table_name="alphas")
|
||||
for name in ("check_type", "neutralization", *reversed(METRICS)):
|
||||
op.drop_column("alphas", name)
|
||||
Reference in New Issue
Block a user