feat: add scoped alpha sync and local self-correlation
This commit is contained in:
+52
-4
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user