feat: persist feature research and versioned alpha assessments
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
"""Snapshot-based research assessment. Model advice cannot replace deterministic findings."""
|
||||
|
||||
import math
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import select
|
||||
|
||||
from ..backtests.service import Backtests, uid
|
||||
from ..models import Alpha, ResearchEvaluation, ResearchExperiment, SelfCorrelation
|
||||
from .experiments import Experiments
|
||||
from .serialization import encode_snapshot as jsonable_encoder
|
||||
|
||||
|
||||
def assess(snapshot, rules):
|
||||
metrics = snapshot.get("is") or {}
|
||||
evidence, missing, failed = [], [], []
|
||||
for key, bound, direction in (
|
||||
("sharpe", rules.sharpe_min, "min"),
|
||||
("fitness", rules.fitness_min, "min"),
|
||||
("turnover", rules.turnover_max, "max"),
|
||||
):
|
||||
value = metrics.get(key)
|
||||
if type(value) not in (int, float) or not math.isfinite(value):
|
||||
missing.append(key)
|
||||
value, status = None, "unknown"
|
||||
else:
|
||||
status = "pass" if (value >= bound if direction == "min" else value <= bound) else "block"
|
||||
if status == "block":
|
||||
failed.append(key)
|
||||
evidence.append(
|
||||
{"metric": key, "value": value, "bound": bound, "direction": direction, "status": status}
|
||||
)
|
||||
checks = metrics.get("checks") or []
|
||||
if not checks:
|
||||
missing.append("platform_checks")
|
||||
for check in checks:
|
||||
if check.get("result") == "FAIL":
|
||||
failed.append(f"platform:{check.get('name', 'unknown')}")
|
||||
unknown_checks = [check for check in checks if check.get("result") not in ("PASS", "FAIL")]
|
||||
if unknown_checks:
|
||||
missing.append("unresolved_platform_checks")
|
||||
return {
|
||||
"verdict": "block" if failed else "review" if missing else "pass",
|
||||
"evidence": evidence,
|
||||
"failed": failed,
|
||||
"missing": missing,
|
||||
"existing_platform_checks": checks,
|
||||
"meaning": "本地研究筛选结果,不是官方提交资格",
|
||||
}
|
||||
|
||||
|
||||
class Evaluations:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
|
||||
async def create(self, body):
|
||||
records = []
|
||||
if body.alpha_id:
|
||||
alpha = await self.db.get(Alpha, body.alpha_id)
|
||||
if not alpha:
|
||||
raise HTTPException(404, "Alpha 尚未同步")
|
||||
records.append(
|
||||
{
|
||||
"alpha_id": alpha.id,
|
||||
"snapshot": alpha.raw,
|
||||
"observed_at": alpha.synced_at,
|
||||
"client_item_id": None,
|
||||
}
|
||||
)
|
||||
else:
|
||||
run = await Backtests(self.db).run(body.backtest_run_id)
|
||||
if body.experiment_id and run["source"].get("research_id") != body.experiment_id:
|
||||
raise HTTPException(422, "回测运行不属于指定研究实验")
|
||||
for offset in range(0, run["total"], 100):
|
||||
page = await Backtests(self.db).results(body.backtest_run_id, 100, offset)
|
||||
for item in page["items"]:
|
||||
records.append(
|
||||
{
|
||||
"alpha_id": item["alpha_id"],
|
||||
"snapshot": item["result"]["snapshot"] if item["result"] else {},
|
||||
"observed_at": item["result"]["observed_at"] if item["result"] else None,
|
||||
"client_item_id": item["client_item_id"],
|
||||
"item_id": item["id"],
|
||||
"error": item["error"],
|
||||
"complete": bool(item["result"] and item["result"]["complete"]),
|
||||
"expression": item["expression"],
|
||||
"settings": item["settings"],
|
||||
}
|
||||
)
|
||||
if body.experiment_id and not await self.db.get(ResearchExperiment, body.experiment_id):
|
||||
raise HTTPException(404, "研究实验不存在")
|
||||
findings = []
|
||||
for record in records:
|
||||
correlation = (
|
||||
await self.db.get(SelfCorrelation, record["alpha_id"]) if record["alpha_id"] else None
|
||||
)
|
||||
finding = assess(record["snapshot"], body.rules)
|
||||
if record.get("error") or record.get("complete") is False:
|
||||
finding["missing"].append("backtest_error")
|
||||
if finding["verdict"] == "pass":
|
||||
finding["verdict"] = "review"
|
||||
findings.append(
|
||||
{
|
||||
**record,
|
||||
**finding,
|
||||
"local_correlation": {
|
||||
"result": correlation.result,
|
||||
"stale": correlation.stale,
|
||||
"calculated_at": correlation.calculated_at,
|
||||
}
|
||||
if correlation
|
||||
else None,
|
||||
}
|
||||
)
|
||||
report = jsonable_encoder(
|
||||
{
|
||||
"rules": body.rules.model_dump(),
|
||||
"records": findings,
|
||||
"backtest_run_id": body.backtest_run_id,
|
||||
"experiment": await Experiments(self.db).get(body.experiment_id)
|
||||
if body.experiment_id
|
||||
else None,
|
||||
"model_advice": None,
|
||||
"verdict": "block"
|
||||
if any(r["verdict"] == "block" for r in findings)
|
||||
else "review"
|
||||
if not findings or any(r["verdict"] == "review" for r in findings)
|
||||
else "pass",
|
||||
}
|
||||
)
|
||||
row = ResearchEvaluation(
|
||||
id=uid(), alpha_id=body.alpha_id, experiment_id=body.experiment_id, report=report
|
||||
)
|
||||
self.db.add(row)
|
||||
await self.db.flush()
|
||||
return await self.get(row.id)
|
||||
|
||||
async def get(self, evaluation_id):
|
||||
row = await self.db.get(ResearchEvaluation, evaluation_id)
|
||||
if not row:
|
||||
raise HTTPException(404, "评估报告不存在")
|
||||
return jsonable_encoder(
|
||||
{key: getattr(row, key) for key in ("id", "alpha_id", "experiment_id", "report", "created_at")}
|
||||
)
|
||||
|
||||
async def list(self, alpha_id=None, experiment_id=None, limit=25, offset=0):
|
||||
query = select(ResearchEvaluation)
|
||||
if alpha_id:
|
||||
query = query.where(ResearchEvaluation.alpha_id == alpha_id)
|
||||
if experiment_id:
|
||||
query = query.where(ResearchEvaluation.experiment_id == experiment_id)
|
||||
rows = await self.db.scalars(
|
||||
query.order_by(ResearchEvaluation.created_at.desc()).limit(limit).offset(offset)
|
||||
)
|
||||
return {"items": [await self.get(row.id) for row in rows]}
|
||||
|
||||
async def add_advice(self, evaluation_id, advice, model_evidence):
|
||||
original = await self.get(evaluation_id)
|
||||
report = {
|
||||
**original["report"],
|
||||
"model_advice": advice,
|
||||
"model_evidence": model_evidence,
|
||||
"previous_evaluation_id": evaluation_id,
|
||||
}
|
||||
row = ResearchEvaluation(
|
||||
id=uid(), alpha_id=original["alpha_id"], experiment_id=original["experiment_id"], report=report
|
||||
)
|
||||
self.db.add(row)
|
||||
await self.db.flush()
|
||||
return await self.get(row.id)
|
||||
Reference in New Issue
Block a user