Refactor project components and workflows
Deploy production / deploy (push) Successful in 51s

This commit is contained in:
yuxuanhui
2026-09-20 11:20:51 +08:00
parent 13a2168ca5
commit 69c19ed25f
23 changed files with 1146 additions and 550 deletions
+72 -28
View File
@@ -7,7 +7,15 @@ from collections import defaultdict
from fastapi import HTTPException
from sqlalchemy import func, select, update
from ..backtests.contracts import Candidate, DraftInput, PreviewInput, SimulationSettings, Source, StartInput
from ..backtests.contracts import (
Candidate,
DraftInput,
PreviewInput,
SimulationSettings,
Source,
StartInput,
fingerprint,
)
from ..backtests.service import Backtests, uid
from ..catalog.research_metadata import ResearchMetadata
from ..catalog.service import Catalog
@@ -311,6 +319,26 @@ class Experiments:
}
)
async def template_candidates(self, experiment_id, limit=25, offset=0):
"""Read a bounded page of stored template candidates and their immutable references."""
experiment = await self.get(experiment_id)
if experiment["kind"] != "template":
raise HTTPException(422, "此入口仅用于模板候选集合")
candidates = experiment["candidates"]
template = experiment["evidence"].get("template", {})
return {
"id": experiment_id, "experiment_id": experiment_id,
"name": experiment["name"], "archived": experiment["archived"],
"template": {k: template.get(k) for k in ("id", "version", "name")},
"inputs": [{k: item.get(k) for k in ("id", "preparation_id", "preparation_version", "scope")}
for item in experiment["inputs"]],
"items": [{k: c[k] for k in ("client_item_id", "expression", "settings", "alpha_type", "bindings") if k in c}
for c in candidates[offset:offset + limit]],
"total": len(candidates), "limit": limit, "offset": offset,
"has_more": offset + limit < len(candidates),
"backtest_run_ids": experiment["backtest_run_ids"],
}
async def archive(self, experiment_id):
"""Hide an immutable experiment; backtests and lineage must still resolve it.
@@ -324,7 +352,8 @@ class Experiments:
raise HTTPException(404, "研究实验不存在")
return {"ok": True}
async def preview(self, experiment_id, candidate_ids=None, source_kind=None, reference=None):
async def backtest_input(self, experiment_id, candidate_ids=None, source_kind=None, reference=None):
"""Build the complete fixed selection and enforce its domain checks."""
experiment = await self.get(experiment_id)
candidates = experiment["candidates"]
if candidate_ids is not None:
@@ -348,33 +377,32 @@ class Experiments:
scope = scope_of(SimulationSettings.model_validate(candidate["settings"]))
if not inputs or any(item["scope"] != scope for item in inputs):
raise HTTPException(422, "数据准备与回测参数组合不一致,请重新生成候选集合")
return await Backtests(self.db).preview(
PreviewInput(
inline=DraftInput(
name=experiment["name"],
source=Source(
kind=source_kind or experiment["kind"],
reference=reference or experiment_id,
research_id=experiment_id,
input_snapshot_ids=[i["id"] for i in inputs],
input_snapshot_id=inputs[0]["id"] if len(inputs) == 1 else None,
hypothesis=experiment["hypothesis"][:2000],
),
candidates=[
Candidate.model_validate(
{
key: item[key]
for key in ("client_item_id", "expression", "settings", "alpha_type")
}
)
for item in candidates
],
)
return DraftInput(
name=experiment["name"],
source=Source(
kind=source_kind or experiment["kind"],
reference=reference or experiment_id,
research_id=experiment_id,
input_snapshot_ids=[i["id"] for i in inputs],
input_snapshot_id=inputs[0]["id"] if len(inputs) == 1 else None,
hypothesis=experiment["hypothesis"][:2000],
),
preserve_source=True,
candidates=[
Candidate.model_validate(
{
key: item[key]
for key in ("client_item_id", "expression", "settings", "alpha_type")
}
)
for item in candidates
],
)
async def start_template_backtest(self, experiment_id, body):
async def preview(self, experiment_id, candidate_ids=None, source_kind=None, reference=None, *, backtests=None):
draft = await self.backtest_input(experiment_id, candidate_ids, source_kind, reference)
return await (backtests or Backtests(self.db)).preview(PreviewInput(inline=draft), preserve_source=True)
async def start_template_backtest(self, experiment_id, body, *, backtests=None, confirmed_preview=None):
"""Start the explicitly selected immutable collection in the caller's transaction.
Account locking covers preview creation as well as run creation, so concurrent
@@ -398,8 +426,24 @@ class Experiments:
raise HTTPException(422, "此入口仅用于模板候选集合")
if experiment["archived"]:
raise HTTPException(409, "候选集合已删除")
preview = await self.preview(experiment_id, body.candidate_ids)
return await Backtests(self.db).start(StartInput(
service = backtests or Backtests(self.db)
if confirmed_preview is None:
preview = await self.preview(experiment_id, body.candidate_ids, backtests=service)
else:
# Approval authorizes all persisted candidates, not the first display page.
draft = (await self.backtest_input(experiment_id, body.candidate_ids)).model_dump(mode="json")
expected = fingerprint({"candidates": draft["candidates"], "source": draft["source"]})
saved = await self.db.get(BacktestPreview, confirmed_preview["preview_id"])
if (
saved is None
or saved.version != confirmed_preview["version"]
or saved.digest != confirmed_preview["digest"]
or saved.digest != expected
or fingerprint({"candidates": saved.candidates, "source": saved.source}) != expected
):
raise HTTPException(409, "回测候选与确认内容不匹配,请重新确认")
preview = confirmed_preview
return await service.start(StartInput(
preview_id=preview["preview_id"], version=preview["version"], idempotency_key=body.idempotency_key,
))