feat: expose local self-correlation tools through MCP
Deploy production / deploy (push) Successful in 57s

This commit is contained in:
yuxuanhui
2026-09-10 09:42:24 +08:00
parent 53b01eb770
commit d4c33d0124
7 changed files with 187 additions and 9 deletions
+9
View File
@@ -11,6 +11,7 @@ from ..schemas import Contract
Identifier = Annotated[str, Field(min_length=1, max_length=100)]
RunId = Annotated[str, Field(min_length=1, max_length=36)]
AlphaId = Annotated[str, Field(min_length=1, max_length=100, pattern=r"^[A-Za-z0-9_-]+$")]
class Empty(Contract):
@@ -124,6 +125,14 @@ class JobReference(Page):
job_id: RunId
class SelfCorrelationCheck(Contract):
alpha_ids: list[AlphaId] = Field(min_length=1, max_length=100)
class SelfCorrelationReference(Contract):
alpha_id: AlphaId
class History(Page):
source: str | None = Field(default=None, max_length=100)
reference: str | None = Field(default=None, max_length=200)
+32 -2
View File
@@ -16,6 +16,7 @@ from ..catalog.contracts import CatalogJobInput
from ..catalog.platform import platform_options, validate_platform_scope
from ..catalog.research_metadata import ResearchMetadata, availability_key
from ..catalog.service import Catalog
from ..correlation import MIN_SAMPLES, THRESHOLD, WINDOW_YEARS
from ..models import Account, Alpha, BacktestItem, Job, JobItem, ResearchRequest, SimulationAttempt, now
from ..research.serialization import encode_snapshot
from ..research.workspace_contracts import FieldAvailabilityInput
@@ -48,7 +49,15 @@ class ResearchAccess:
"settings_schema": DirectCandidate.model_json_schema(),
"confirmation": "调用者须已获本批执行授权;直接提交后返回稳定运行 ID",
"duplicate_policies": ["reject", "rerun"], "permissions": sorted(self.principal.scopes),
"metadata_only": True, "actual_platform_allowance": None}
"metadata_only": True, "actual_platform_allowance": None,
"self_correlation": {
"check_with": "check_self_correlation", "read_with": "get_self_correlation",
"job_with": "get_refresh_job", "max_targets": 100, "source": "local",
"reference_scope": "本地已同步的同地区已提交 Alpha,排除自身",
"method": "累计 PnL 日变化的 Pearson 相关系数,取带符号最大值",
"threshold": THRESHOLD, "min_samples": MIN_SAMPLES, "window_years": WINDOW_YEARS,
"platform_check": False,
}}
async def catalog(self, args):
data = await Catalog(self.db).search(args.filters, args.dataset_id)
@@ -101,7 +110,7 @@ class ResearchAccess:
async def refresh_job(self, args):
job = await self.db.get(Job, args.job_id)
if not job or job.kind not in {"catalog_sync", "field_sync", "pnl_refresh"}:
if not job or job.kind not in {"catalog_sync", "field_sync", "pnl_refresh", "self_correlation"}:
raise ResearchError("NOT_FOUND", "研究刷新任务不存在")
result = await self.business.get_job_status(args.job_id)
query = select(JobItem).where(JobItem.job_id == job.id, JobItem.error.is_not(None))
@@ -111,6 +120,27 @@ class ResearchAccess:
return {**result, "job_id": job.id, "artifact_reference": job.payload,
"errors": page([{"alpha_id": e.alpha_id, "error": e.error} for e in errors], total, args.limit, args.offset)}
async def check_self_correlation(self, args):
"""Queue local checks for synced IDs; caller commits before waking the runner.
The shared job service deduplicates active batches. Missing PnL is fetched
by the durable runner, so slow upstream reads do not hold the MCP call.
"""
ids = sorted(set(args.alpha_ids))
existing = set(await self.db.scalars(select(Alpha.id).where(Alpha.id.in_(ids))))
if existing != set(ids):
raise ResearchError("NOT_FOUND", "部分 Alpha 尚未同步,请先导入", affected_items=sorted(set(ids)-existing))
job = await self.business.create_sync_job(JobInput(kind="self_correlation", alpha_ids=ids))
self.wake = "jobs"
return {"job_id": job["id"], "status": job["status"], "alpha_ids": ids,
"source": "local", "job_with": "get_refresh_job", "read_with": "get_self_correlation"}
async def self_correlation(self, args):
"""Read the latest local result without fetching PnL or starting a check."""
data = await self.business.get_self_correlation(args.alpha_id)
status = "not_cached" if not data["cached"] else "stale" if data["result"]["stale"] else "available"
return {"alpha_id": args.alpha_id, "source": "local", "status": status, **data}
async def history(self, args):
return await self.evidence.history(args)