feat(research): streamline template details and enable bot versioning
Deploy production / deploy (push) Successful in 1m35s

This commit is contained in:
yuxuanhui
2026-09-20 09:57:37 +08:00
parent ba60d8e5c4
commit 34f1a4fa77
15 changed files with 593 additions and 374 deletions
+12 -4
View File
@@ -10,23 +10,27 @@ from ..research.workspace_contracts import AssetWrite
from .queries import item_summary
async def create_template(db, args, principal):
"""Save a new asset inside the caller's account-locked, idempotent transaction.
async def create_template(db, args, principal, *, asset_id=None):
"""Save a new asset or revision inside the caller's account-locked, idempotent transaction.
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.
"""
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,
).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
rows = (await db.execute(select(BacktestItem, BacktestResult).outerjoin(
BacktestResult, BacktestResult.item_id == BacktestItem.id,
).where(BacktestItem.id.in_(args.source_item_ids)))).all()
@@ -48,12 +52,16 @@ async def create_template(db, args, principal):
"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}
asset = await Assets(db).save(AssetWrite(
kind="template", content=args.template.model_dump(mode="json"),
), provenance=provenance)
version=args.expected_version if asset_id else None,
), asset_id=asset_id, provenance=provenance)
return {
**asset, "template_id": asset["id"],
"combination_count": str(math.prod(len(v.values) for v in args.template.variables.values())),
"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",
"expanded_candidates": "not_validated", "platform_semantics": "unknown"},
"next_step": "在模板工坊选择固定输入及模拟设置,展开并核验候选,再确认批量回测。",