feat: add Super Alpha research, management and MCP workflows
Deploy production / deploy (push) Successful in 56s
Deploy production / deploy (push) Successful in 56s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user