feat: add AI descriptions and platform submission checks

This commit is contained in:
yuxuanhui
2026-09-09 20:24:47 +08:00
parent 6b4990f100
commit 53b01eb770
16 changed files with 1007 additions and 4 deletions
+44 -3
View File
@@ -1,4 +1,4 @@
"""WorldQuant adapter. Only authentication and explicit backtests allow upstream POST.
"""WorldQuant adapter. Explicit descriptions allow PATCH; Alpha submission is not exposed.
No upstream response body or request headers are included in exceptions: they may
contain credentials, cookies, or temporary authentication links.
@@ -268,7 +268,7 @@ class WqClient:
async def get(self, path: str, params=None, headers=None):
return await self._read_json("GET", path, params=params, headers=headers)
async def _read_json(self, method: str, path: str, *, allow_list=False, poll_attempts=None, **kwargs):
async def _read_json(self, method: str, path: str, *, allow_list=False, poll_attempts=None, wait_for_retry_header=False, **kwargs):
"""Authenticated read with shared refresh/retry handling; callers use GET or OPTIONS."""
if not self.credentials:
raise WqError("请先连接 WorldQuant", "disconnected")
@@ -292,7 +292,7 @@ class WqClient:
# Recordsets may return 200/202 with Retry-After before results exist.
if (
response.headers.get("Retry-After")
and self.retry_delay(response.headers["Retry-After"], 0) > 0
and (wait_for_retry_header or self.retry_delay(response.headers["Retry-After"], 0) > 0)
):
if attempt + 1 == attempts:
raise WqError("平台数据仍在准备,请稍后重试", "pending")
@@ -390,6 +390,47 @@ class WqClient:
async def alpha(self, alpha_id):
return await self.get(f"/alphas/{alpha_id}")
async def patch_descriptions(self, alpha_id, descriptions):
"""Write only reviewed descriptions; ambiguous writes require GET reconciliation.
No blind transport/5xx retry: the first PATCH may already have succeeded.
A job retry reads the current Alpha before deciding whether PATCH is needed.
"""
if not re.fullmatch(r"[A-Za-z0-9_-]{1,100}", alpha_id) or not descriptions or not set(descriptions) <= {"regular", "selection", "combo"}:
raise WqError("Description 写入参数无效", "invalid_description")
if any(not isinstance(value, dict) or set(value) != {"description"} or not isinstance(value["description"], str) for value in descriptions.values()):
raise WqError("仅允许写入 Description", "invalid_description")
if not self.credentials:
raise WqError("请先连接 WorldQuant", "disconnected")
if not self.authenticated:
await self.authenticate(*self.credentials)
for attempt in range(2):
generation = self.auth_generation
try:
response = await self.client.patch(f"/alphas/{alpha_id}", json=descriptions)
except httpx.TransportError:
raise WqError("Description 写回结果未知;重试任务将先核对平台内容", "description_unknown") from None
if response.status_code == 401 and attempt == 0:
await self.authenticate(*self.credentials, stale_generation=generation)
continue
if response.status_code in (200, 204):
return
raise WqError(f"Description 写回未确认(HTTP {response.status_code});可重试任务核对平台内容", "description_write_failed")
async def submission_check(self, alpha_id):
"""cnhkmcp /check contract: wait on Retry-After and return only is.checks."""
if not re.fullmatch(r"[A-Za-z0-9_-]{1,100}", alpha_id):
raise WqError("Alpha ID 无效", "invalid_alpha")
raw = await self._read_json(
"GET", f"/alphas/{alpha_id}/check", poll_attempts=self.settings.pnl_poll_attempts,
wait_for_retry_header=True,
)
metrics = raw.get("is")
checks = metrics.get("checks") if isinstance(metrics, dict) else None
if not isinstance(checks, list) or any(not isinstance(item, dict) for item in checks):
raise WqError("平台检查尚无有效 is.checks 结果,请稍后重试", "pending")
return checks
async def pnl(self, alpha_id):
"""Wait for slow PnL generation separately from transport-error retries.