feat: display five GLB PnL series with distinct colors
Deploy production / deploy (push) Successful in 56s

This commit is contained in:
yuxuanhui
2026-09-10 14:09:41 +08:00
parent f8e23dccb9
commit 6ce33eb572
10 changed files with 241 additions and 56 deletions
+3
View File
@@ -26,10 +26,13 @@ async def search(ctx, args):
async def pnl(ctx, args):
data = await ctx.business.get_alpha_pnl(args.alpha_id)
points = data.pop("points")
# Chart data is fetched by the UI; keep large series out of model context.
series = data.pop("series", [])
return {
**data,
"alpha_id": args.alpha_id,
"count": len(points),
"series": [{"id": item["id"], "label": item["label"]} for item in series],
"first": points[0] if points else None,
"last": points[-1] if points else None,
"null_count": sum(p["value"] is None for p in points),
+35 -5
View File
@@ -251,7 +251,7 @@ def summary(item: Alpha, research: Research):
return result
def pnl_points(raw):
def pnl_points(raw, column=None):
"""Use schema column names, preserving missing values rather than creating zero PnL."""
records = raw.get("records")
schema = raw.get("schema") or {}
@@ -261,6 +261,7 @@ def pnl_points(raw):
else:
names = [p.get("name", "") if isinstance(p, dict) else str(p) for p in properties]
normalized = [name.lower() for name in names]
value_names = (column,) if column else ("pnl", "value")
if not isinstance(records, list):
raise ValueError("PnL 缺少 records")
points = []
@@ -268,15 +269,17 @@ def pnl_points(raw):
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)
value = next((row[k] for k in value_names 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):
pnl_i = next((i for i, n in enumerate(normalized) if n in value_names), None)
if date_i is None or pnl_i is None or not isinstance(row, list) or len(row) <= date_i:
raise ValueError("PnL schema 无法识别日期或数值列")
timestamp, value = row[date_i], row[pnl_i]
if len(row) <= pnl_i and column is None:
raise ValueError("PnL schema 无法识别日期或数值列")
timestamp, value = row[date_i], row[pnl_i] if len(row) > pnl_i else None
if timestamp is not None:
if isinstance(timestamp, (int, float)):
from datetime import timezone
@@ -286,3 +289,30 @@ def pnl_points(raw):
).isoformat()
points.append({"date": str(timestamp), "value": number(value)})
return sorted(points, key=lambda p: p["date"])
def glb_pnl_series(raw, points):
"""Read GLB display series from cached raw data; keep the correlation baseline intact.
Missing columns are omitted, while missing values remain gaps. Legacy caches
containing only normalized points still return their overall PnL.
"""
series = [{"id": "pnl", "label": "总体 PnL", "points": points}]
schema = raw.get("schema") or {}
properties = schema.get("properties", []) if isinstance(schema, dict) else schema
names = properties if isinstance(properties, dict) else [
p.get("name", "") if isinstance(p, dict) else str(p) for p in properties
]
available = {name.lower() for name in names}
for row in raw.get("records", []):
if isinstance(row, dict):
available.update(str(key).lower() for key in row)
for column, label in (
("investability-constrained-pnl", "可投资性约束 PnL"),
("amer-pnl", "AMER PnL"),
("apac-pnl", "APAC PnL"),
("emea-pnl", "EMEA PnL"),
):
if column in available:
series.append({"id": column, "label": label, "points": pnl_points(raw, column)})
return series
+4 -2
View File
@@ -10,7 +10,7 @@ from uuid import uuid4
from fastapi import HTTPException
from sqlalchemy import delete, func, select, update
from .alphas import list_statement, sorted_statement, submission_condition, summary
from .alphas import glb_pnl_series, list_statement, sorted_statement, submission_condition, summary
from .jobs import ACTIVE
from .models import Account, Alpha, Job, JobItem, Pnl, Research, ResearchTag, SelfCorrelation, now
from .research.provenance import alpha_sources, source_kinds
@@ -140,12 +140,14 @@ class Business:
).model_dump(mode="json")
async def get_alpha_pnl(self, alpha_id):
if not await self.db.get(Alpha, alpha_id):
alpha = await self.db.get(Alpha, alpha_id)
if not alpha:
raise HTTPException(404, "Alpha 尚未同步")
row = await self.db.get(Pnl, alpha_id)
return {
"cached": row is not None,
"points": row.points if row else [],
"series": glb_pnl_series(row.raw, row.points) if row and alpha.region == "GLB" else [],
"fetched_at": row.fetched_at.isoformat() if row else None,
}
+7
View File
@@ -358,9 +358,16 @@ class PnlPoint(BaseModel):
value: float | None
class PnlSeries(BaseModel):
id: str
label: str
points: list[PnlPoint]
class PnlOutput(BaseModel):
cached: bool
points: list[PnlPoint]
series: list[PnlSeries] = Field(default_factory=list)
fetched_at: datetime | None