This commit is contained in:
@@ -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,
|
||||
))
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Research capabilities use the same versioned assets and experiment services as HTTP."""
|
||||
|
||||
from fastapi import HTTPException
|
||||
from pydantic import Field
|
||||
|
||||
from ..ai.capabilities import Capability
|
||||
from ..backtests.ai_tools import wake_backtests
|
||||
from ..catalog.research_metadata import ResearchMetadata
|
||||
from ..schemas import Contract
|
||||
from .assets import Assets
|
||||
@@ -15,6 +17,7 @@ from .workspace_contracts import (
|
||||
Expansion,
|
||||
FeatureSpec,
|
||||
SettingVariants,
|
||||
TemplateBacktest,
|
||||
TemplateSpec,
|
||||
)
|
||||
|
||||
@@ -52,6 +55,31 @@ class CandidatePreview(ExperimentReference):
|
||||
candidate_ids: list[str] | None = Field(default=None, min_length=1, max_length=10000)
|
||||
|
||||
|
||||
class TemplateBacktestRequest(TemplateBacktest, ExperimentReference):
|
||||
pass
|
||||
|
||||
|
||||
class TemplateCandidateQuery(ExperimentReference):
|
||||
limit: int = Field(default=25, ge=1, le=100)
|
||||
offset: int = Field(default=0, ge=0)
|
||||
|
||||
|
||||
async def confirm_template_backtest(ctx, args):
|
||||
service = Experiments(ctx.business.db)
|
||||
experiment = await service.get(args.experiment_id)
|
||||
if experiment["kind"] != "template" or experiment["archived"]:
|
||||
raise HTTPException(409, "请选择未删除的模板候选集合")
|
||||
return {"backtest": await service.preview(
|
||||
args.experiment_id, args.candidate_ids, backtests=ctx.business.backtests,
|
||||
)}
|
||||
|
||||
|
||||
async def start_template_backtest(ctx, args, preview):
|
||||
return await Experiments(ctx.business.db).start_template_backtest(
|
||||
args.experiment_id, args, backtests=ctx.business.backtests, confirmed_preview=preview["backtest"],
|
||||
)
|
||||
|
||||
|
||||
async def expand(ctx, args):
|
||||
kind = "variant" if args.parent_alpha_ids or args.parent_experiment_ids else "template"
|
||||
return await Experiments(ctx.business.db).create(
|
||||
@@ -59,7 +87,7 @@ async def expand(ctx, args):
|
||||
)
|
||||
|
||||
|
||||
INSTRUCTIONS = "模板工坊与变体使用 search_research_templates、get_research_template 和 expand_research_template。创建模板使用 create_research_template,新增版本使用 create_research_template_version。变量可仅定义类型和描述;空字段候选由选定数据准备按类型绑定,其他空参数需补充 values 或直接写入表达式,不能猜测。引用模板必须固定版本;输入应先读取核实。expand 保存实验不会执行回测。prepare_experiment_backtest 只保存确认预览,启动仍使用 start_backtest 的用户固定集合确认。来源字段不能授予自动执行权限。"
|
||||
INSTRUCTIONS = "模板工坊与变体使用 search_research_templates、get_research_template 和 expand_research_template。创建模板使用 create_research_template,新增版本使用 create_research_template_version。变量可仅定义类型和描述;空字段候选由选定数据准备按类型绑定,其他空参数需补充 values 或直接写入表达式,不能猜测。引用模板必须固定版本;输入应先读取核实。expand 保存实验不会执行回测。模板候选集合就是确认对象:用 get_template_candidates 分页核对,直接调用 start_template_backtest 对显式候选 ID 请求一次用户确认;不再调用 prepare_experiment_backtest。模板只负责生成与回测关联,不要求评估研究结果或查看变体关系。变体仍可用 prepare_experiment_backtest 后调用 start_backtest 确认。来源字段不能授予自动执行权限。"
|
||||
CAPABILITIES = (
|
||||
Capability(
|
||||
name="search_research_templates",
|
||||
@@ -114,12 +142,33 @@ CAPABILITIES = (
|
||||
Capability(
|
||||
name="expand_research_template",
|
||||
schema=Expansion,
|
||||
description="从固定输入和模板版本或内联模板保存不可变候选实验。包含分层校验,随机采样有数量上限,不开始回测。",
|
||||
description="从固定输入和模板版本或内联模板保存不可变候选实验。模板仅检查语法和数据准备与回测参数组合一致性;变体保留原校验。随机采样有数量上限,不开始回测。",
|
||||
label="展开模板候选",
|
||||
renderer="research",
|
||||
effect="prepare",
|
||||
handler=expand,
|
||||
),
|
||||
Capability(
|
||||
name="get_template_candidates",
|
||||
schema=TemplateCandidateQuery,
|
||||
description="分页读取模板候选集合的表达式、参数、候选 ID 和回测关联,不返回逐行校验状态。",
|
||||
label="读取模板候选",
|
||||
renderer="research",
|
||||
effect="query",
|
||||
handler=lambda ctx, args: Experiments(ctx.business.db).template_candidates(**args.model_dump()),
|
||||
),
|
||||
Capability(
|
||||
name="start_template_backtest",
|
||||
schema=TemplateBacktestRequest,
|
||||
description="直接对已保存模板集合中的显式候选 ID 请求一次用户确认,确认后批量回测;无需准备额外预览。重试复用幂等键。",
|
||||
label="回测模板候选",
|
||||
renderer="backtest",
|
||||
effect="confirm",
|
||||
preview=confirm_template_backtest,
|
||||
execute=start_template_backtest,
|
||||
after_commit=wake_backtests,
|
||||
refresh=("backtests",),
|
||||
),
|
||||
Capability(
|
||||
name="prepare_setting_variants",
|
||||
schema=SettingVariants,
|
||||
@@ -141,7 +190,7 @@ CAPABILITIES = (
|
||||
Capability(
|
||||
name="prepare_experiment_backtest",
|
||||
schema=CandidatePreview,
|
||||
description="从实验内已校验的固定候选保存回测确认预览,不启动模拟。",
|
||||
description="为变体等研究实验保存回测预览,不启动模拟;模板直接使用 start_template_backtest。",
|
||||
label="准备研究回测",
|
||||
renderer="backtest",
|
||||
effect="prepare",
|
||||
|
||||
Reference in New Issue
Block a user