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:
@@ -5,7 +5,7 @@ from typing import Annotated, Literal
|
||||
|
||||
from pydantic import Field, model_validator
|
||||
|
||||
from ..backtests.contracts import Candidate, SimulationSettings
|
||||
from ..backtests.contracts import Candidate, SimulationSettings, SuperSimulationSettings
|
||||
from ..catalog.contracts import CatalogFilters, Scope
|
||||
from ..preparations.contracts import PreparationReference
|
||||
from ..research.workspace_contracts import TemplateSpec
|
||||
@@ -50,11 +50,26 @@ class CompleteSettings(SimulationSettings):
|
||||
model_config = {"json_schema_extra": {"required": list(SimulationSettings.model_fields)}}
|
||||
|
||||
|
||||
class CompleteSuperSettings(SuperSimulationSettings):
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def complete(cls, value):
|
||||
if isinstance(value, dict) and set(cls.model_fields) - value.keys():
|
||||
raise ValueError("必须提供每项完整 SUPER 设置;先读取 get_research_capabilities")
|
||||
return value
|
||||
|
||||
model_config = {"json_schema_extra": {"required": list(SuperSimulationSettings.model_fields)}}
|
||||
|
||||
|
||||
class DirectCandidate(Candidate):
|
||||
settings: CompleteSettings
|
||||
settings: CompleteSuperSettings | CompleteSettings
|
||||
|
||||
|
||||
class Provenance(Contract):
|
||||
research_id: RunId | None = None
|
||||
superalpha_plan_id: RunId | None = None
|
||||
superalpha_plan_version: int | None = Field(default=None, ge=1)
|
||||
selection_snapshot_ids: list[RunId] = Field(default_factory=list, max_length=100)
|
||||
reference: str | None = Field(default=None, max_length=200)
|
||||
batch_id: str | None = Field(default=None, max_length=200)
|
||||
hypothesis: str | None = Field(default=None, max_length=2000)
|
||||
@@ -129,12 +144,18 @@ class Scopes(Contract):
|
||||
|
||||
class SettingOptions(Page):
|
||||
kind: Literal["settings"]
|
||||
alpha_type: Literal["REGULAR", "SUPER"] = "REGULAR"
|
||||
|
||||
|
||||
class Operators(Page):
|
||||
kind: Literal["operators"]
|
||||
q: str = Field(default="", max_length=300)
|
||||
category: str | None = None
|
||||
stage: Literal["REGULAR", "SELECTION", "COMBO"] | None = None
|
||||
|
||||
|
||||
class SuperMetadata(Contract):
|
||||
kind: Literal["superalpha"]
|
||||
|
||||
|
||||
class Availability(Contract):
|
||||
@@ -144,7 +165,7 @@ class Availability(Contract):
|
||||
|
||||
|
||||
class Metadata(Contract):
|
||||
query: Annotated[Scopes | SettingOptions | Operators | Availability, Field(discriminator="kind")]
|
||||
query: Annotated[Scopes | SettingOptions | Operators | Availability | SuperMetadata, Field(discriminator="kind")]
|
||||
|
||||
|
||||
class CatalogRefresh(Contract):
|
||||
@@ -192,6 +213,8 @@ class SubmissionCheck(Contract):
|
||||
|
||||
|
||||
class History(Page):
|
||||
research_id: str | None = Field(default=None, max_length=36)
|
||||
alpha_type: Literal["REGULAR", "SUPER"] | None = None
|
||||
source: str | None = Field(default=None, max_length=100)
|
||||
reference: str | None = Field(default=None, max_length=200)
|
||||
status: str | None = Field(default=None, max_length=30)
|
||||
@@ -224,13 +247,13 @@ class Results(Page):
|
||||
|
||||
class Artifact(Page):
|
||||
item_id: RunId
|
||||
kind: Literal["snapshot", "pnl"]
|
||||
kind: Literal["snapshot", "pnl", "components"]
|
||||
date_from: date | None = None
|
||||
date_to: date | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def dates(self):
|
||||
if self.kind == "snapshot" and (self.date_from or self.date_to):
|
||||
if self.kind != "pnl" and (self.date_from or self.date_to):
|
||||
raise ValueError("日期筛选仅用于 PnL")
|
||||
if self.date_from and self.date_to and self.date_from > self.date_to:
|
||||
raise ValueError("起始日期不能晚于结束日期")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, or_, select
|
||||
|
||||
from ..alphas import number, sanitize
|
||||
from ..backtests.contracts import fingerprint
|
||||
@@ -54,7 +54,7 @@ def item_summary(item, result):
|
||||
("sharpe", "fitness", "returns", "turnover", "margin", "drawdown")}
|
||||
return encode_snapshot({
|
||||
**{k: getattr(item, k) for k in (
|
||||
"id", "run_id", "client_item_id", "expression", "settings", "attempt_id",
|
||||
"id", "run_id", "client_item_id", "expression", "selection", "combo", "alpha_type", "settings", "attempt_id",
|
||||
"platform_status", "collection_status", "persistence_status", "simulation_id", "alpha_id",
|
||||
)},
|
||||
"error": sanitize(item.error), "metrics": metrics,
|
||||
@@ -73,10 +73,12 @@ class EvidenceQueries:
|
||||
query = select(BacktestItem, BacktestResult, BacktestRun).join(
|
||||
BacktestRun, BacktestRun.id == BacktestItem.run_id
|
||||
).outerjoin(BacktestResult, BacktestResult.item_id == BacktestItem.id)
|
||||
for key in ("source", "reference"):
|
||||
for key in ("source", "reference", "research_id"):
|
||||
value = getattr(args, key)
|
||||
if value is not None:
|
||||
query = query.where(BacktestRun.source["kind" if key == "source" else key].as_string() == value)
|
||||
if args.alpha_type:
|
||||
query = query.where(BacktestItem.alpha_type == args.alpha_type)
|
||||
if args.status:
|
||||
query = query.where(BacktestRun.status == args.status)
|
||||
if args.created_from:
|
||||
@@ -89,7 +91,7 @@ class EvidenceQueries:
|
||||
query = query.where(BacktestItem.settings["delay"].as_integer() == args.scope.delay)
|
||||
if args.q:
|
||||
escaped = args.q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
query = query.where(BacktestItem.expression.ilike(f"%{escaped}%", escape="\\"))
|
||||
query = query.where(or_(*(getattr(BacktestItem, key).ilike(f"%{escaped}%", escape="\\") for key in ("expression", "selection", "combo"))))
|
||||
matches = {}
|
||||
if args.candidates:
|
||||
for c in args.candidates:
|
||||
@@ -120,6 +122,9 @@ class EvidenceQueries:
|
||||
if not item:
|
||||
raise ResearchError("NOT_FOUND", "候选不存在")
|
||||
result = await self.db.get(BacktestResult, item.id)
|
||||
if args.kind == "components":
|
||||
from ..superalpha.evidence import actual_components
|
||||
return await actual_components(self.db, item.id, args.limit, args.offset)
|
||||
if args.kind == "snapshot":
|
||||
# Top-level entries retain complete nested values; no hidden string/list truncation.
|
||||
entries = [{"key": k, "value": v} for k, v in sanitize(result.snapshot).items()] if result else []
|
||||
|
||||
@@ -25,6 +25,7 @@ from ..research.workspace_contracts import FieldAvailabilityInput
|
||||
from ..schemas import JobInput
|
||||
from ..submission import CheckInput, correlation_allows_check, create_check_job, local_alpha, source
|
||||
from ..submission import fingerprint as submission_fingerprint
|
||||
from ..superalpha.access import SuperResearchAccess
|
||||
from ..worldquant import WqError
|
||||
from .contracts import DirectCandidate, History
|
||||
from .queries import EvidenceQueries, page
|
||||
@@ -37,7 +38,7 @@ class ResearchError(Exception):
|
||||
"retry_after": retry_after, "affected_items": affected_items or []}
|
||||
|
||||
|
||||
class ResearchAccess:
|
||||
class ResearchAccess(SuperResearchAccess):
|
||||
def __init__(self, db, principal, client, public_origin):
|
||||
self.db, self.principal, self.client = db, principal, client
|
||||
self.public_origin = public_origin.rstrip("/")
|
||||
@@ -71,6 +72,10 @@ class ResearchAccess:
|
||||
async def capabilities(self, args):
|
||||
return {**await self.backtests.capabilities(), "max_candidates": 100,
|
||||
"settings_schema": DirectCandidate.model_json_schema(),
|
||||
"superalpha": {"plan_with": "save_superalpha_plan", "build_with": "build_superalpha_candidates",
|
||||
"preview_with": "preview_superalpha_selection", "read_selection_with": "get_superalpha_selection",
|
||||
"job_with": "get_refresh_job", "backtest_with": "submit_backtests", "platform_batch_size": 1,
|
||||
"component_evidence": "预览与实际组件分别记录,未知不能认定为同池"},
|
||||
"confirmation": "调用者须已获本批执行授权;直接提交后返回稳定运行 ID",
|
||||
"duplicate_policies": ["reject", "rerun"], "permissions": sorted(self.principal.scopes),
|
||||
"metadata_only": True, "actual_platform_allowance": None,
|
||||
@@ -159,15 +164,23 @@ class ResearchAccess:
|
||||
async def metadata(self, args):
|
||||
q = args.query
|
||||
metadata = ResearchMetadata(self.db)
|
||||
if q.kind == "superalpha":
|
||||
from ..superalpha.metadata import metadata as super_metadata
|
||||
return await super_metadata(self.db)
|
||||
if q.kind == "scopes":
|
||||
return {"source": "worldquant_platform", **await platform_options(self.client)}
|
||||
if q.kind == "operators":
|
||||
data = await metadata.operators(q.q, q.category, limit=q.limit, offset=q.offset)
|
||||
data = await metadata.operators(q.q, q.category, limit=q.limit, offset=q.offset, stage=q.stage)
|
||||
return {**data, "status": "available" if data["fetched_at"] else "not_cached",
|
||||
"has_more": q.offset + len(data["items"]) < data["total"]}
|
||||
if q.kind == "settings":
|
||||
data = await metadata.get("settings")
|
||||
items = data["content"].get("items", [])
|
||||
special = {"selectionHandling", "selectionLimit", "componentActivation"}
|
||||
if q.alpha_type == "REGULAR":
|
||||
items = [{**r, "fields": {k: v for k, v in r.get("fields", {}).items() if k not in special}} for r in items]
|
||||
else:
|
||||
items = [{**r, "super_settings_completeness": "cached" if special <= r.get("fields", {}).keys() else "unknown"} for r in items]
|
||||
return {"status": "available" if data["fetched_at"] else "not_cached",
|
||||
"fetched_at": data["fetched_at"], **page(items[q.offset:q.offset+q.limit], len(items), q.limit, q.offset)}
|
||||
data = await metadata.get(availability_key(q.field_id, q.scope))
|
||||
@@ -201,14 +214,16 @@ class ResearchAccess:
|
||||
|
||||
async def refresh_job(self, args):
|
||||
job = await self.db.get(Job, args.job_id)
|
||||
if not job or job.kind not in {"catalog_sync", "field_sync", "pnl_refresh", "self_correlation", "submission_check"}:
|
||||
if not job or job.kind not in {"catalog_sync", "field_sync", "pnl_refresh", "self_correlation", "submission_check", "super_selection_preview"}:
|
||||
raise ResearchError("NOT_FOUND", "研究刷新任务不存在")
|
||||
result = await self.business.get_job_status(args.job_id)
|
||||
query = select(JobItem).where(JobItem.job_id == job.id, JobItem.error.is_not(None))
|
||||
total = await self.db.scalar(select(func.count()).select_from(query.subquery()))
|
||||
errors = list(await self.db.scalars(query.order_by(JobItem.alpha_id).limit(args.limit).offset(args.offset)))
|
||||
result.pop("errors", None)
|
||||
return {**result, "job_id": job.id, "artifact_reference": job.payload,
|
||||
artifact = ({"job_id": job.id, "snapshot_id": job.checkpoint.get("snapshot_id"), "read_with": "get_superalpha_selection"}
|
||||
if job.kind == "super_selection_preview" else job.payload)
|
||||
return {**result, "job_id": job.id, "artifact_reference": artifact,
|
||||
"errors": page([{"alpha_id": e.alpha_id, "error": e.error} for e in errors], total, args.limit, args.offset)}
|
||||
|
||||
async def check_self_correlation(self, args):
|
||||
@@ -306,7 +321,11 @@ class ResearchAccess:
|
||||
invalid.append(c.client_item_id)
|
||||
if invalid:
|
||||
raise ResearchError("UNSUPPORTED_SETTINGS", "已缓存平台设置不支持这些组合;可显式刷新后重试", affected_items=invalid)
|
||||
return {"settings_validation": "cached", "fetched_at": snapshot["fetched_at"], "field_validation": "unknown"}
|
||||
result = {"settings_validation": "cached", "fetched_at": snapshot["fetched_at"], "field_validation": "unknown"}
|
||||
if any(c.alpha_type == "SUPER" for c in candidates):
|
||||
from ..superalpha.settings import validate_settings
|
||||
result["super_settings_validation"] = await validate_settings(self.db, [c.settings for c in candidates if c.alpha_type == "SUPER"])
|
||||
return result
|
||||
|
||||
async def submit(self, args):
|
||||
previous, digest = await self.previous("submit_backtests", args)
|
||||
|
||||
Reference in New Issue
Block a user