mcp-server-worldquantbrain 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_server_worldquantbrain/__init__.py +11 -0
- mcp_server_worldquantbrain/__main__.py +3 -0
- mcp_server_worldquantbrain/cli.py +127 -0
- mcp_server_worldquantbrain/client/__init__.py +3 -0
- mcp_server_worldquantbrain/client/alphas.py +259 -0
- mcp_server_worldquantbrain/client/base.py +607 -0
- mcp_server_worldquantbrain/client/cache.py +48 -0
- mcp_server_worldquantbrain/client/data.py +103 -0
- mcp_server_worldquantbrain/client/filters.py +60 -0
- mcp_server_worldquantbrain/client/ledger.py +132 -0
- mcp_server_worldquantbrain/client/paging.py +22 -0
- mcp_server_worldquantbrain/client/retry.py +37 -0
- mcp_server_worldquantbrain/client/session.py +107 -0
- mcp_server_worldquantbrain/client/simulations.py +103 -0
- mcp_server_worldquantbrain/client/users.py +77 -0
- mcp_server_worldquantbrain/config.py +185 -0
- mcp_server_worldquantbrain/errors.py +79 -0
- mcp_server_worldquantbrain/http_app.py +50 -0
- mcp_server_worldquantbrain/integrations/__init__.py +0 -0
- mcp_server_worldquantbrain/integrations/langgraph.py +137 -0
- mcp_server_worldquantbrain/knowledge/__init__.py +20 -0
- mcp_server_worldquantbrain/knowledge/fastexpr-cheatsheet.md +36 -0
- mcp_server_worldquantbrain/knowledge/settings-guide.md +20 -0
- mcp_server_worldquantbrain/knowledge/submission-criteria.md +24 -0
- mcp_server_worldquantbrain/logging_setup.py +18 -0
- mcp_server_worldquantbrain/models/__init__.py +0 -0
- mcp_server_worldquantbrain/models/alpha.py +190 -0
- mcp_server_worldquantbrain/models/auth.py +12 -0
- mcp_server_worldquantbrain/models/base.py +23 -0
- mcp_server_worldquantbrain/models/data.py +132 -0
- mcp_server_worldquantbrain/models/simulation.py +117 -0
- mcp_server_worldquantbrain/models/user.py +38 -0
- mcp_server_worldquantbrain/ops/__init__.py +9 -0
- mcp_server_worldquantbrain/ops/alphas.py +200 -0
- mcp_server_worldquantbrain/ops/auth.py +34 -0
- mcp_server_worldquantbrain/ops/data.py +132 -0
- mcp_server_worldquantbrain/ops/registry.py +151 -0
- mcp_server_worldquantbrain/ops/simulations.py +147 -0
- mcp_server_worldquantbrain/ops/submit.py +156 -0
- mcp_server_worldquantbrain/ops/users.py +38 -0
- mcp_server_worldquantbrain/prompts.py +38 -0
- mcp_server_worldquantbrain/py.typed +0 -0
- mcp_server_worldquantbrain/redaction.py +70 -0
- mcp_server_worldquantbrain/resources.py +51 -0
- mcp_server_worldquantbrain/server.py +52 -0
- mcp_server_worldquantbrain/sim_manager.py +467 -0
- mcp_server_worldquantbrain/tools.py +121 -0
- mcp_server_worldquantbrain-0.1.0.dist-info/METADATA +285 -0
- mcp_server_worldquantbrain-0.1.0.dist-info/RECORD +52 -0
- mcp_server_worldquantbrain-0.1.0.dist-info/WHEEL +4 -0
- mcp_server_worldquantbrain-0.1.0.dist-info/entry_points.txt +3 -0
- mcp_server_worldquantbrain-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from mcp_server_worldquantbrain.client import BrainClient
|
|
4
|
+
from mcp_server_worldquantbrain.config import Settings
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("mcp-server-worldquantbrain")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
__version__ = "0.0.0"
|
|
10
|
+
|
|
11
|
+
__all__ = ["BrainClient", "Settings", "__version__"]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import asyncio
|
|
3
|
+
import getpass
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from pydantic import SecretStr, ValidationError
|
|
7
|
+
|
|
8
|
+
from mcp_server_worldquantbrain import __version__
|
|
9
|
+
from mcp_server_worldquantbrain.client.base import BrainClient
|
|
10
|
+
from mcp_server_worldquantbrain.client.session import write_private
|
|
11
|
+
from mcp_server_worldquantbrain.config import Settings
|
|
12
|
+
from mcp_server_worldquantbrain.errors import BrainError
|
|
13
|
+
from mcp_server_worldquantbrain.logging_setup import configure_logging
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _parser() -> argparse.ArgumentParser:
|
|
17
|
+
parser = argparse.ArgumentParser(
|
|
18
|
+
prog="mcp-server-worldquantbrain", description="MCP server for WorldQuant BRAIN."
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
21
|
+
commands = parser.add_subparsers(dest="command")
|
|
22
|
+
|
|
23
|
+
serve = commands.add_parser("serve", help="run the MCP server (default)")
|
|
24
|
+
serve.add_argument("--transport", choices=["stdio", "streamable-http"], default="stdio")
|
|
25
|
+
serve.add_argument("--host", default="127.0.0.1")
|
|
26
|
+
serve.add_argument("--port", type=int, default=8000)
|
|
27
|
+
|
|
28
|
+
auth = commands.add_parser("auth", help="log in and store the session")
|
|
29
|
+
auth.add_argument("--force", action="store_true", help="ignore the saved session")
|
|
30
|
+
|
|
31
|
+
commands.add_parser("status", help="show the session state")
|
|
32
|
+
commands.add_parser("tools", help="list the tools this configuration exposes")
|
|
33
|
+
return parser
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _serve(args: argparse.Namespace, settings: Settings) -> int:
|
|
37
|
+
from mcp_server_worldquantbrain.server import create_server
|
|
38
|
+
|
|
39
|
+
server = create_server(settings)
|
|
40
|
+
if args.transport == "stdio":
|
|
41
|
+
server.run()
|
|
42
|
+
return 0
|
|
43
|
+
import uvicorn
|
|
44
|
+
|
|
45
|
+
from mcp_server_worldquantbrain.http_app import build_http_app
|
|
46
|
+
|
|
47
|
+
app = build_http_app(server, settings, host=args.host)
|
|
48
|
+
uvicorn.run(app, host=args.host, port=args.port, log_config=None)
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _prompt_credentials(settings: Settings) -> None:
|
|
53
|
+
print("No BRAIN credentials found.", file=sys.stderr)
|
|
54
|
+
settings.email = input("Email: ").strip()
|
|
55
|
+
settings.password = SecretStr(getpass.getpass("Password: "))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _store_credentials(settings: Settings) -> None:
|
|
59
|
+
assert settings.email and settings.password
|
|
60
|
+
path = settings.resolved_credentials_file
|
|
61
|
+
write_private(path, [settings.email, settings.password.get_secret_value()])
|
|
62
|
+
print(f"Saved credentials to {path}", file=sys.stderr)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def _auth(args: argparse.Namespace, settings: Settings) -> int:
|
|
66
|
+
prompted = settings.credentials() is None
|
|
67
|
+
if prompted:
|
|
68
|
+
_prompt_credentials(settings)
|
|
69
|
+
async with BrainClient(settings) as client:
|
|
70
|
+
status = await client.authenticate(force=args.force or prompted)
|
|
71
|
+
if prompted and status.state in ("authenticated", "biometric_required"):
|
|
72
|
+
_store_credentials(settings)
|
|
73
|
+
for _ in range(5):
|
|
74
|
+
if status.state != "biometric_required":
|
|
75
|
+
break
|
|
76
|
+
print(f"\nFinish the biometric check in a browser:\n\n {status.biometric_url}\n")
|
|
77
|
+
input("Press Enter when done...")
|
|
78
|
+
status = await client.complete_authentication()
|
|
79
|
+
print(status.model_dump_json(indent=2))
|
|
80
|
+
return 0 if status.state == "authenticated" else 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def _status(settings: Settings) -> int:
|
|
84
|
+
async with BrainClient(settings) as client:
|
|
85
|
+
status = await client.auth_status()
|
|
86
|
+
print(status.model_dump_json(indent=2))
|
|
87
|
+
return 0 if status.state == "authenticated" else 1
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _tools(settings: Settings) -> int:
|
|
91
|
+
from mcp_server_worldquantbrain.ops import enabled_operations
|
|
92
|
+
|
|
93
|
+
operations = enabled_operations(settings)
|
|
94
|
+
width = max(len(op.name) for op in operations)
|
|
95
|
+
for op in operations:
|
|
96
|
+
print(f"{op.name:<{width}} {op.title}")
|
|
97
|
+
if not settings.allow_submit:
|
|
98
|
+
print("\nsubmit_alpha is disabled. Set WQB_ALLOW_SUBMIT=true to enable it.")
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv: list[str] | None = None) -> int:
|
|
103
|
+
args = _parser().parse_args(argv)
|
|
104
|
+
try:
|
|
105
|
+
settings = Settings()
|
|
106
|
+
except ValidationError as exc:
|
|
107
|
+
# Only names and reasons: the input values could be secrets.
|
|
108
|
+
for error in exc.errors():
|
|
109
|
+
name = "_".join(str(part) for part in error["loc"]).upper()
|
|
110
|
+
print(f"Invalid WQB_{name}: {error['msg']}", file=sys.stderr)
|
|
111
|
+
return 2
|
|
112
|
+
configure_logging(settings.log_level)
|
|
113
|
+
if args.command is None:
|
|
114
|
+
args = _parser().parse_args(["serve"])
|
|
115
|
+
try:
|
|
116
|
+
if args.command == "serve":
|
|
117
|
+
return _serve(args, settings)
|
|
118
|
+
if args.command == "auth":
|
|
119
|
+
return asyncio.run(_auth(args, settings))
|
|
120
|
+
if args.command == "status":
|
|
121
|
+
return asyncio.run(_status(settings))
|
|
122
|
+
return _tools(settings)
|
|
123
|
+
except BrainError as exc:
|
|
124
|
+
print(exc.for_agent(), file=sys.stderr)
|
|
125
|
+
return 1
|
|
126
|
+
except (KeyboardInterrupt, EOFError):
|
|
127
|
+
return 130
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
from typing import Any, Literal
|
|
2
|
+
|
|
3
|
+
from mcp_server_worldquantbrain.client.base import BrainClient
|
|
4
|
+
from mcp_server_worldquantbrain.client.paging import page_info, segment
|
|
5
|
+
from mcp_server_worldquantbrain.errors import NotFound, PollTimeout, ServerError
|
|
6
|
+
from mcp_server_worldquantbrain.models.alpha import (
|
|
7
|
+
AlphaPage,
|
|
8
|
+
AlphaSummary,
|
|
9
|
+
Check,
|
|
10
|
+
CheckResult,
|
|
11
|
+
CorrelationResult,
|
|
12
|
+
DatePnl,
|
|
13
|
+
PnlSummary,
|
|
14
|
+
RecordSet,
|
|
15
|
+
RecordSetNames,
|
|
16
|
+
YearPnl,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
TOP_CORRELATIONS = 10
|
|
20
|
+
_TOP_COLUMNS = ("id", "name", "correlation", "sharpe", "fitness", "region", "universe")
|
|
21
|
+
MAX_DAILY_ROWS = 500
|
|
22
|
+
_SETTLED_RESULTS = {"PASS", "WARNING", "FAIL", "ERROR"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def decode_records(payload: Any) -> tuple[list[str], list[dict[str, Any]]]:
|
|
26
|
+
"""Turn BRAIN's positional {schema, records} block into named rows."""
|
|
27
|
+
if not isinstance(payload, dict):
|
|
28
|
+
return [], []
|
|
29
|
+
schema = payload.get("schema")
|
|
30
|
+
properties = schema.get("properties") if isinstance(schema, dict) else None
|
|
31
|
+
columns = [str(column.get("name")) for column in properties or []]
|
|
32
|
+
rows = [
|
|
33
|
+
dict(zip(columns, record, strict=False))
|
|
34
|
+
for record in payload.get("records") or []
|
|
35
|
+
if isinstance(record, list | tuple)
|
|
36
|
+
]
|
|
37
|
+
return columns, rows
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def find_checks(payload: Any) -> list[dict[str, Any]]:
|
|
41
|
+
"""Collect every check in the payload, since a failure may sit in any block."""
|
|
42
|
+
if not isinstance(payload, dict):
|
|
43
|
+
return []
|
|
44
|
+
found: list[dict[str, Any]] = []
|
|
45
|
+
for key, value in payload.items():
|
|
46
|
+
if key == "checks" and isinstance(value, list):
|
|
47
|
+
found.extend(check for check in value if isinstance(check, dict))
|
|
48
|
+
else:
|
|
49
|
+
found.extend(find_checks(value))
|
|
50
|
+
return found
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def get_alpha(client: BrainClient, alpha_id: str, *, max_seconds: float = 60) -> AlphaSummary:
|
|
54
|
+
response = await client.poll(f"/alphas/{segment(alpha_id)}", max_seconds=max_seconds)
|
|
55
|
+
body = response.json()
|
|
56
|
+
if not isinstance(body, dict):
|
|
57
|
+
raise ServerError(f"GET /alphas/{alpha_id} returned an unexpected body.")
|
|
58
|
+
return AlphaSummary.from_api(body)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def list_alphas(client: BrainClient, query: list[tuple[str, Any]]) -> AlphaPage:
|
|
62
|
+
body = await client.request_json("GET", "/users/self/alphas", params=query) or {}
|
|
63
|
+
results = body.get("results") or []
|
|
64
|
+
query_map = dict(query)
|
|
65
|
+
next_offset, truncated = page_info(
|
|
66
|
+
body.get("count"), int(query_map["offset"]), len(results), int(query_map["limit"])
|
|
67
|
+
)
|
|
68
|
+
return AlphaPage(
|
|
69
|
+
count=body.get("count"),
|
|
70
|
+
next_offset=next_offset,
|
|
71
|
+
truncated_by_api=truncated,
|
|
72
|
+
items=[AlphaSummary.from_api(item) for item in results],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def list_recordsets(client: BrainClient, alpha_id: str) -> RecordSetNames:
|
|
77
|
+
body = await client.request_json("GET", f"/alphas/{segment(alpha_id)}/recordsets")
|
|
78
|
+
names: list[str] = []
|
|
79
|
+
entries = body.get("results", body) if isinstance(body, dict) else body
|
|
80
|
+
for entry in entries or []:
|
|
81
|
+
name = entry.get("name") or entry.get("id") if isinstance(entry, dict) else entry
|
|
82
|
+
if name:
|
|
83
|
+
names.append(str(name))
|
|
84
|
+
return RecordSetNames(alpha_id=alpha_id, names=names)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def _recordset_payload(
|
|
88
|
+
client: BrainClient, alpha_id: str, name: str, max_seconds: float
|
|
89
|
+
) -> Any:
|
|
90
|
+
url = f"/alphas/{segment(alpha_id)}/recordsets/{segment(name)}"
|
|
91
|
+
try:
|
|
92
|
+
response = await client.poll(url, max_seconds=max_seconds)
|
|
93
|
+
except NotFound as exc:
|
|
94
|
+
available = await list_recordsets(client, alpha_id)
|
|
95
|
+
exc.hint = f"Available recordsets: {', '.join(available.names) or 'none'}."
|
|
96
|
+
raise
|
|
97
|
+
return response.json()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def get_recordset(
|
|
101
|
+
client: BrainClient, alpha_id: str, name: str, *, max_rows: int, max_seconds: float = 60
|
|
102
|
+
) -> RecordSet:
|
|
103
|
+
columns, rows = decode_records(await _recordset_payload(client, alpha_id, name, max_seconds))
|
|
104
|
+
return RecordSet(
|
|
105
|
+
alpha_id=alpha_id,
|
|
106
|
+
name=name,
|
|
107
|
+
columns=columns,
|
|
108
|
+
rows=rows[-max_rows:],
|
|
109
|
+
total_rows=len(rows),
|
|
110
|
+
truncated=len(rows) > max_rows,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def get_pnl(
|
|
115
|
+
client: BrainClient,
|
|
116
|
+
alpha_id: str,
|
|
117
|
+
*,
|
|
118
|
+
include_daily: bool,
|
|
119
|
+
last_n: int | None,
|
|
120
|
+
max_seconds: float = 60,
|
|
121
|
+
) -> PnlSummary:
|
|
122
|
+
columns, rows = decode_records(await _recordset_payload(client, alpha_id, "pnl", max_seconds))
|
|
123
|
+
date_column = next((c for c in columns if c.lower() == "date"), None)
|
|
124
|
+
pnl_column = next((c for c in columns if "pnl" in c.lower()), None)
|
|
125
|
+
if date_column is None or pnl_column is None:
|
|
126
|
+
return PnlSummary(alpha_id=alpha_id, total_rows=len(rows))
|
|
127
|
+
points = [(str(r[date_column]), r[pnl_column]) for r in rows if r.get(pnl_column) is not None]
|
|
128
|
+
# The pnl recordset is cumulative, so a year's PnL is the change between year-end values.
|
|
129
|
+
year_end: dict[int, float] = {}
|
|
130
|
+
for date, value in points:
|
|
131
|
+
year_end[int(date[:4])] = float(value)
|
|
132
|
+
yearly: list[YearPnl] = []
|
|
133
|
+
previous = 0.0
|
|
134
|
+
for year in sorted(year_end):
|
|
135
|
+
yearly.append(
|
|
136
|
+
YearPnl(year=year, pnl=year_end[year] - previous, cumulative_pnl=year_end[year])
|
|
137
|
+
)
|
|
138
|
+
previous = year_end[year]
|
|
139
|
+
daily = None
|
|
140
|
+
truncated = False
|
|
141
|
+
if include_daily or last_n:
|
|
142
|
+
keep = last_n or MAX_DAILY_ROWS
|
|
143
|
+
daily = [DatePnl(date=d, cumulative_pnl=v) for d, v in points[-keep:]]
|
|
144
|
+
truncated = len(points) > keep
|
|
145
|
+
return PnlSummary(
|
|
146
|
+
alpha_id=alpha_id,
|
|
147
|
+
start_date=points[0][0] if points else None,
|
|
148
|
+
end_date=points[-1][0] if points else None,
|
|
149
|
+
total_pnl=float(points[-1][1]) if points else None,
|
|
150
|
+
total_rows=len(rows),
|
|
151
|
+
yearly=yearly,
|
|
152
|
+
daily=daily,
|
|
153
|
+
truncated=truncated,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
async def update_alpha(client: BrainClient, alpha_id: str, body: dict[str, Any]) -> AlphaSummary:
|
|
158
|
+
data = await client.request_json("PATCH", f"/alphas/{segment(alpha_id)}", json_body=body)
|
|
159
|
+
if isinstance(data, dict) and data.get("id"):
|
|
160
|
+
return AlphaSummary.from_api(data)
|
|
161
|
+
return await get_alpha(client, alpha_id)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def summarize_checks(alpha_id: str, payload: Any) -> CheckResult:
|
|
165
|
+
checks = [Check.model_validate(check) for check in find_checks(payload)]
|
|
166
|
+
failed = [c.name or "" for c in checks if c.result in ("FAIL", "ERROR")]
|
|
167
|
+
warnings = [c.name or "" for c in checks if c.result == "WARNING"]
|
|
168
|
+
# An unknown or missing result is not a pass: treating it as one could let a bad alpha submit.
|
|
169
|
+
unsettled = [c for c in checks if c.result not in _SETTLED_RESULTS]
|
|
170
|
+
# BRAIN leaves correlation checks PENDING here even once it has answered, and the correlation
|
|
171
|
+
# endpoints report them separately, so they do not hold the verdict open.
|
|
172
|
+
correlation_pending = [
|
|
173
|
+
c.name or ""
|
|
174
|
+
for c in unsettled
|
|
175
|
+
if c.result == "PENDING" and (c.name or "").endswith("_CORRELATION")
|
|
176
|
+
]
|
|
177
|
+
pending = not checks or len(unsettled) > len(correlation_pending)
|
|
178
|
+
verdict = "FAIL" if failed else "PENDING" if pending else "WARNING" if warnings else "PASS"
|
|
179
|
+
next_step = None
|
|
180
|
+
if verdict == "PENDING":
|
|
181
|
+
next_step = "Call check_alpha_submission again in a minute."
|
|
182
|
+
elif correlation_pending:
|
|
183
|
+
next_step = "Call get_alpha_self_correlation for the correlation figure."
|
|
184
|
+
return CheckResult(
|
|
185
|
+
alpha_id=alpha_id,
|
|
186
|
+
verdict=verdict,
|
|
187
|
+
checks=checks,
|
|
188
|
+
failed=failed,
|
|
189
|
+
warnings=warnings,
|
|
190
|
+
pending=pending and not failed,
|
|
191
|
+
correlation_pending=correlation_pending,
|
|
192
|
+
next_step=next_step,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def check_submission(
|
|
197
|
+
client: BrainClient, alpha_id: str, *, wait_seconds: float
|
|
198
|
+
) -> CheckResult:
|
|
199
|
+
try:
|
|
200
|
+
response = await client.poll(f"/alphas/{segment(alpha_id)}/check", max_seconds=wait_seconds)
|
|
201
|
+
except PollTimeout:
|
|
202
|
+
return CheckResult(
|
|
203
|
+
alpha_id=alpha_id,
|
|
204
|
+
verdict="PENDING",
|
|
205
|
+
pending=True,
|
|
206
|
+
next_step="BRAIN is still computing the checks. Call check_alpha_submission again.",
|
|
207
|
+
)
|
|
208
|
+
return summarize_checks(alpha_id, response.json())
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
async def get_correlation(
|
|
212
|
+
client: BrainClient,
|
|
213
|
+
alpha_id: str,
|
|
214
|
+
kind: Literal["self", "prod"],
|
|
215
|
+
*,
|
|
216
|
+
wait_seconds: float,
|
|
217
|
+
threshold: float,
|
|
218
|
+
) -> CorrelationResult:
|
|
219
|
+
try:
|
|
220
|
+
response = await client.poll(
|
|
221
|
+
f"/alphas/{segment(alpha_id)}/correlations/{kind}",
|
|
222
|
+
max_seconds=wait_seconds,
|
|
223
|
+
)
|
|
224
|
+
except PollTimeout:
|
|
225
|
+
return CorrelationResult(
|
|
226
|
+
alpha_id=alpha_id,
|
|
227
|
+
kind=kind,
|
|
228
|
+
threshold=threshold,
|
|
229
|
+
pending=True,
|
|
230
|
+
next_step=f"BRAIN is still computing it. Call get_alpha_{kind}_correlation again.",
|
|
231
|
+
)
|
|
232
|
+
payload = response.json()
|
|
233
|
+
_, rows = decode_records(payload)
|
|
234
|
+
highest = payload.get("max") if isinstance(payload, dict) else None
|
|
235
|
+
lowest = payload.get("min") if isinstance(payload, dict) else None
|
|
236
|
+
if kind == "self":
|
|
237
|
+
ranked = sorted(rows, key=lambda row: row.get("correlation") or 0, reverse=True)
|
|
238
|
+
top = [{k: r[k] for k in _TOP_COLUMNS if r.get(k) is not None} for r in ranked]
|
|
239
|
+
if highest is None and ranked:
|
|
240
|
+
highest = ranked[0].get("correlation")
|
|
241
|
+
else:
|
|
242
|
+
top = [row for row in rows if row.get("alphas")][-TOP_CORRELATIONS:]
|
|
243
|
+
if highest is None and top:
|
|
244
|
+
highest = top[-1].get("max")
|
|
245
|
+
passes: bool | None = None
|
|
246
|
+
if highest is not None:
|
|
247
|
+
passes = highest < threshold
|
|
248
|
+
elif isinstance(payload, dict) and payload.get("records") == []:
|
|
249
|
+
passes = True # BRAIN answered with an empty list: nothing to correlate against.
|
|
250
|
+
return CorrelationResult(
|
|
251
|
+
alpha_id=alpha_id,
|
|
252
|
+
kind=kind,
|
|
253
|
+
max_correlation=highest,
|
|
254
|
+
min_correlation=lowest,
|
|
255
|
+
threshold=threshold,
|
|
256
|
+
passes=passes,
|
|
257
|
+
count=len(rows),
|
|
258
|
+
top=top[:TOP_CORRELATIONS],
|
|
259
|
+
)
|