feat: add local correlation filters and full recheck action
Deploy production / deploy (push) Successful in 54s
Deploy production / deploy (push) Successful in 54s
This commit is contained in:
@@ -195,6 +195,22 @@ def list_statement(filters):
|
||||
query = query.where(Alpha.submission_blocked == filters.submission_blocked)
|
||||
if filters.submission_blocked:
|
||||
query = query.where(submission_condition("UNSUBMITTED"))
|
||||
if (filters.local_correlation_status is not None or filters.local_correlation_min is not None
|
||||
or filters.local_correlation_max is not None):
|
||||
# One cache row per Alpha keeps totals/export stable; stale overrides the displayed status.
|
||||
query = query.outerjoin(SelfCorrelation, SelfCorrelation.alpha_id == Alpha.id)
|
||||
status = filters.local_correlation_status
|
||||
if status == "not_cached":
|
||||
query = query.where(SelfCorrelation.alpha_id.is_(None))
|
||||
elif status == "stale":
|
||||
query = query.where(SelfCorrelation.stale.is_(True))
|
||||
elif status is not None:
|
||||
query = query.where(SelfCorrelation.stale.is_(False), SelfCorrelation.result["status"].as_string() == status)
|
||||
coefficient = SelfCorrelation.result["max_correlation"].as_float()
|
||||
if filters.local_correlation_min is not None:
|
||||
query = query.where(coefficient >= filters.local_correlation_min)
|
||||
if filters.local_correlation_max is not None:
|
||||
query = query.where(coefficient <= filters.local_correlation_max)
|
||||
source_filters = {k: getattr(filters, k) for k in ("source", "source_reference", "research_id", "backtest_run_id")}
|
||||
if any(source_filters.values()):
|
||||
query = query.where(Alpha.id.in_(source_alpha_ids(**source_filters)))
|
||||
|
||||
+11
-2
@@ -193,7 +193,7 @@ 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 body.kind != "self_correlation" and (
|
||||
if body.kind not in ("self_correlation", "self_correlation_recheck") and (
|
||||
not account.password_encrypted or account.connection_status in ("disconnected", "error")
|
||||
):
|
||||
raise HTTPException(409, "请先连接 WorldQuant")
|
||||
@@ -205,9 +205,18 @@ class Business:
|
||||
for job in (
|
||||
await self.db.scalars(select(Job).where(Job.kind == body.kind, Job.status.in_(ACTIVE)))
|
||||
).all():
|
||||
if body.kind == "pnl_backfill" or job.payload == payload:
|
||||
if body.kind in ("pnl_backfill", "self_correlation_recheck") or job.payload == payload:
|
||||
return JobOutput.model_validate(job).model_dump(mode="json")
|
||||
job = Job(id=str(uuid4()), kind=body.kind, payload=payload)
|
||||
if body.kind == "self_correlation_recheck":
|
||||
# Freeze every qualifying target at click time, without the manual-ID batch limit.
|
||||
ids = list((await self.db.scalars(
|
||||
select(Alpha.id).where(Alpha.check_type.in_(("PRE_CHECK", "PASS"))).order_by(Alpha.id)
|
||||
)).all())
|
||||
job.payload = {"alpha_ids": ids}
|
||||
job.total = len(ids)
|
||||
if not ids:
|
||||
job.status = "completed"
|
||||
if body.kind == "pnl_backfill":
|
||||
# Fix the full missing set on the server, independently of UI paging.
|
||||
# The account lock above also serializes duplicate button clicks.
|
||||
|
||||
+1
-1
@@ -242,7 +242,7 @@ class Runner:
|
||||
async with self.sessions() as db:
|
||||
job = await db.get(Job, job_id)
|
||||
kind, payload = job.kind, job.payload
|
||||
if kind == "self_correlation":
|
||||
if kind in ("self_correlation", "self_correlation_recheck"):
|
||||
# Cached local comparisons also work while the platform is disconnected.
|
||||
await self.check_correlations(job_id, payload["alpha_ids"])
|
||||
elif kind == "verify":
|
||||
|
||||
@@ -69,6 +69,9 @@ class PreferencesInput(Contract):
|
||||
|
||||
|
||||
class AlphaFilters(Contract):
|
||||
local_correlation_status: Literal["not_cached", "stale", "low", "high", "partial", "insufficient_data"] | None = None
|
||||
local_correlation_min: float | None = Field(default=None, ge=-1, le=1)
|
||||
local_correlation_max: float | None = Field(default=None, ge=-1, le=1)
|
||||
submission_blocked: bool | None = None
|
||||
submission: Submission | None = None
|
||||
source: str | None = Field(default=None, max_length=100)
|
||||
@@ -125,7 +128,7 @@ class AlphaFilters(Contract):
|
||||
@model_validator(mode="after")
|
||||
def range_order(self):
|
||||
for key in (
|
||||
"sharpe", "fitness", "returns", "turnover", "margin", "drawdown",
|
||||
"local_correlation", "sharpe", "fitness", "returns", "turnover", "margin", "drawdown",
|
||||
"sub_universe_sharpe", "robust_universe_sharpe", "two_year_sharpe", "prod_correlation", "pnl",
|
||||
):
|
||||
lo, hi = getattr(self, f"{key}_min"), getattr(self, f"{key}_max")
|
||||
@@ -188,7 +191,7 @@ class BulkUpdate(BulkInput):
|
||||
|
||||
|
||||
class JobInput(Contract):
|
||||
kind: Literal["full_sync", "daily_sync", "alpha_refresh", "pnl_refresh", "pnl_backfill", "self_correlation"]
|
||||
kind: Literal["full_sync", "daily_sync", "alpha_refresh", "pnl_refresh", "pnl_backfill", "self_correlation", "self_correlation_recheck"]
|
||||
alpha_ids: list[str] = Field(default_factory=list)
|
||||
submission: Submission | None = None
|
||||
date_from: date | None = None
|
||||
@@ -196,7 +199,10 @@ class JobInput(Contract):
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_ids(self):
|
||||
if self.kind == "pnl_backfill":
|
||||
if self.kind == "self_correlation_recheck":
|
||||
if self.alpha_ids or self.submission is not None or self.date_from is not None or self.date_to is not None:
|
||||
raise ValueError("重检自相关覆盖全部预检通过和检查通过的 Alpha,不接受 ID、分组或日期范围")
|
||||
elif self.kind == "pnl_backfill":
|
||||
if self.alpha_ids or self.submission is not None or self.date_from is not None or self.date_to is not None:
|
||||
raise ValueError("检查 PnL 自动覆盖全部本地已提交 Alpha,不接受 ID、分组或日期范围")
|
||||
elif self.kind in ("full_sync", "daily_sync"):
|
||||
|
||||
Reference in New Issue
Block a user