This commit is contained in:
@@ -96,7 +96,7 @@ async def wake_backtests(runner, result):
|
||||
runner.backtests.wake.set()
|
||||
|
||||
|
||||
INSTRUCTIONS = "回测先读取能力再准备固定候选预览,每次运行确认一次;后续候选新建预览。停止生成不取消回测。\n回测结果追问用 get_backtest/get_backtest_results。上下文或历史没有运行 ID 时,可用 list_backtests 按 source=chatbox 和会话 reference 找回;不得把启动返回当作结果。"
|
||||
INSTRUCTIONS = "模板集合使用 start_template_backtest 直接请求确认;其他回测先读取能力再准备固定候选预览。每次运行确认一次;后续候选新建预览。停止生成不取消回测。\n回测结果追问用 get_backtest/get_backtest_results。上下文或历史没有运行 ID 时,可用 list_backtests 按 source=chatbox 和会话 reference 找回;不得把启动返回当作结果。"
|
||||
|
||||
|
||||
CAPABILITIES = (
|
||||
|
||||
@@ -35,8 +35,11 @@ TOOLS = {
|
||||
"get_data_preparation": (c.PreparationRead, "preparation", "research:read", "按集合 ID 与版本分页预览字段、类型、描述和数据集归属。提交回测时携带 preparation_refs,由服务端核对版本并固定独立输入快照;空集合不能用于研究。"),
|
||||
"search_research_templates": (c.TemplateSearch, "templates", "research:read", "分页搜索模板工坊的模板与最新版本,不执行研究。"),
|
||||
"get_research_template": (c.TemplateRead, "template", "research:read", "读取模板内容、字段定义和来源;新增版本前核对最新版本。"),
|
||||
"create_research_template_version": (c.CreateTemplateVersion, "create_template_version", "research:write", "为已有模板新增不可变版本。先读取模板,携带 template_id、expected_version、完整 template、hypothesis、1–20 个已完成采集的 source_item_ids 及 idempotency_key。沿用创建模板的结构和来源校验;版本冲突须重新读取,不覆盖历史、不调用模型、不回测。"),
|
||||
"create_research_template": (c.CreateTemplate, "create_template", "research:write", "将调用方大模型研究后自行总结的参数化模板保存到模板工坊,供用户后续批量回测。先用 get_backtest_results 阅读实际指标和检查,选择 1–20 个已完成采集的 source_item_ids,并说明 hypothesis;不要把 completed 当作检查通过。template 使用 {name} 占位符及逐一对应的 variables,字段变量须声明 MATRIX/VECTOR/GROUP,VECTOR 聚合须明确写入表达式。提供唯一名称和 idempotency_key,可附 reference。返回模板 ID、版本和理论组合数;仅核验结构及来源,不验证所有参数组合,不再次调用模型、不执行回测、不覆盖已有模板。"),
|
||||
"create_research_template_version": (c.CreateTemplateVersion, "create_template_version", "research:write", "为已有模板新增不可变版本。先读取模板,携带 template_id、expected_version、完整 template、hypothesis 和 idempotency_key。source_item_ids 可选,提供时须为真实、已完成采集的回测项。版本冲突须重新读取;不调用模型、不回测。"),
|
||||
"create_research_template": (c.CreateTemplate, "create_template", "research:write", "保存调用方编写的模板,不要求已有回测结果。提供 template、hypothesis、唯一名称和 idempotency_key;source_item_ids 可选,提供时须为真实、已完成采集的回测项。template 使用 {name} 占位符及对应 variables,字段定义只需类型和描述;空字段 values 由展开时的数据准备绑定,其他空参数须补充 values 或直接写入表达式。仅保存模板,不调用模型、不展开、不执行回测。"),
|
||||
"expand_research_template": (c.TemplateExpansion, "expand_template", "research:write", "按 template_id/version、preparation_refs 和完整 settings 生成固定候选集合,支持全组合或固定 seed 随机采样。仅检查语法和数据准备/回测参数组合一致性,无逐行校验状态,不回测。返回首25项和 experiment_id,更多候选用 get_template_candidates 分页读取;用户授权后按明确 candidate_ids 调用 start_template_backtest。重试复用 idempotency_key。"),
|
||||
"get_template_candidates": (c.TemplateCandidates, "template_candidates", "research:read", "分页读取固定模板候选集合的表达式、参数、候选 ID、模板版本、数据准备及关联回测;每页最多100条,total 不是当前页数量。"),
|
||||
"start_template_backtest": (c.SubmitTemplateBacktest, "start_template_backtest", "backtests:execute", "对用户已授权的模板候选集合执行批量回测。提供 experiment_id、明确的 candidate_ids 和 idempotency_key;服务端使用保存的表达式和参数并保留来源,不接受重写输入。不需要另建预览,不重新校验类型或平台可用性。相同请求重试返回同一运行;新幂等键会创建新的回测,包括重复候选。立即返回运行 ID,结果另行查询。"),
|
||||
"get_submission_check": (c.SelfCorrelationReference, "submission_check_context", "research:read", "读取已导入 Alpha 的表达式、Description、snapshot 和缓存检查结果;check_summary 分离 Alpha 检查和 REGULAR_SUBMISSION 提交限制,原始 checks 保留;限制不代表当前额度。不发起检查。先核对或生成三段 Description,再调用 check_submission。"),
|
||||
"check_submission": (c.SubmissionCheck, "check_submission", "research:refresh", "对单个待提交 Alpha 写回已获用户授权的 Description 并调用平台 GET /check,返回 job_id。须先用 get_submission_check 获取 snapshot;保留本地自相关门槛和冲突保护。通过 get_refresh_job 查进度、get_submission_check 读结果。无论检查结果如何,都不会调用 /submit 或正式提交 Alpha。"),
|
||||
"get_worldquant_connection": (c.ConnectionReference, "connection", "research:read", "读取 WorldQuant 连接状态及可选认证 job_id 的进度,不发起认证;人工验证在网页完成。"),
|
||||
@@ -70,7 +73,7 @@ class MCPResearchServer:
|
||||
self.mutation_lock = asyncio.Lock()
|
||||
self.server = Server("wq-alpha-research", version="1.0.0", on_list_tools=self.list_tools,
|
||||
on_call_tool=self.call_tool,
|
||||
instructions="自由探索,直接固定候选回测,无需先建研究资产。工具不安排定时研究;结果按运行 ID 查询。")
|
||||
instructions="自由探索,直接固定候选回测,无需先建研究资产。使用已有模板时,expand_research_template 生成候选,get_template_candidates 分页核对,获得用户授权后 start_template_backtest 执行;无需额外预览或结果评估步骤。工具不安排定时研究;结果按运行 ID 查询。")
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
host = urlsplit(settings.public_origin).netloc
|
||||
@@ -85,8 +88,8 @@ class MCPResearchServer:
|
||||
return types.ListToolsResult(tools=[types.Tool(name=name, description=description,
|
||||
inputSchema=schema.model_json_schema(), annotations=types.ToolAnnotations(
|
||||
readOnlyHint=scope == "research:read", destructiveHint=method == "control",
|
||||
idempotentHint=method in {"submit", "control", "create_template", "create_template_version", "save_super_plan", "build_super_candidates"} or scope == "research:read",
|
||||
openWorldHint=method in {"refresh", "submit", "metadata", "check_self_correlation", "check_submission", "authenticate", "pyramid_distribution", "preview_super_selection"}))
|
||||
idempotentHint=method in {"submit", "control", "create_template", "create_template_version", "save_super_plan", "build_super_candidates", "expand_template", "start_template_backtest"} or scope == "research:read",
|
||||
openWorldHint=method in {"refresh", "submit", "metadata", "check_self_correlation", "check_submission", "authenticate", "pyramid_distribution", "preview_super_selection", "start_template_backtest"}))
|
||||
for name, (schema, method, scope, description) in TOOLS.items()
|
||||
if scope in principal.scopes and "research:read" in principal.scopes])
|
||||
|
||||
@@ -139,7 +142,7 @@ class MCPResearchServer:
|
||||
data = {"error": ResearchError(code, "研究操作失败;可使用原幂等键重试或查询历史", retryable=True).data}
|
||||
db.add(MCPAudit(id=str(uuid4()), token_id=principal.token_id, tool=name,
|
||||
request_id=fingerprint({"request_id": request_id}), input_digest=digest,
|
||||
business_id=data.get("backtest_run_id", data.get("job_id", data.get("template_id", data.get("id")))),
|
||||
business_id=data.get("backtest_run_id", data.get("job_id", data.get("experiment_id", data.get("template_id", data.get("id"))))),
|
||||
result_code=code, elapsed_ms=int((time.monotonic()-started)*1000)))
|
||||
if not error:
|
||||
if access.wake == "backtests":
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -116,7 +116,7 @@ class Control(Contract):
|
||||
class CreateTemplate(Contract):
|
||||
template: TemplateSpec
|
||||
hypothesis: str = Field(min_length=1, max_length=10000)
|
||||
source_item_ids: list[RunId] = Field(min_length=1, max_length=20)
|
||||
source_item_ids: list[RunId] = Field(default_factory=list, max_length=20)
|
||||
reference: str | None = Field(default=None, max_length=200)
|
||||
idempotency_key: Identifier
|
||||
|
||||
@@ -147,6 +147,27 @@ class TemplateSearch(Page):
|
||||
q: str = Field(default="", max_length=200)
|
||||
|
||||
|
||||
class TemplateExpansion(Contract):
|
||||
template_id: RunId
|
||||
version: int = Field(ge=1)
|
||||
preparation_refs: list[PreparationReference] = Field(min_length=1, max_length=20)
|
||||
settings: CompleteSettings
|
||||
mode: Literal["all", "random"] = "all"
|
||||
limit: int = Field(default=100, ge=1, le=10000)
|
||||
seed: int = 0
|
||||
idempotency_key: Identifier
|
||||
|
||||
|
||||
class TemplateCandidates(Page):
|
||||
experiment_id: RunId
|
||||
|
||||
|
||||
class SubmitTemplateBacktest(Contract):
|
||||
experiment_id: RunId
|
||||
candidate_ids: list[Identifier] = Field(min_length=1, max_length=10000)
|
||||
idempotency_key: Identifier
|
||||
|
||||
|
||||
class CatalogSearch(Contract):
|
||||
filters: CatalogFilters
|
||||
dataset_id: str | None = Field(default=None, min_length=1, max_length=200)
|
||||
|
||||
@@ -83,7 +83,11 @@ class ResearchAccess(SuperResearchAccess):
|
||||
"create_with": "create_research_template", "required_scope": "research:write",
|
||||
"version_with": "create_research_template_version",
|
||||
"read_with": "get_research_template", "search_with": "search_research_templates",
|
||||
"authored_by": "caller", "max_source_items": 20,
|
||||
"authored_by": "caller", "source_items_required": False, "max_source_items": 20,
|
||||
"expand_with": "expand_research_template", "candidates_with": "get_template_candidates",
|
||||
"backtest_with": "start_template_backtest", "execution_scope": "backtests:execute",
|
||||
"max_candidates": 10000, "page_size_max": 100,
|
||||
"validation": "syntax_and_preparation_settings_combination",
|
||||
"source_items_with": "get_backtest_results", "starts_backtests": False,
|
||||
"web_url": f"{self.public_origin}/#templates",
|
||||
},
|
||||
@@ -311,6 +315,52 @@ class ResearchAccess(SuperResearchAccess):
|
||||
result["web_url"] = f"{self.public_origin}/#templates"
|
||||
return await self.remember(operation, args, digest, result, business_id=args.template_id)
|
||||
|
||||
async def expand_template(self, args):
|
||||
"""Idempotently freeze a version and preparations; never start execution."""
|
||||
from ..research.experiments import Experiments
|
||||
from ..research.workspace_contracts import Expansion
|
||||
|
||||
operation = "expand_research_template"
|
||||
previous, digest = await self.previous(operation, args)
|
||||
if previous:
|
||||
return previous.response
|
||||
service = Experiments(self.db)
|
||||
asset = await service.assets.get(args.template_id, args.version, "template")
|
||||
experiment = await service.create(Expansion(
|
||||
asset_id=args.template_id, version=args.version, preparation_refs=args.preparation_refs,
|
||||
settings=args.settings, mode=args.mode, limit=args.limit, seed=args.seed,
|
||||
hypothesis=asset["content"].get("description", "").strip() or f"使用模板:{asset['name']}",
|
||||
), extra_evidence={"method": "template", "mcp_token_id": self.principal.token_id,
|
||||
"admin_id": self.principal.admin_id})
|
||||
result = await service.template_candidates(experiment["id"])
|
||||
result.update({"read_with": "get_template_candidates", "backtest_with": "start_template_backtest",
|
||||
"starts_backtests": False})
|
||||
return await self.remember(operation, args, digest, result, business_id=experiment["id"])
|
||||
|
||||
async def template_candidates(self, args):
|
||||
from ..research.experiments import Experiments
|
||||
|
||||
return await Experiments(self.db).template_candidates(args.experiment_id, args.limit, args.offset)
|
||||
|
||||
async def start_template_backtest(self, args):
|
||||
"""Execute only stored candidate IDs; authorization comes from the caller's execute scope."""
|
||||
from ..research.experiments import Experiments
|
||||
from ..research.workspace_contracts import TemplateBacktest
|
||||
|
||||
operation = "start_template_backtest"
|
||||
previous, digest = await self.previous(operation, args)
|
||||
if previous:
|
||||
return previous.response
|
||||
provenance = {"mcp_token_id": self.principal.token_id, "admin_id": self.principal.admin_id}
|
||||
result = await Experiments(self.db).start_template_backtest(
|
||||
args.experiment_id,
|
||||
TemplateBacktest(candidate_ids=args.candidate_ids, idempotency_key="mcp-template-" + str(uuid4())),
|
||||
backtests=Backtests(self.db, provenance),
|
||||
)
|
||||
result = {**result, "input_digest": digest, "web_url": self.run_url(result["backtest_run_id"])}
|
||||
return await self.remember(operation, args, digest, result,
|
||||
business_id=result["backtest_run_id"], wake="backtests")
|
||||
|
||||
async def previous(self, operation, args):
|
||||
# PostgreSQL row lock is shared with HTTP start and catalog/job creation.
|
||||
account = await self.db.scalar(select(Account).where(Account.id == self.principal.account_id).with_for_update())
|
||||
|
||||
@@ -62,7 +62,7 @@ async def create_template(db, args, principal, *, asset_id=None):
|
||||
**asset, "template_id": asset["id"],
|
||||
"combination_count": (str(math.prod(len(v.values) for v in args.template.variables.values()))
|
||||
if all(v.values for v in args.template.variables.values()) else None),
|
||||
"validation": {"structure": "valid", "source_evidence": "recorded",
|
||||
"validation": {"structure": "valid", "source_evidence": "recorded" if args.source_item_ids else "not_provided",
|
||||
"expanded_candidates": "not_validated", "platform_semantics": "unknown"},
|
||||
"next_step": "在模板工坊选择固定输入及模拟设置,展开并核验候选,再确认批量回测。",
|
||||
"next_step": "选择数据准备和回测参数,用 expand_research_template 生成候选集合;核对候选后按已获授权范围调用 start_template_backtest。也可在模板工坊完成。",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user