feat: add Super Alpha research, management and MCP workflows
Deploy production / deploy (push) Successful in 56s

This commit is contained in:
yuxuanhui
2026-09-13 12:32:16 +08:00
parent 7c8188df9c
commit e256d6fef1
59 changed files with 3766 additions and 125 deletions
+38 -10
View File
@@ -27,25 +27,47 @@ class SimulationSettings(Contract):
maxPosition: Literal["ON", "OFF"] = "OFF"
class SuperSimulationSettings(SimulationSettings):
"""SUPER-only selection settings; platform metadata still determines availability."""
selectionHandling: Literal["POSITIVE", "NON_ZERO", "NON_NAN"]
selectionLimit: int = Field(ge=1, le=100000, strict=True)
componentActivation: Literal["IS", "OS"]
class Candidate(Contract):
client_item_id: str = Field(min_length=1, max_length=100)
expression: str = Field(min_length=1, max_length=20000)
settings: SimulationSettings
alpha_type: Literal["REGULAR"] = "REGULAR"
expression: str = Field(default="", max_length=20000)
selection: str | None = Field(default=None, max_length=20000)
combo: str | None = Field(default=None, max_length=20000)
settings: SuperSimulationSettings | SimulationSettings
alpha_type: Literal["REGULAR", "SUPER"] = "REGULAR"
@field_validator("expression")
@field_validator("expression", "selection", "combo")
@classmethod
def nonempty(cls, value):
value = value.strip()
if not value:
raise ValueError("表达式不能为空")
return value
return value.strip() if value is not None else None
@model_validator(mode="after")
def typed_input(self):
if self.alpha_type == "SUPER":
if self.expression or not self.selection or not self.combo:
raise ValueError("SUPER 必须提供非空 selection/combo,不能提供 regular expression")
if not isinstance(self.settings, SuperSimulationSettings):
raise ValueError("SUPER 必须提供 selectionHandling、selectionLimit、componentActivation")
elif not self.expression or self.selection is not None or self.combo is not None or isinstance(self.settings, SuperSimulationSettings):
raise ValueError("REGULAR 必须提供非空 expression,不能包含 SUPER 表达式或设置")
return self
def platform_input(self):
if self.alpha_type == "SUPER":
return {"type": "SUPER", "selection": self.selection, "combo": self.combo,
"settings": self.settings.model_dump()}
return {"type": self.alpha_type, "regular": self.expression, "settings": self.settings.model_dump()}
class Source(Contract):
research_kind: str | None = Field(default=None, max_length=50)
kind: str = Field(default="manual", min_length=1, max_length=100)
reference: str | None = Field(default=None, max_length=200)
batch_id: str | None = Field(default=None, max_length=200)
@@ -54,6 +76,9 @@ class Source(Contract):
research_id: str | None = Field(default=None, max_length=200)
parent_run_id: str | None = Field(default=None, max_length=36)
hypothesis: str | None = Field(default=None, max_length=2000)
superalpha_plan_id: str | None = Field(default=None, max_length=36)
superalpha_plan_version: int | None = Field(default=None, ge=1)
selection_snapshot_ids: list[str] = Field(default_factory=list, max_length=100)
class SourceOutput(Source):
@@ -129,7 +154,7 @@ def fingerprint(payload: dict) -> str:
def group_key(candidate: dict):
settings = candidate["settings"]
return tuple(settings[k] for k in ("region", "delay", "language", "instrumentType"))
return (candidate.get("alpha_type", "REGULAR"), *tuple(settings[k] for k in ("region", "delay", "language", "instrumentType")))
class ReferenceInput(Contract):
@@ -203,7 +228,10 @@ class ItemOutput(Contract):
id: str
client_item_id: str
expression: str
settings: SimulationSettings
alpha_type: Literal["REGULAR", "SUPER"] = "REGULAR"
selection: str | None = None
combo: str | None = None
settings: SuperSimulationSettings | SimulationSettings
attempt_id: str
platform_status: str
collection_status: str
+16 -1
View File
@@ -117,13 +117,14 @@ async def runs(
source: str | None = Query(None, max_length=100),
reference: str | None = Query(None, max_length=200),
research_id: str | None = Query(None, max_length=200),
alpha_type: Literal["REGULAR", "SUPER"] | None = None,
q: str = Query("", max_length=200),
sort: Literal["name", "created_at"] = "created_at",
direction: Literal["asc", "desc"] = "desc",
):
async with request.app.state.sessions() as db:
return await Business(db).backtests.runs(
limit, offset, source, reference, research_id, q, sort, direction
limit, offset, source, reference, research_id, q, sort, direction, alpha_type
)
@@ -195,3 +196,17 @@ async def attach_reference(attempt_id: str, body: ReferenceInput, request: Reque
async def subset(preview_id: str, body: SubsetInput, request: Request):
async with request.app.state.sessions.begin() as db:
return await Business(db).backtests.subset(preview_id, body)
@router.get("/items/{item_id}/artifact")
async def artifact(item_id: str, request: Request, kind: Literal["snapshot", "components", "pnl"], limit: int = Query(25, ge=1, le=100), offset: int = Query(0, ge=0)):
from fastapi import HTTPException
from ..research_access.contracts import Artifact
from ..research_access.queries import EvidenceQueries
from ..research_access.service import ResearchError
async with request.app.state.sessions() as db:
try:
return await EvidenceQueries(db).artifact(Artifact(item_id=item_id, kind=kind, limit=limit, offset=offset))
except ResearchError as exc:
raise HTTPException(404, str(exc)) from None
+11 -3
View File
@@ -448,7 +448,7 @@ class BacktestLane:
def safe_progress(self, value):
# Store useful protocol evidence, never arbitrary upstream diagnostics or credentials.
result = {k: value[k] for k in ("status", "alpha", "regular", "settings", "location") if k in value}
result = {k: value[k] for k in ("status", "type", "alpha", "regular", "selection", "combo", "settings", "location", "warnings") if k in value}
message = value.get("error") or value.get("message")
if isinstance(message, str):
for secret in list(self.client.credentials or ()) + list(self.client.client.cookies.values()):
@@ -485,11 +485,13 @@ class BacktestLane:
matched = [
i
for i in items
if i.expression == expression
if ((i.alpha_type == "REGULAR" and evidence.get("type", "REGULAR") == "REGULAR" and i.expression == expression)
or (i.alpha_type == "SUPER" and evidence.get("type") == "SUPER"
and i.selection == code(evidence.get("selection")) and i.combo == code(evidence.get("combo"))))
and isinstance(settings, dict)
and all(k in settings and settings[k] == v for k, v in i.settings.items())
]
if count == 1:
if count == 1 and items[0].alpha_type == "REGULAR":
matched = (
items
if (expression == items[0].expression or (not expression and detail is None))
@@ -499,6 +501,9 @@ class BacktestLane:
)
else []
)
if count == 1 and items[0].alpha_type == "SUPER" and detail is None:
# A known receipt can record progress, but saving SUPER requires full type/input evidence.
matched = items if not any(k in evidence for k in ("type", "selection", "combo", "settings")) else matched
# Identical inputs within a multi-submit are intentionally not position-matched.
if len(matched) != 1 or (matched[0].simulation_id not in (None, child)):
return
@@ -513,6 +518,9 @@ class BacktestLane:
if not await db.get(BacktestResult, item.id):
from datetime import datetime
if item.alpha_type == "SUPER":
from ..superalpha.evidence import save_actual_components
await save_actual_components(db, item, detail, receipt["observed_at"])
db.add(
BacktestResult(
item_id=item.id,
+15 -4
View File
@@ -93,7 +93,7 @@ class Backtests:
async def capabilities(self):
return {
"alpha_types": ["REGULAR"],
"alpha_types": ["REGULAR", "SUPER"],
"languages": ["FASTEXPR"],
"instrument_types": ["EQUITY"],
"settings_schema": Candidate.model_json_schema(),
@@ -111,6 +111,8 @@ class Backtests:
from ..research.expressions import analyze
if not body.preparation_refs and not body.input_ids:
return
if any(c.alpha_type == "SUPER" for c in body.candidates):
raise HTTPException(422, "SUPER 组件快照不能使用字段数据准备集合")
await Preparations(self.db).bind(body)
snapshots = [await Preparations(self.db).snapshot(i) for i in body.input_ids]
for candidate in body.candidates:
@@ -221,6 +223,8 @@ class Backtests:
if len(candidates) != len(selection):
raise HTTPException(422, "选择包含不属于当前草稿的候选")
data = {"name": draft.name, "source": draft.source, "candidates": candidates}
from ..superalpha.service import validate_source
await validate_source(self.db, data["source"], data["candidates"])
candidates = DraftInput.model_validate(data).model_dump(mode="json")["candidates"]
config = await self.db.get(BacktestConfig, 1)
groups = defaultdict(list)
@@ -253,6 +257,9 @@ class Backtests:
for indices in groups.values():
local_batches = []
for index in indices:
if candidates[index]["alpha_type"] == "SUPER":
batches.append([index])
continue
batch = next(
(
b
@@ -352,6 +359,7 @@ class Backtests:
ordinal=i,
client_item_id=c.client_item_id,
expression=c.expression,
alpha_type=c.alpha_type, selection=c.selection, combo=c.combo,
settings=c.settings.model_dump(),
fingerprint=fingerprint(c.platform_input()),
)
@@ -360,8 +368,10 @@ class Backtests:
await self.db.flush()
return await self.run(run.id)
async def runs(self, limit=25, offset=0, source=None, reference=None, research_id=None, q="", sort="created_at", direction="desc"):
async def runs(self, limit=25, offset=0, source=None, reference=None, research_id=None, q="", sort="created_at", direction="desc", alpha_type=None):
query = select(BacktestRun)
if alpha_type:
query = query.where(BacktestRun.id.in_(select(BacktestItem.run_id).where(BacktestItem.alpha_type == alpha_type)))
if q:
query = query.where(BacktestRun.name.contains(q, autoescape=True))
column = {"name": BacktestRun.name, "created_at": BacktestRun.created_at}[sort]
@@ -460,7 +470,7 @@ class Backtests:
for k in (
"id",
"client_item_id",
"expression",
"expression", "alpha_type", "selection", "combo",
"settings",
"attempt_id",
"platform_status",
@@ -610,7 +620,8 @@ class Backtests:
source=Source.model_validate({**run.source, "parent_run_id": run.id}),
candidates=[
Candidate(
client_item_id=r.client_item_id, expression=r.expression, settings=r.settings
client_item_id=r.client_item_id, expression=r.expression, settings=r.settings,
alpha_type=r.alpha_type, selection=r.selection, combo=r.combo
)
for r in selected
],