feat: add scoped alpha sync and local self-correlation

This commit is contained in:
yuxuanhui
2026-09-08 10:40:49 +08:00
parent 404a4d8a04
commit d2ccd94721
28 changed files with 1656 additions and 69 deletions
+52 -4
View File
@@ -4,6 +4,7 @@ Mutations never commit here, so the AI executor can atomically save their audit
Job runner notifications must happen after commit, using ``notify_job``.
"""
from datetime import timezone
from uuid import uuid4
from fastapi import HTTPException
@@ -11,7 +12,7 @@ from sqlalchemy import delete, func, select, update
from .alphas import list_statement, sorted_statement, summary
from .jobs import ACTIVE
from .models import Account, Alpha, Job, JobItem, Pnl, Research, ResearchTag, now
from .models import Account, Alpha, Job, JobItem, Pnl, Research, ResearchTag, SelfCorrelation, now
from .schemas import AlphaDetail, AlphaPage, BulkUpdate, JobInput, JobOutput, ResearchUpdate, normalize_tags
@@ -29,10 +30,51 @@ class Business:
.offset(filters.offset)
)
).all()
correlations = {
row.alpha_id: self.correlation_summary(row)
for row in (
await self.db.scalars(
select(SelfCorrelation).where(SelfCorrelation.alpha_id.in_([a.id for a, _ in rows]))
)
).all()
}
return AlphaPage(
items=[summary(a, r) for a, r in rows], total=total, limit=filters.limit, offset=filters.offset
items=[{**summary(a, r), "local_correlation": correlations.get(a.id)} for a, r in rows],
total=total,
limit=filters.limit,
offset=filters.offset,
).model_dump(mode="json")
@staticmethod
def correlation_summary(row):
return {
**{
key: row.result.get(key)
for key in ("status", "max_correlation", "compared_count", "skipped_count")
},
"stale": row.stale,
"calculated_at": row.calculated_at.replace(
tzinfo=row.calculated_at.tzinfo or timezone.utc
).isoformat(),
}
async def get_self_correlation(self, alpha_id):
if not await self.db.get(Alpha, alpha_id):
raise HTTPException(404, "Alpha 尚未同步")
row = await self.db.get(SelfCorrelation, alpha_id)
return {
"cached": row is not None,
"result": {
**row.result,
"stale": row.stale,
"calculated_at": row.calculated_at.replace(
tzinfo=row.calculated_at.tzinfo or timezone.utc
).isoformat(),
}
if row
else None,
}
async def get_alpha_facets(self):
result = {}
for key in ("region", "universe", "alpha_type", "language", "status", "stage"):
@@ -126,9 +168,15 @@ class Business:
async def create_sync_job(self, body: JobInput):
account = await self.db.scalar(select(Account).where(Account.id == 1).with_for_update())
if not account.password_encrypted or account.connection_status in ("disconnected", "error"):
if body.kind != "self_correlation" and (
not account.password_encrypted or account.connection_status in ("disconnected", "error")
):
raise HTTPException(409, "请先连接 WorldQuant")
payload = {"alpha_ids": body.alpha_ids}
if body.kind == "self_correlation":
found = set((await self.db.scalars(select(Alpha.id).where(Alpha.id.in_(body.alpha_ids)))).all())
if found != set(body.alpha_ids):
raise HTTPException(404, "部分 Alpha 尚未同步")
payload = body.model_dump(mode="json", exclude={"kind"}, exclude_none=True)
for job in (
await self.db.scalars(select(Job).where(Job.kind == body.kind, Job.status.in_(ACTIVE)))
).all():