Files
worldquant-alpha-system/backend/app/research_access/templates.py
T

69 lines
3.7 KiB
Python
Raw Normal View History

2026-09-11 23:05:01 +08:00
"""Persist caller-authored templates with frozen local research evidence."""
import math
from sqlalchemy import select
from ..models import BacktestItem, BacktestResult, ResearchAsset
from ..research.assets import Assets
from ..research.workspace_contracts import AssetWrite
from .queries import item_summary
async def create_template(db, args, principal, *, asset_id=None):
"""Save a new asset or revision inside the caller's account-locked, idempotent transaction.
2026-09-11 23:05:01 +08:00
Args contain the external model's TemplateSpec and local source item IDs.
Return the versioned asset and theoretical combination count. Raise
ResearchError for name conflicts or missing/incomplete research evidence.
Stored evidence proves provenance, not profitability or platform eligibility;
schema validation does not validate every expanded FASTEXPR combination.
Optional asset_id selects a version update guarded by args.expected_version;
a stale version raises HTTP 409 and cannot overwrite a historical revision.
2026-09-11 23:05:01 +08:00
"""
from .service import ResearchError
existing = await db.scalar(select(ResearchAsset).where(
ResearchAsset.kind == "template", ResearchAsset.name == args.template.name,
ResearchAsset.id != asset_id if asset_id else True,
2026-09-11 23:05:01 +08:00
).order_by(ResearchAsset.id).limit(1))
if existing:
raise ResearchError("TEMPLATE_NAME_CONFLICT", "模板名称已存在,请使用新名称;此工具不覆盖已有模板",
affected_items=[{"template_id": existing.id, "version": existing.version}])
previous = await Assets(db).get(asset_id, expected_kind="template") if asset_id else None
2026-09-11 23:05:01 +08:00
rows = (await db.execute(select(BacktestItem, BacktestResult).outerjoin(
BacktestResult, BacktestResult.item_id == BacktestItem.id,
).where(BacktestItem.id.in_(args.source_item_ids)))).all()
found = {item.id: (item, result) for item, result in rows}
missing = [item_id for item_id in args.source_item_ids if item_id not in found]
if missing:
raise ResearchError("NOT_FOUND", "部分来源回测候选不存在", affected_items=missing)
incomplete = [item.id for item, result in rows if (
item.platform_status != "completed" or item.collection_status != "complete"
or item.persistence_status != "saved" or not result or not result.complete
)]
if incomplete:
raise ResearchError("SOURCE_NOT_READY", "来源候选须完成回测、结果采集和持久化,请先读取 get_backtest_results",
affected_items=sorted(incomplete))
provenance = {
"source": {"kind": "mcp", "reference": args.reference},
"hypothesis": args.hypothesis,
"mcp_token_id": principal.token_id,
"admin_id": principal.admin_id,
"source_items": [item_summary(*found[item_id]) for item_id in args.source_item_ids],
}
if previous:
provenance["parent_template"] = {"id": asset_id, "version": args.expected_version}
2026-09-11 23:05:01 +08:00
asset = await Assets(db).save(AssetWrite(
kind="template", content=args.template.model_dump(mode="json"),
version=args.expected_version if asset_id else None,
), asset_id=asset_id, provenance=provenance)
2026-09-11 23:05:01 +08:00
return {
**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),
2026-09-11 23:05:01 +08:00
"validation": {"structure": "valid", "source_evidence": "recorded",
"expanded_candidates": "not_validated", "platform_semantics": "unknown"},
"next_step": "在模板工坊选择固定输入及模拟设置,展开并核验候选,再确认批量回测。",
}