alphaengine 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {alphaengine-0.3.0 → alphaengine-0.4.0}/PKG-INFO +5 -1
- {alphaengine-0.3.0 → alphaengine-0.4.0}/README.md +4 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/_version.py +13 -1
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/cli.py +266 -11
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/client/executor.py +81 -17
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/client/session.py +61 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/commands.py +21 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/signals.py +8 -1
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/study/report.py +6 -1
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_cli.py +168 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_client.py +49 -8
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_smoke.py +7 -2
- {alphaengine-0.3.0 → alphaengine-0.4.0}/.github/workflows/ci.yml +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/.github/workflows/publish.yml +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/.gitignore +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/LICENSE +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/SECURITY.md +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/pyproject.toml +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/scripts/gen_docs.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/__init__.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/__main__.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/agent/__init__.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/agent/answer.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/agent/driver.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/auth.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/client/__init__.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/client/agent.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/__init__.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/backtest.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/factors.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/pairs.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/performance.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/profile.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/risk.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/screen.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/series_shapes.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/stress.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/technical.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/core/validation.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/demo.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/demo_book.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/demo_returns.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/demo_signal.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/demo_universe.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/loaders.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/model.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/py.typed +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/study/__init__.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/study/schema.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/sweep/__init__.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/src/alphaengine/sweep/runner.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_agent.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_answer.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_commands.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_goldens.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_loaders.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_screen.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_signals.py +0 -0
- {alphaengine-0.3.0 → alphaengine-0.4.0}/tests/test_sweep.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alphaengine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Validated research tooling for investment strategies: deflation, overfitting detection, and honest trial counts.
|
|
5
5
|
Project-URL: Homepage, https://github.com/quantOSC/alphaengine
|
|
6
6
|
Project-URL: Documentation, https://github.com/quantOSC/alphaengine#readme
|
|
@@ -149,6 +149,7 @@ distinction is the whole of the data boundary below.
|
|
|
149
149
|
| Command | Does | Where |
|
|
150
150
|
|---|---|---|
|
|
151
151
|
| `demo` | run the built-in example offline, with no account and no data | shell + session |
|
|
152
|
+
| `runs [--limit N]` | your own week: what ran, what it decided, what it filed | shell + session |
|
|
152
153
|
| `workflows` | what the server offers, what each needs, and which reproduce | shell + session |
|
|
153
154
|
| `key [quantos \| anthropic \| openai]` | enter a credential now, or see which rungs are unlocked | session |
|
|
154
155
|
| `commands [verb]` | this directory, or one command in full | shell + session |
|
|
@@ -209,6 +210,7 @@ distinction is the whole of the data boundary below.
|
|
|
209
210
|
| `--label TEXT` | what to call the artifact this run produces |
|
|
210
211
|
| `--input K=V` | a workflow input; repeatable |
|
|
211
212
|
| `--quiet` | only the result, no step narration |
|
|
213
|
+
| `--limit N` | how many rows to show (default 25) |
|
|
212
214
|
|
|
213
215
|
### Examples
|
|
214
216
|
|
|
@@ -222,6 +224,8 @@ alphaengine overlap
|
|
|
222
224
|
alphaengine size
|
|
223
225
|
alphaengine monitor
|
|
224
226
|
alphaengine demo
|
|
227
|
+
alphaengine runs
|
|
228
|
+
alphaengine runs --limit 50
|
|
225
229
|
alphaengine workflows
|
|
226
230
|
alphaengine commands
|
|
227
231
|
alphaengine run screen_universe --universe sp500
|
|
@@ -108,6 +108,7 @@ distinction is the whole of the data boundary below.
|
|
|
108
108
|
| Command | Does | Where |
|
|
109
109
|
|---|---|---|
|
|
110
110
|
| `demo` | run the built-in example offline, with no account and no data | shell + session |
|
|
111
|
+
| `runs [--limit N]` | your own week: what ran, what it decided, what it filed | shell + session |
|
|
111
112
|
| `workflows` | what the server offers, what each needs, and which reproduce | shell + session |
|
|
112
113
|
| `key [quantos \| anthropic \| openai]` | enter a credential now, or see which rungs are unlocked | session |
|
|
113
114
|
| `commands [verb]` | this directory, or one command in full | shell + session |
|
|
@@ -168,6 +169,7 @@ distinction is the whole of the data boundary below.
|
|
|
168
169
|
| `--label TEXT` | what to call the artifact this run produces |
|
|
169
170
|
| `--input K=V` | a workflow input; repeatable |
|
|
170
171
|
| `--quiet` | only the result, no step narration |
|
|
172
|
+
| `--limit N` | how many rows to show (default 25) |
|
|
171
173
|
|
|
172
174
|
### Examples
|
|
173
175
|
|
|
@@ -181,6 +183,8 @@ alphaengine overlap
|
|
|
181
183
|
alphaengine size
|
|
182
184
|
alphaengine monitor
|
|
183
185
|
alphaengine demo
|
|
186
|
+
alphaengine runs
|
|
187
|
+
alphaengine runs --limit 50
|
|
184
188
|
alphaengine workflows
|
|
185
189
|
alphaengine commands
|
|
186
190
|
alphaengine run screen_universe --universe sp500
|
|
@@ -17,4 +17,16 @@ is what a changed figure costs. The API may still move underneath it.
|
|
|
17
17
|
# column, so any grid with a failure read the wrong survivor's column (or
|
|
18
18
|
# raised). Fixing it changes the figure a 0.2 install would have produced from
|
|
19
19
|
# the same inputs — which is exactly what the MINOR position costs here.
|
|
20
|
-
|
|
20
|
+
#
|
|
21
|
+
# 0.4.0 IS THE SAME RULE AGAIN, and it is the harder case to spot because it
|
|
22
|
+
# reads as "we added some charts". The wire's figure cap went 64 -> 512 and the
|
|
23
|
+
# per-period cap in `core.signals` went with it, so the SAME INPUTS NOW PRODUCE
|
|
24
|
+
# DIFFERENT OUTPUT: `ic_by_period` returns up to 512 readings where it returned
|
|
25
|
+
# 64, `best_curve` and `drawdown_curve` carry a curve rather than a sketch of
|
|
26
|
+
# one, a grid of up to 512 configurations records its full surface instead of
|
|
27
|
+
# reporting `trials_recorded: false`, and `compute.overlap` emits a rolling
|
|
28
|
+
# correlation and a scatter it did not emit before.
|
|
29
|
+
#
|
|
30
|
+
# Not one of those is a bug fix, and every one changes what a saved study
|
|
31
|
+
# reproduces. That costs the MINOR position while the leading digit is 0.
|
|
32
|
+
__version__ = "0.4.0"
|
|
@@ -112,6 +112,34 @@ _ENV_KEY = "QUANTOS_API_KEY"
|
|
|
112
112
|
_ENV_URL = "QUANTOS_API_URL"
|
|
113
113
|
DEFAULT_BASE_URL = "https://alpha-backend-production-51df.up.railway.app"
|
|
114
114
|
|
|
115
|
+
#: WHERE A RUN IS READ. The CLI printed a bare run id and left the reader to
|
|
116
|
+
#: work out that `/portal/runs/<id>` exists — and named four screens that had
|
|
117
|
+
#: been redirects for months besides ("Work", "the sleeve page", "your Book",
|
|
118
|
+
#: "accept it in the portal", the last pointing at a surface that does not
|
|
119
|
+
#: exist at all). An id nobody can turn into a URL is a reference to nothing.
|
|
120
|
+
#:
|
|
121
|
+
#: Overridable, and derived from the API host when that has been pointed
|
|
122
|
+
#: somewhere else, so a self-hosted deployment does not send its users to ours.
|
|
123
|
+
DEFAULT_PORTAL_URL = "https://qosai.app"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def portal_url(api_url: str | None = None) -> str:
|
|
127
|
+
"""The portal this account's runs live on.
|
|
128
|
+
|
|
129
|
+
`$QUANTOS_PORTAL_URL` wins. Otherwise the public portal — EXCEPT when the
|
|
130
|
+
API has been pointed at localhost, where sending somebody to the hosted
|
|
131
|
+
portal to read a run that exists only on their machine is the config-default
|
|
132
|
+
failure this codebase has already paid for four times: present, wrong, and
|
|
133
|
+
answering every check.
|
|
134
|
+
"""
|
|
135
|
+
override = os.environ.get("QUANTOS_PORTAL_URL")
|
|
136
|
+
if override:
|
|
137
|
+
return override.rstrip("/")
|
|
138
|
+
host = (api_url or os.environ.get(_ENV_URL) or DEFAULT_BASE_URL).lower()
|
|
139
|
+
if "localhost" in host or "127.0.0.1" in host:
|
|
140
|
+
return "http://localhost:3000"
|
|
141
|
+
return DEFAULT_PORTAL_URL
|
|
142
|
+
|
|
115
143
|
|
|
116
144
|
# ── output ─────────────────────────────────────────────────────────────────
|
|
117
145
|
#
|
|
@@ -549,6 +577,69 @@ def cmd_workflows(args: argparse.Namespace) -> int:
|
|
|
549
577
|
return 0
|
|
550
578
|
|
|
551
579
|
|
|
580
|
+
def cmd_runs(args: argparse.Namespace) -> int:
|
|
581
|
+
"""YOUR OWN WEEK, from where the work happens.
|
|
582
|
+
|
|
583
|
+
The terminal could start a run and could not show you one. "What did I
|
|
584
|
+
already try" was a question you had to open a browser to ask, from the one
|
|
585
|
+
place you are least likely to want to leave — and a quant's week is fifteen
|
|
586
|
+
runs of which most are stops.
|
|
587
|
+
|
|
588
|
+
THE STOPS ARE THE POINT. They are the majority of a week's output and the
|
|
589
|
+
part that evaporates everywhere else, so a stop prints its REASON rather
|
|
590
|
+
than only its token: the sentence is the most useful thing the run produced.
|
|
591
|
+
"""
|
|
592
|
+
from .client import Offline, ServerError
|
|
593
|
+
|
|
594
|
+
session, url = _session(args.url, args.key)
|
|
595
|
+
try:
|
|
596
|
+
rows = session.recent_runs(limit=args.limit)
|
|
597
|
+
except Offline:
|
|
598
|
+
_explain_offline(url)
|
|
599
|
+
return 2
|
|
600
|
+
except ServerError as exc:
|
|
601
|
+
refused(exc)
|
|
602
|
+
if is_auth_error(exc) and offer_key():
|
|
603
|
+
say(dim(" Key set. Run that again."))
|
|
604
|
+
return 2
|
|
605
|
+
|
|
606
|
+
if not rows:
|
|
607
|
+
say("")
|
|
608
|
+
say(dim(" Nothing recorded on this account yet."))
|
|
609
|
+
say(dim(" A run files itself as it goes: ") + "alphaengine screen --universe <name>")
|
|
610
|
+
return 0
|
|
611
|
+
|
|
612
|
+
say("")
|
|
613
|
+
for r in rows:
|
|
614
|
+
when = str(r.get("created_at") or "")[:10]
|
|
615
|
+
kind = str(r.get("kind") or "workflow")
|
|
616
|
+
verdict = str(r.get("verdict") or r.get("status") or "")
|
|
617
|
+
mark = (
|
|
618
|
+
yellow("stopped")
|
|
619
|
+
if verdict == "stopped"
|
|
620
|
+
else green("closed")
|
|
621
|
+
if verdict == "completed"
|
|
622
|
+
else dim(verdict or "open")
|
|
623
|
+
)
|
|
624
|
+
line = f" {dim(when)} {bold(kind.ljust(16))} {mark}"
|
|
625
|
+
# THE DENOMINATOR TRAVELS WITH THE COUNT, here as everywhere. A number
|
|
626
|
+
# that was counted and one that was asserted are different claims.
|
|
627
|
+
if r.get("honest_n") is not None:
|
|
628
|
+
src = str(r.get("n_trials_source") or "unknown").replace("_", " ")
|
|
629
|
+
line += dim(f" n={r['honest_n']} {DOT} {src}")
|
|
630
|
+
filed = r.get("signal_file")
|
|
631
|
+
if isinstance(filed, dict) and filed.get("version"):
|
|
632
|
+
line += dim(f" {DOT} filed v{filed['version']}")
|
|
633
|
+
say(line)
|
|
634
|
+
reason = str(r.get("reason") or "").strip()
|
|
635
|
+
if reason:
|
|
636
|
+
for text in _wrap(reason, 68):
|
|
637
|
+
say(" " + dim(text))
|
|
638
|
+
say("")
|
|
639
|
+
say(dim(f" {len(rows)} shown {DOT} open one in the portal to act on it"))
|
|
640
|
+
return 0
|
|
641
|
+
|
|
642
|
+
|
|
552
643
|
def preflight(catalogue: list[dict[str, Any]], name: str, *, data: Any, backtest_fn: Any) -> str | None:
|
|
553
644
|
"""What is missing before a run starts. `None` when nothing is.
|
|
554
645
|
|
|
@@ -1028,7 +1119,14 @@ def _propose_sleeve(session: Any, run: Any, thesis: dict[str, Any]) -> None:
|
|
|
1028
1119
|
|
|
1029
1120
|
say("")
|
|
1030
1121
|
say(f" {green('Proposed.')} {len(ranked)} names for {bold(title)}")
|
|
1031
|
-
|
|
1122
|
+
# THE PROPOSAL SURFACE WAS RETIRED IN PHASE 8 and this line went on naming
|
|
1123
|
+
# it, so the one instruction printed here pointed at a redirect. Accepting
|
|
1124
|
+
# work happens on the RUN now, which is where its figures, its caveats and
|
|
1125
|
+
# its review pass already are.
|
|
1126
|
+
say(
|
|
1127
|
+
dim(" Nothing exists yet. Accept or decline it on the run: ")
|
|
1128
|
+
+ f"{portal_url()}/portal/runs/{getattr(run, 'run_id', '')}"
|
|
1129
|
+
)
|
|
1032
1130
|
say(dim(f" proposal {filed.get('id')}"))
|
|
1033
1131
|
|
|
1034
1132
|
|
|
@@ -1106,15 +1204,63 @@ def _publish_signals(session: Any, run: Any, *, workspace_id: str | None, label:
|
|
|
1106
1204
|
+ dim(f"{saved.get('label')} {DOT} {saved.get('asof')}")
|
|
1107
1205
|
+ (dim(f" {DOT} v{version}") if version else "")
|
|
1108
1206
|
)
|
|
1109
|
-
|
|
1207
|
+
_say_diff(run)
|
|
1208
|
+
|
|
1209
|
+
|
|
1210
|
+
def _found(before: set[str], run: Any) -> str:
|
|
1211
|
+
"""What the step that just ran FOUND, in the reader's words.
|
|
1212
|
+
|
|
1213
|
+
── THE RULE THAT MAKES THIS SAFE: NARRATION IS RETROSPECTIVE ──────────────
|
|
1214
|
+
|
|
1215
|
+
`protocol.py` is emphatic that the client must never learn the graph — a
|
|
1216
|
+
step names an operation and never a purpose, and "a progress bar is a graph
|
|
1217
|
+
disclosure in a friendly hat". So this may never say what comes NEXT.
|
|
1218
|
+
|
|
1219
|
+
It does not have to. Every word here describes a step that has ALREADY run,
|
|
1220
|
+
and the past is not the graph's future. `executor.execute` returns the
|
|
1221
|
+
figures and `Run.step` records them by op before the round trip, so the
|
|
1222
|
+
client already knows what it just found out, in numbers, as it finds them.
|
|
1223
|
+
|
|
1224
|
+
Only figures this step added, and only ones carrying a LABEL.
|
|
1225
|
+
`_FIGURE_LABELS` is the same vocabulary the closing recap uses — a figure
|
|
1226
|
+
called one thing while it streams and another when it lands is two figures
|
|
1227
|
+
to a reader.
|
|
1228
|
+
"""
|
|
1229
|
+
fresh: dict[str, Any] = {}
|
|
1230
|
+
for op, blob in (getattr(run, "figures", {}) or {}).items():
|
|
1231
|
+
if op in before or not isinstance(blob, dict):
|
|
1232
|
+
continue
|
|
1233
|
+
fresh.update(blob)
|
|
1234
|
+
|
|
1235
|
+
parts: list[str] = []
|
|
1236
|
+
# A STATUS OR A VERDICT OUTRANKS A NUMBER. "the book again" is the entire
|
|
1237
|
+
# result of an overlap run, and printing a correlation instead is how the
|
|
1238
|
+
# one conclusion becomes the one thing left unsaid.
|
|
1239
|
+
for key in ("status", "verdict"):
|
|
1240
|
+
word = fresh.get(key)
|
|
1241
|
+
if isinstance(word, str) and word:
|
|
1242
|
+
parts.append(bold(word.replace("_", " ")))
|
|
1243
|
+
break
|
|
1244
|
+
for key, (label, unit) in _FIGURE_LABELS.items():
|
|
1245
|
+
if len(parts) >= 3: # three is a line; more is a table, and one lands below
|
|
1246
|
+
break
|
|
1247
|
+
v = fresh.get(key)
|
|
1248
|
+
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
|
1249
|
+
parts.append(f"{label} {bold(_num(v))}{(' ' + unit) if unit else ''}")
|
|
1250
|
+
return f" {DOT} ".join(parts)
|
|
1110
1251
|
|
|
1111
1252
|
|
|
1112
1253
|
def _drive(run: Any, *, quiet: bool = False) -> None:
|
|
1113
|
-
"""Execute the run, narrating each step.
|
|
1254
|
+
"""Execute the run, narrating each step BY WHAT IT FOUND.
|
|
1114
1255
|
|
|
1115
1256
|
Wraps `Run.step` rather than calling `drive()` so there is something to
|
|
1116
1257
|
watch. The loop itself — what is permitted, in what order, what stops it —
|
|
1117
1258
|
is still entirely the server's.
|
|
1259
|
+
|
|
1260
|
+
IT USED TO PRINT THE OP NAME AND A DURATION: `server -> compute.screen 1.2s`.
|
|
1261
|
+
That is a progress log. A quant watching it learned which functions fired
|
|
1262
|
+
and never what was being found out — and the process IS the product here, so
|
|
1263
|
+
a run that will not say what it is discovering is showing the wrong half.
|
|
1118
1264
|
"""
|
|
1119
1265
|
n = 0
|
|
1120
1266
|
failures: dict[str, int] = {}
|
|
@@ -1128,6 +1274,7 @@ def _drive(run: Any, *, quiet: bool = False) -> None:
|
|
|
1128
1274
|
for step in batch:
|
|
1129
1275
|
op = step.get("op", "?")
|
|
1130
1276
|
before = run.status
|
|
1277
|
+
seen = set(getattr(run, "figures", {}) or {})
|
|
1131
1278
|
# THE STEP IS WATCHED WHILE IT RUNS, not announced and then silent.
|
|
1132
1279
|
# `compute.*` executes in-process against the caller's own frames, so
|
|
1133
1280
|
# this is the one place in the product where a long wait is entirely
|
|
@@ -1160,7 +1307,12 @@ def _drive(run: Any, *, quiet: bool = False) -> None:
|
|
|
1160
1307
|
}
|
|
1161
1308
|
return
|
|
1162
1309
|
elif not quiet:
|
|
1163
|
-
|
|
1310
|
+
# WHAT IT FOUND, or the op when it produced nothing nameable —
|
|
1311
|
+
# a step with no labelled figure is a real thing (`record.*`,
|
|
1312
|
+
# `emit.*` seal rather than measure) and printing a blank line
|
|
1313
|
+
# for it would read as a stall.
|
|
1314
|
+
found = _found(seen, run)
|
|
1315
|
+
say(f" {dim(ARROW)} {found or bold(op)} {took}")
|
|
1164
1316
|
|
|
1165
1317
|
if run.status != "open":
|
|
1166
1318
|
return
|
|
@@ -1261,6 +1413,7 @@ def _report(run: Any) -> int:
|
|
|
1261
1413
|
if run.status == "closed":
|
|
1262
1414
|
say(green("Closed.") + " " + dim(str((run.artifact or {}).get("workflow", ""))))
|
|
1263
1415
|
_render_artifact(run.artifact or {})
|
|
1416
|
+
_say_gaps(run)
|
|
1264
1417
|
_where_it_lives(run)
|
|
1265
1418
|
_say_next(run)
|
|
1266
1419
|
return 0
|
|
@@ -1268,6 +1421,10 @@ def _report(run: Any) -> int:
|
|
|
1268
1421
|
stop = run.stopped or {}
|
|
1269
1422
|
say(yellow("Stopped.") + " " + str(stop.get("reason", "")))
|
|
1270
1423
|
say(dim(" A stop is a result. The run did what it was built to do."))
|
|
1424
|
+
# A STOP GETS ITS GAPS TOO, and needs them more than a close does: the
|
|
1425
|
+
# run refused to answer, so what it could not cover is most of what
|
|
1426
|
+
# there is to know about it.
|
|
1427
|
+
_say_gaps(run)
|
|
1271
1428
|
_where_it_lives(run)
|
|
1272
1429
|
return 0
|
|
1273
1430
|
if run.status == "abandoned":
|
|
@@ -1284,6 +1441,96 @@ def _report(run: Any) -> int:
|
|
|
1284
1441
|
return 1
|
|
1285
1442
|
|
|
1286
1443
|
|
|
1444
|
+
def _say_diff(run: Any) -> None:
|
|
1445
|
+
"""WHAT MOVED since the last file of the same label.
|
|
1446
|
+
|
|
1447
|
+
Nobody reads a hundred ranked names every morning; they read what changed. A
|
|
1448
|
+
weekly screen's real question is the delta, and until now it was answerable
|
|
1449
|
+
only in a browser — from the one place the person asking it is least likely
|
|
1450
|
+
to want to leave.
|
|
1451
|
+
|
|
1452
|
+
NO PREDECESSOR IS ITS OWN SENTENCE. "First file of this label — nothing to
|
|
1453
|
+
compare against" and "nothing moved" are opposite answers on the one morning
|
|
1454
|
+
somebody is looking hardest, and collapsing them is the failure the
|
|
1455
|
+
`first_file` flag exists to prevent.
|
|
1456
|
+
"""
|
|
1457
|
+
try:
|
|
1458
|
+
diff = run.filed_diff()
|
|
1459
|
+
except Exception: # noqa: BLE001 — a delta is worth losing; a result is not
|
|
1460
|
+
return
|
|
1461
|
+
if not diff:
|
|
1462
|
+
return
|
|
1463
|
+
|
|
1464
|
+
if diff.get("first_file"):
|
|
1465
|
+
say(dim(" First file under this label — nothing to compare against yet."))
|
|
1466
|
+
return
|
|
1467
|
+
|
|
1468
|
+
n_in = int(diff.get("n_entered") or 0)
|
|
1469
|
+
n_out = int(diff.get("n_exited") or 0)
|
|
1470
|
+
n_moved = int(diff.get("n_moved") or 0)
|
|
1471
|
+
n_held = len(diff.get("held") or [])
|
|
1472
|
+
if not (n_in or n_out or n_moved):
|
|
1473
|
+
say(dim(f" Nothing moved against {diff.get('compared_with')} {DOT} {n_held} names, same ranking."))
|
|
1474
|
+
return
|
|
1475
|
+
|
|
1476
|
+
parts = []
|
|
1477
|
+
if n_in:
|
|
1478
|
+
parts.append(green(f"{n_in} in"))
|
|
1479
|
+
if n_out:
|
|
1480
|
+
parts.append(red(f"{n_out} out"))
|
|
1481
|
+
if n_moved:
|
|
1482
|
+
parts.append(f"{n_moved} re-ranked")
|
|
1483
|
+
parts.append(dim(f"{n_held} unchanged"))
|
|
1484
|
+
say(f" {dim('vs ' + str(diff.get('compared_with')))} {DOT} " + f" {DOT} ".join(parts))
|
|
1485
|
+
|
|
1486
|
+
# THE NAMES, not just the counts — a count tells you something happened and
|
|
1487
|
+
# a desk still has to open a browser to find out what.
|
|
1488
|
+
for key, colour, word in (("entered", green, "in"), ("exited", red, "out")):
|
|
1489
|
+
names = [str(r.get("ticker")) for r in (diff.get(key) or []) if r.get("ticker")]
|
|
1490
|
+
if names:
|
|
1491
|
+
shown = ", ".join(names[:12])
|
|
1492
|
+
more = f" +{len(names) - 12} more" if len(names) > 12 else ""
|
|
1493
|
+
say(f" {colour(word)} {dim(shown + more)}")
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
def _say_gaps(run: Any) -> None:
|
|
1497
|
+
"""WHAT THIS RUN DOES NOT TELL YOU, printed where it can still be acted on.
|
|
1498
|
+
|
|
1499
|
+
The quant sees the gap at the moment of the run — before delivering, while
|
|
1500
|
+
closing it is still cheap — and the PM sees the SAME derived object on the
|
|
1501
|
+
run's page at the moment of the decision. One derivation, two readings; that
|
|
1502
|
+
is the whole reason it lives on the server rather than in this file.
|
|
1503
|
+
|
|
1504
|
+
THE VOICE IS THE ONE THIS TERMINAL ALREADY USES FOR MEASUREMENT FACTS:
|
|
1505
|
+
quiet, factual, no colour. A gap is not a warning — it is a statement about
|
|
1506
|
+
what was and was not asked, and amber on "nobody has measured these scores
|
|
1507
|
+
yet" would make a normal Tuesday read as an alarm.
|
|
1508
|
+
|
|
1509
|
+
Never fatal. `Run.gaps()` swallows its own transport failures and returns
|
|
1510
|
+
`[]`, so a server that cannot answer costs the block and not the result.
|
|
1511
|
+
"""
|
|
1512
|
+
try:
|
|
1513
|
+
gaps = run.gaps()
|
|
1514
|
+
except Exception: # noqa: BLE001 — a recap must never take the run down
|
|
1515
|
+
return
|
|
1516
|
+
if not gaps:
|
|
1517
|
+
return
|
|
1518
|
+
|
|
1519
|
+
say("")
|
|
1520
|
+
say(dim(" What this does not tell you"))
|
|
1521
|
+
for g in gaps:
|
|
1522
|
+
says = str(g.get("says") or "").strip()
|
|
1523
|
+
if not says:
|
|
1524
|
+
continue
|
|
1525
|
+
for i, line in enumerate(_wrap(says, 72)):
|
|
1526
|
+
say((" " if i else " " + dim("·") + " ") + dim(line))
|
|
1527
|
+
# THE REMEDY IS A COMMAND, not a suggestion to think about it. A gap
|
|
1528
|
+
# that cannot name one prints nothing here rather than a vague verb.
|
|
1529
|
+
closes = str(g.get("closes_with") or "")
|
|
1530
|
+
if closes:
|
|
1531
|
+
say(" " + dim("closes with ") + f"alphaengine run {closes}")
|
|
1532
|
+
|
|
1533
|
+
|
|
1287
1534
|
def _say_next(run: Any) -> None:
|
|
1288
1535
|
"""The next verb, derived from what the run actually produced.
|
|
1289
1536
|
|
|
@@ -1317,7 +1564,7 @@ def _say_next(run: Any) -> None:
|
|
|
1317
1564
|
elif flat.get("deflated_sharpe") is not None:
|
|
1318
1565
|
line = "find where it breaks: run stress_study --input turnover=<one-way per period>"
|
|
1319
1566
|
elif flat.get("target_weight") is not None:
|
|
1320
|
-
line = "record the fill
|
|
1567
|
+
line = "record the fill on the Portfolio, or take the whole list at once."
|
|
1321
1568
|
elif flat.get("mean_ic") is not None:
|
|
1322
1569
|
line = "it carries information; give it a simulator: run validate_study --project <module>"
|
|
1323
1570
|
elif "usable" in flat:
|
|
@@ -1342,7 +1589,7 @@ def _say_next(run: Any) -> None:
|
|
|
1342
1589
|
else "the data is clean: run screen_universe"
|
|
1343
1590
|
)
|
|
1344
1591
|
elif flat.get("status") == "breached":
|
|
1345
|
-
line = "
|
|
1592
|
+
line = "it crossed a line: the breach is waiting for the desk in the portal."
|
|
1346
1593
|
|
|
1347
1594
|
if line:
|
|
1348
1595
|
say(dim(f" next {DOT} ") + line)
|
|
@@ -1362,11 +1609,10 @@ def _where_it_lives(run: Any) -> None:
|
|
|
1362
1609
|
if not run_id:
|
|
1363
1610
|
return
|
|
1364
1611
|
say("")
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
)
|
|
1612
|
+
# THE URL, NOT THE ID. A reader who has to know that `/portal/runs/<id>`
|
|
1613
|
+
# exists is a reader who does not open it. Most terminals make this
|
|
1614
|
+
# clickable; the ones that do not still let it be copied whole.
|
|
1615
|
+
say(dim(" Recorded ") + f"{portal_url()}/portal/runs/{run_id}")
|
|
1370
1616
|
|
|
1371
1617
|
|
|
1372
1618
|
#: How many shortlist rows the terminal prints before saying how many it kept
|
|
@@ -2862,6 +3108,12 @@ def cmd_repl(args: argparse.Namespace) -> int:
|
|
|
2862
3108
|
if verb == "workflows":
|
|
2863
3109
|
cmd_workflows(args)
|
|
2864
3110
|
continue
|
|
3111
|
+
if verb == "runs":
|
|
3112
|
+
# THE SESSION HAS ONE TOO, and it is the same record. A quant
|
|
3113
|
+
# mid-session asking "what have I already tried" should not have to
|
|
3114
|
+
# leave to find out.
|
|
3115
|
+
cmd_runs(argparse.Namespace(url=args.url, key=args.key, limit=25))
|
|
3116
|
+
continue
|
|
2865
3117
|
if verb == "status":
|
|
2866
3118
|
say(dim("no run yet") if last is None else f"{last.run_id} {last.status}")
|
|
2867
3119
|
continue
|
|
@@ -3076,6 +3328,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3076
3328
|
sub.add_parser("version", parents=[common], help="print the version")
|
|
3077
3329
|
sub.add_parser("workflows", parents=[common], help="list what the server offers")
|
|
3078
3330
|
sub.add_parser("demo", parents=[common], help="run the built-in example, offline")
|
|
3331
|
+
rl = sub.add_parser("runs", parents=[common], help="your own week: what ran, and what it decided")
|
|
3332
|
+
rl.add_argument("--limit", type=int, default=25, help="how many to show (default 25)")
|
|
3079
3333
|
sub.add_parser("logout", parents=[common], help="remove stored credentials")
|
|
3080
3334
|
d = sub.add_parser("commands", parents=[common], help="every command, grouped")
|
|
3081
3335
|
d.add_argument("verb", nargs="?", help="expand one command in full")
|
|
@@ -3143,6 +3397,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
3143
3397
|
handlers: dict[str | None, Callable[[argparse.Namespace], int]] = {
|
|
3144
3398
|
"version": cmd_version,
|
|
3145
3399
|
"workflows": cmd_workflows,
|
|
3400
|
+
"runs": cmd_runs,
|
|
3146
3401
|
"demo": cmd_demo,
|
|
3147
3402
|
"logout": cmd_logout,
|
|
3148
3403
|
"commands": cmd_commands,
|
|
@@ -61,19 +61,26 @@ Figures = dict[str, Any]
|
|
|
61
61
|
Workspace = dict[str, Any]
|
|
62
62
|
Handler = Callable[[Figures, Workspace], Figures]
|
|
63
63
|
|
|
64
|
-
# Mirrors the server's guard. Enforced on
|
|
65
|
-
# leaves" should not depend on the other
|
|
66
|
-
|
|
64
|
+
# Mirrors the server's guard (`harness/vocabulary.MAX_LIST_LEN`). Enforced on
|
|
65
|
+
# our side too, because "the data never leaves" should not depend on the other
|
|
66
|
+
# end remembering to check. THE TWO MUST MOVE TOGETHER: a client cap above the
|
|
67
|
+
# server's turns every long figure into a rejected step, and one below it
|
|
68
|
+
# silently truncates work the server would have accepted.
|
|
69
|
+
MAX_FIGURE_LIST = 512
|
|
67
70
|
|
|
68
71
|
# ── the story figures, bounded by construction ─────────────────────────────
|
|
69
|
-
# A run's detail page can only draw what travels, and what travels
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
# A run's detail page can only draw what travels, and what travels is a DERIVED
|
|
73
|
+
# SUMMARY of the caller's own backtest — never their input series.
|
|
74
|
+
#
|
|
75
|
+
# RAISED 2026-08-08 with the wire (64 -> 512). At the old bound these curves
|
|
76
|
+
# arrived pre-bucketed to 64 points, which is a sketch of a curve rather than
|
|
77
|
+
# the curve: a 500-day equity path collapsed to 64 buckets loses every drawdown
|
|
78
|
+
# shorter than eight sessions, and the drawdowns are the part a reader is
|
|
79
|
+
# looking for. The bucketing still happens HERE, on the machine that holds the
|
|
80
|
+
# data, so a longer history is still summarised rather than shipped whole.
|
|
81
|
+
CURVE_POINTS = 512 # the best configuration's path, and its drawdown
|
|
82
|
+
IC_POINTS = 256 # per-period ICs
|
|
83
|
+
COST_POINTS = 64 # cost rungs — still a handful of levels in practice
|
|
77
84
|
|
|
78
85
|
|
|
79
86
|
def _bucketed(
|
|
@@ -374,11 +381,16 @@ class StepExecutor:
|
|
|
374
381
|
# The parameter surface, per trial — the artifact a sweep exists to
|
|
375
382
|
# produce, and a derived statistic per configuration, so recording it
|
|
376
383
|
# crosses no data boundary. Sent only when the WHOLE grid fits the
|
|
377
|
-
# wire's
|
|
378
|
-
#
|
|
379
|
-
#
|
|
380
|
-
#
|
|
381
|
-
|
|
384
|
+
# wire's cap: a sampled surface would read as the full one, which is
|
|
385
|
+
# exactly the misrepresentation the cap exists to prevent. Failed trials
|
|
386
|
+
# are omitted so a configuration that did not run renders as a hole in
|
|
387
|
+
# the surface, never as a number.
|
|
388
|
+
#
|
|
389
|
+
# BOUND TO THE CAP RATHER THAN REPEATING IT. This was a literal 64 while
|
|
390
|
+
# the cap was 64, so raising one silently left the other — and the
|
|
391
|
+
# symptom would have been a 300-configuration surface still reporting
|
|
392
|
+
# `trials_recorded: false` for no visible reason.
|
|
393
|
+
if result.n_trials <= MAX_FIGURE_LIST:
|
|
382
394
|
figures["trials"] = [
|
|
383
395
|
{**t.params, "sharpe": t.sharpe_annualized} for t in result.trials if t.failed is None
|
|
384
396
|
]
|
|
@@ -659,7 +671,59 @@ class StepExecutor:
|
|
|
659
671
|
"compute.overlap found `book_returns` but no candidate. Put the "
|
|
660
672
|
"idea's own series under `returns`."
|
|
661
673
|
)
|
|
662
|
-
|
|
674
|
+
out = dict(overlap_stats(candidate, book))
|
|
675
|
+
|
|
676
|
+
# THE STORY FIGURES. A correlation and a beta are two numbers standing
|
|
677
|
+
# in for a relationship, and a desk cannot act on the summary alone:
|
|
678
|
+
#
|
|
679
|
+
# the SCATTER is the joint distribution the correlation summarises,
|
|
680
|
+
# and it is where you see that a 0.2 reading is really a cloud plus
|
|
681
|
+
# four shared crashes;
|
|
682
|
+
#
|
|
683
|
+
# ROLLING CORRELATION is whether the reading is stable. An idea that
|
|
684
|
+
# averages 0.2 to the book but runs at 0.9 in every drawdown is the
|
|
685
|
+
# book again exactly when it matters, and the average says the
|
|
686
|
+
# opposite. This is the figure that changes a sizing decision.
|
|
687
|
+
#
|
|
688
|
+
# Both are derived from the caller's own two series, on this machine,
|
|
689
|
+
# and bounded before anything travels.
|
|
690
|
+
if out.get("correlation") is not None:
|
|
691
|
+
c = np.asarray(candidate, dtype=float)
|
|
692
|
+
b = np.asarray(book, dtype=float)
|
|
693
|
+
depth = int(out.get("n_obs") or min(c.size, b.size))
|
|
694
|
+
c, b = c[-depth:], b[-depth:]
|
|
695
|
+
|
|
696
|
+
stride = max(1, depth // CURVE_POINTS)
|
|
697
|
+
out["overlap_scatter"] = [
|
|
698
|
+
{"x": round(float(b[i]), 6), "y": round(float(c[i]), 6)} for i in range(0, depth, stride)
|
|
699
|
+
]
|
|
700
|
+
|
|
701
|
+
# A quarter of the history, floored so the window is a measurement
|
|
702
|
+
# rather than a coincidence, and skipped entirely when the series
|
|
703
|
+
# is too short to roll — a rolling reading over 12 points would be
|
|
704
|
+
# noise drawn as a trend.
|
|
705
|
+
window = max(20, depth // 4)
|
|
706
|
+
if depth >= window * 2:
|
|
707
|
+
rolling: list[float] = []
|
|
708
|
+
for end in range(window, depth + 1):
|
|
709
|
+
cw, bw = c[end - window : end], b[end - window : end]
|
|
710
|
+
sc, sb = float(cw.std(ddof=1)), float(bw.std(ddof=1))
|
|
711
|
+
if sc == 0 or sb == 0:
|
|
712
|
+
rolling.append(float("nan"))
|
|
713
|
+
continue
|
|
714
|
+
cov = float(np.mean((cw - cw.mean()) * (bw - bw.mean())))
|
|
715
|
+
rolling.append(cov / (sc * sb) * window / (window - 1))
|
|
716
|
+
clean = [v for v in rolling if v == v]
|
|
717
|
+
if clean:
|
|
718
|
+
out["rolling_correlation"] = [
|
|
719
|
+
{"i": i + window, "v": round(v, 6)}
|
|
720
|
+
for i, v in _bucketed(rolling, CURVE_POINTS, _last)
|
|
721
|
+
if v == v
|
|
722
|
+
]
|
|
723
|
+
out["rolling_window"] = window
|
|
724
|
+
out["max_rolling_correlation"] = round(max(clean), 6)
|
|
725
|
+
out["min_rolling_correlation"] = round(min(clean), 6)
|
|
726
|
+
return out
|
|
663
727
|
|
|
664
728
|
# ── emit / record ──────────────────────────────────────────────────────
|
|
665
729
|
#
|
|
@@ -133,6 +133,52 @@ class Run:
|
|
|
133
133
|
out = self.session._get(f"/api/harness/runs/{self.run_id}/trace")
|
|
134
134
|
return list(out.get("events") or [])
|
|
135
135
|
|
|
136
|
+
def gaps(self) -> list[Figures]:
|
|
137
|
+
"""WHAT THIS RUN DOES NOT TELL YOU.
|
|
138
|
+
|
|
139
|
+
Derived server-side by a query over the record — which of the eight
|
|
140
|
+
questions were asked, of what data, and when — and never by a model. A
|
|
141
|
+
gap a model was invited to find is one it can decline to find, and worse
|
|
142
|
+
one it can invent. Each gap names the workflow that closes it, which is
|
|
143
|
+
what makes it a next step rather than an observation.
|
|
144
|
+
|
|
145
|
+
NEVER RAISES. A recap that cannot list its gaps is still worth printing;
|
|
146
|
+
one that takes the result down with it is not. `[]` covers both "none
|
|
147
|
+
found" and "could not ask", so the caller that needs to distinguish them
|
|
148
|
+
has to say so itself.
|
|
149
|
+
"""
|
|
150
|
+
try:
|
|
151
|
+
out = self.session._get(f"/api/harness/runs/{self.run_id}/gaps")
|
|
152
|
+
except (Offline, ServerError):
|
|
153
|
+
return []
|
|
154
|
+
return list(out.get("gaps") or [])
|
|
155
|
+
|
|
156
|
+
def filed_diff(self) -> Figures | None:
|
|
157
|
+
"""What moved since the last file of the same label, or None.
|
|
158
|
+
|
|
159
|
+
NOBODY READS A HUNDRED RANKED NAMES EVERY MORNING; they read what moved.
|
|
160
|
+
A weekly screen's real question is the delta, and it was answerable only
|
|
161
|
+
in a browser while the person asking it was in a terminal.
|
|
162
|
+
|
|
163
|
+
`first_file` distinguishes NO PREDECESSOR from NOTHING CHANGED, which
|
|
164
|
+
are opposite answers on the one morning somebody is looking hardest —
|
|
165
|
+
so the caller must not collapse them.
|
|
166
|
+
|
|
167
|
+
None means the run filed nothing, or the diff could not be read. Never
|
|
168
|
+
raises: a delta is worth losing, a result is not.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
filed = self.session._get("/api/me/signals?limit=60")
|
|
172
|
+
except (Offline, ServerError):
|
|
173
|
+
return None
|
|
174
|
+
mine = [f for f in (filed.get("signals") or []) if f.get("source_run_id") == self.run_id]
|
|
175
|
+
if not mine:
|
|
176
|
+
return None
|
|
177
|
+
try:
|
|
178
|
+
return self.session._get(f"/api/me/signals/{mine[0].get('id')}/diff")
|
|
179
|
+
except (Offline, ServerError):
|
|
180
|
+
return None
|
|
181
|
+
|
|
136
182
|
def step(self, step: Figures) -> Figures:
|
|
137
183
|
"""Execute one permitted step locally and report the figures."""
|
|
138
184
|
attempt_id = uuid.uuid4().hex
|
|
@@ -276,6 +322,21 @@ class Session:
|
|
|
276
322
|
"""Names and versions. That is all a client is given, and all it needs."""
|
|
277
323
|
return list(self._get("/api/harness/workflows").get("workflows") or [])
|
|
278
324
|
|
|
325
|
+
def recent_runs(self, limit: int = 25) -> list[Figures]:
|
|
326
|
+
"""YOUR OWN WEEK: what ran, what it decided, what it filed.
|
|
327
|
+
|
|
328
|
+
The terminal could start work and could not show you any of it, so "what
|
|
329
|
+
did I already try" was a question you had to open a browser to ask —
|
|
330
|
+
from the one place you are least likely to want to leave.
|
|
331
|
+
|
|
332
|
+
Rows carry the verdict, the honest trial count with its provenance, and
|
|
333
|
+
the stop reason where there is one. Mostly stops, which is the point:
|
|
334
|
+
the dead ends are the majority of a week's output and the part that
|
|
335
|
+
evaporates everywhere else.
|
|
336
|
+
"""
|
|
337
|
+
out = self._get(f"/api/harness/runs?limit={int(limit)}")
|
|
338
|
+
return list(out.get("runs") or [])
|
|
339
|
+
|
|
279
340
|
def universes(self) -> list[Figures]:
|
|
280
341
|
"""Universes you registered in the portal: names, and the SYMBOLS in them.
|
|
281
342
|
|
|
@@ -92,6 +92,7 @@ FLAGS: dict[str, Flag] = {
|
|
|
92
92
|
"label": Flag("--label", "TEXT", "what to call the artifact this run produces"),
|
|
93
93
|
"input": Flag("--input", "K=V", "a workflow input; repeatable"),
|
|
94
94
|
"quiet": Flag("--quiet", "", "only the result, no step narration"),
|
|
95
|
+
"limit": Flag("--limit", "N", "how many rows to show (default 25)"),
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
|
|
@@ -192,6 +193,26 @@ COMMANDS: tuple[Command, ...] = _question_commands() + (
|
|
|
192
193
|
),
|
|
193
194
|
examples=("alphaengine demo",),
|
|
194
195
|
),
|
|
196
|
+
Command(
|
|
197
|
+
verb="runs",
|
|
198
|
+
group="start",
|
|
199
|
+
args="[--limit N]",
|
|
200
|
+
scope="both",
|
|
201
|
+
purpose="your own week: what ran, what it decided, what it filed",
|
|
202
|
+
body=(
|
|
203
|
+
"The terminal could START work and could not show you any of it, so "
|
|
204
|
+
'"what did I already try" was a question you had to open a browser to '
|
|
205
|
+
"ask, from the one place you are least likely to want to leave.\n\n"
|
|
206
|
+
"MOSTLY STOPS, AND THAT IS THE POINT. The dead ends are the majority "
|
|
207
|
+
"of a research week and the part that evaporates everywhere else, so a "
|
|
208
|
+
"stopped run prints its REASON here rather than only its token: the "
|
|
209
|
+
"sentence is the most useful thing the run produced.\n\n"
|
|
210
|
+
"Every row carries the honest trial count WITH its provenance, because "
|
|
211
|
+
"a count that was derived from a grid that ran and one that was "
|
|
212
|
+
"asserted are different claims about the same number."
|
|
213
|
+
),
|
|
214
|
+
examples=("alphaengine runs", "alphaengine runs --limit 50"),
|
|
215
|
+
),
|
|
195
216
|
Command(
|
|
196
217
|
verb="workflows",
|
|
197
218
|
group="start",
|
|
@@ -41,7 +41,14 @@ import numpy as np
|
|
|
41
41
|
|
|
42
42
|
from .series_shapes import series_values
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
#: The longest per-period sequence a reading reports, and the longest bounded
|
|
45
|
+
#: name list beside it.
|
|
46
|
+
#:
|
|
47
|
+
#: RAISED 64 -> 512 on 2026-08-08 with the wire cap. This one is easy to miss and
|
|
48
|
+
#: silently defeats the others: the core truncates `ic_by_period` HERE, before
|
|
49
|
+
#: `StepExecutor` ever buckets it, so leaving this at 64 would have held IC
|
|
50
|
+
#: through time at 64 points no matter what `IC_POINTS` said.
|
|
51
|
+
MAX_PERIODS = 512
|
|
45
52
|
_RND = 6
|
|
46
53
|
|
|
47
54
|
DEFAULT_HORIZON = 21
|
|
@@ -54,7 +54,12 @@ class ReportError(RuntimeError):
|
|
|
54
54
|
# here rather than imported from `client` so that `alphaengine.study` never
|
|
55
55
|
# pulls the client in — a guarantee `test_client.py` enforces, and one worth
|
|
56
56
|
# more than not repeating fifteen lines.
|
|
57
|
-
|
|
57
|
+
#
|
|
58
|
+
# DUPLICATED ON PURPOSE, WHICH MEANS IT CAN DRIFT BY ACCIDENT. Raised with
|
|
59
|
+
# `client.executor.MAX_FIGURE_LIST` on 2026-08-08; `test_client.py` holds the
|
|
60
|
+
# two equal so the deliberate duplication cannot become an accidental
|
|
61
|
+
# disagreement.
|
|
62
|
+
MAX_FIGURE_LIST = 512
|
|
58
63
|
|
|
59
64
|
|
|
60
65
|
def _guard(payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1322,3 +1322,171 @@ def test_an_unreachable_portal_is_not_an_error_here():
|
|
|
1322
1322
|
raise RuntimeError("offline")
|
|
1323
1323
|
|
|
1324
1324
|
assert cli._universe_named_in("screen my sp100 universe", _Down()) is None
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
# ── the run narrates itself, and never leaks the graph ─────────────────────
|
|
1328
|
+
#
|
|
1329
|
+
# The terminal used to print `server -> compute.screen 1.2s`: which functions
|
|
1330
|
+
# fired, never what was being found out. These hold the replacement to the one
|
|
1331
|
+
# rule that makes it safe — NARRATION IS RETROSPECTIVE. Every line describes a
|
|
1332
|
+
# step that has already run, so the past cannot disclose the graph's future.
|
|
1333
|
+
|
|
1334
|
+
|
|
1335
|
+
class _Narrating:
|
|
1336
|
+
"""A run whose figures have just gained one op's worth of results."""
|
|
1337
|
+
|
|
1338
|
+
def __init__(self, figures):
|
|
1339
|
+
self.figures = figures
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
def test_narration_reports_only_what_the_last_step_added():
|
|
1343
|
+
before = {"compute.profile": {"universe_size": 500}}
|
|
1344
|
+
run = _Narrating({**before, "compute.screen": {"n_evaluated": 431, "n_insufficient": 56}})
|
|
1345
|
+
line = cli._found(set(before), run)
|
|
1346
|
+
assert "431" in line and "56" in line
|
|
1347
|
+
# The earlier step's figure is not restated — it was already narrated.
|
|
1348
|
+
assert "500" not in line
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def test_a_verdict_outranks_a_number():
|
|
1352
|
+
""" "the book again" is the entire result of an overlap run; a correlation
|
|
1353
|
+
printed instead is the one conclusion left unsaid."""
|
|
1354
|
+
run = _Narrating({"compute.overlap": {"correlation": 0.91, "verdict": "the_book_again"}})
|
|
1355
|
+
line = cli._found(set(), run)
|
|
1356
|
+
assert line.index("the book again") < line.index("0.91")
|
|
1357
|
+
|
|
1358
|
+
|
|
1359
|
+
def test_a_status_leads_the_line():
|
|
1360
|
+
run = _Narrating({"compute.monitor": {"status": "breached", "n_obs": 250}})
|
|
1361
|
+
assert cli._found(set(), run).startswith(cli.bold("breached"))
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
def test_narration_never_names_an_op_or_a_stage():
|
|
1365
|
+
"""Rule 1 of the protocol: a step names an OPERATION, and the reader of this
|
|
1366
|
+
line must not learn even that. Stage names are the graph's shape."""
|
|
1367
|
+
run = _Narrating({"compute.screen": {"n_evaluated": 431}})
|
|
1368
|
+
line = cli._found(set(), run)
|
|
1369
|
+
assert "compute" not in line and "screen" not in line
|
|
1370
|
+
|
|
1371
|
+
|
|
1372
|
+
def test_a_step_that_produced_no_labelled_figure_narrates_nothing():
|
|
1373
|
+
"""`emit.*` and `record.*` seal rather than measure. The caller prints the
|
|
1374
|
+
op in that case — a blank line would read as a stall."""
|
|
1375
|
+
run = _Narrating({"emit.study": {"artifact_id": "abc", "sealed": True}})
|
|
1376
|
+
assert cli._found(set(), run) == ""
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def test_narration_is_bounded_to_a_line():
|
|
1380
|
+
run = _Narrating({"compute.all": {k: 1.0 for k in list(cli._FIGURE_LABELS)[:9]}})
|
|
1381
|
+
assert cli._found(set(), run).count(cli.DOT) <= 2 # three parts, two separators
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
# ── the gaps block ─────────────────────────────────────────────────────────
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
def test_the_gap_block_prints_the_sentence_and_the_command(capsys):
|
|
1388
|
+
class _R:
|
|
1389
|
+
def gaps(self):
|
|
1390
|
+
return [
|
|
1391
|
+
{
|
|
1392
|
+
"code": "scores_never_measured",
|
|
1393
|
+
"kind": "sequence",
|
|
1394
|
+
"says": "These names are ranked by a score nothing has measured.",
|
|
1395
|
+
"closes_with": "evaluate_signal",
|
|
1396
|
+
}
|
|
1397
|
+
]
|
|
1398
|
+
|
|
1399
|
+
cli._say_gaps(_R())
|
|
1400
|
+
out = capsys.readouterr().out
|
|
1401
|
+
assert "What this does not tell you" in out
|
|
1402
|
+
assert "nothing has measured" in out
|
|
1403
|
+
# THE REMEDY IS A COMMAND, not a suggestion to think about it.
|
|
1404
|
+
assert "alphaengine run evaluate_signal" in out
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def test_a_gap_with_no_remedy_prints_no_command(capsys):
|
|
1408
|
+
class _R:
|
|
1409
|
+
def gaps(self):
|
|
1410
|
+
return [
|
|
1411
|
+
{
|
|
1412
|
+
"code": "engine_moved_on",
|
|
1413
|
+
"kind": "provenance",
|
|
1414
|
+
"says": "Computed on an older engine.",
|
|
1415
|
+
"closes_with": "",
|
|
1416
|
+
}
|
|
1417
|
+
]
|
|
1418
|
+
|
|
1419
|
+
cli._say_gaps(_R())
|
|
1420
|
+
out = capsys.readouterr().out
|
|
1421
|
+
assert "older engine" in out and "alphaengine run" not in out
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def test_no_gaps_prints_no_heading(capsys):
|
|
1425
|
+
class _R:
|
|
1426
|
+
def gaps(self):
|
|
1427
|
+
return []
|
|
1428
|
+
|
|
1429
|
+
cli._say_gaps(_R())
|
|
1430
|
+
assert capsys.readouterr().out == ""
|
|
1431
|
+
|
|
1432
|
+
|
|
1433
|
+
def test_a_recap_never_takes_the_run_down(capsys):
|
|
1434
|
+
"""The result is already on screen; losing the gap block costs a block."""
|
|
1435
|
+
|
|
1436
|
+
class _R:
|
|
1437
|
+
def gaps(self):
|
|
1438
|
+
raise RuntimeError("the server is having a day")
|
|
1439
|
+
|
|
1440
|
+
cli._say_gaps(_R())
|
|
1441
|
+
assert capsys.readouterr().out == ""
|
|
1442
|
+
|
|
1443
|
+
|
|
1444
|
+
# ── the portal bridge points at addresses that exist ───────────────────────
|
|
1445
|
+
#
|
|
1446
|
+
# The CLI printed a bare run id and named four screens that had been redirects
|
|
1447
|
+
# for months: "Work", "the sleeve page", "your Book", and — worst — "accept it
|
|
1448
|
+
# in the portal", pointing at a proposal surface that no longer exists at all.
|
|
1449
|
+
# An id nobody can turn into a URL is a reference to nothing.
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def test_a_run_is_printed_as_a_url_not_a_bare_id(capsys, monkeypatch):
|
|
1453
|
+
monkeypatch.delenv("QUANTOS_PORTAL_URL", raising=False)
|
|
1454
|
+
monkeypatch.delenv("QUANTOS_API_URL", raising=False)
|
|
1455
|
+
|
|
1456
|
+
class _R:
|
|
1457
|
+
run_id = "run_abc123"
|
|
1458
|
+
|
|
1459
|
+
cli._where_it_lives(_R())
|
|
1460
|
+
out = capsys.readouterr().out
|
|
1461
|
+
assert "https://qosai.app/portal/runs/run_abc123" in out
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def test_the_portal_follows_the_api_to_localhost(monkeypatch):
|
|
1465
|
+
"""A CONFIG DEFAULT THAT IS PRESENT AND WRONG is worse than one that is
|
|
1466
|
+
missing. Sending somebody to the hosted portal to read a run that exists
|
|
1467
|
+
only on their machine is that failure exactly."""
|
|
1468
|
+
monkeypatch.delenv("QUANTOS_PORTAL_URL", raising=False)
|
|
1469
|
+
monkeypatch.setenv("QUANTOS_API_URL", "http://localhost:8000")
|
|
1470
|
+
assert cli.portal_url() == "http://localhost:3000"
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
def test_the_portal_url_is_overridable(monkeypatch):
|
|
1474
|
+
"""A self-hosted deployment must not send its users to ours."""
|
|
1475
|
+
monkeypatch.setenv("QUANTOS_PORTAL_URL", "https://research.example.com/")
|
|
1476
|
+
assert cli.portal_url() == "https://research.example.com"
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
def test_no_line_names_a_screen_that_is_a_redirect():
|
|
1480
|
+
"""Four names that had been redirects since Phase 8. This asserts the
|
|
1481
|
+
SOURCE rather than one code path, because the next one to go stale will be
|
|
1482
|
+
somewhere none of these tests happens to reach."""
|
|
1483
|
+
import pathlib
|
|
1484
|
+
|
|
1485
|
+
source = pathlib.Path(cli.__file__).read_text(encoding="utf-8")
|
|
1486
|
+
for gone in (
|
|
1487
|
+
"Open Work in the portal",
|
|
1488
|
+
"the sleeve page shows",
|
|
1489
|
+
"against your Book in the portal",
|
|
1490
|
+
"Open it in the portal to accept",
|
|
1491
|
+
):
|
|
1492
|
+
assert gone not in source, gone
|
|
@@ -93,12 +93,16 @@ def test_sweep_records_the_parameter_surface_when_the_grid_fits():
|
|
|
93
93
|
|
|
94
94
|
|
|
95
95
|
def test_an_oversized_grid_records_the_absence_not_a_sample():
|
|
96
|
+
"""A sampled surface would read as the full one, which is the whole reason
|
|
97
|
+
the cap exists. Sized off MAX_FIGURE_LIST — at 130 trials against a literal
|
|
98
|
+
this test silently inverted when the cap went to 512 and began asserting
|
|
99
|
+
that an oversized grid IS recorded."""
|
|
100
|
+
fast = list(range(2, 15)) # 13
|
|
101
|
+
slow = list(range(20, 20 + 2 * (MAX_FIGURE_LIST // len(fast) + 2), 2))
|
|
96
102
|
ex = executor()
|
|
97
|
-
out = ex.execute(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
assert out["n_trials"] == 130
|
|
103
|
+
out = ex.execute("compute.sweep", {"grid": {"fast": fast, "slow": slow}})
|
|
104
|
+
|
|
105
|
+
assert out["n_trials"] == len(fast) * len(slow) > MAX_FIGURE_LIST
|
|
102
106
|
assert "trials" not in out
|
|
103
107
|
assert out["trials_recorded"] is False
|
|
104
108
|
|
|
@@ -143,14 +147,23 @@ def test_a_handler_can_be_supplied_for_ops_only_your_environment_answers():
|
|
|
143
147
|
# ── the data does not leave, enforced on this side too ─────────────────────
|
|
144
148
|
def test_the_executor_refuses_to_send_a_series():
|
|
145
149
|
"""Enforced client side as well as server side. 'The data stays put' should
|
|
146
|
-
not depend on the other end remembering to check.
|
|
147
|
-
|
|
150
|
+
not depend on the other end remembering to check.
|
|
151
|
+
|
|
152
|
+
SIZED OFF THE CAP, NOT OFF A LITERAL. These fixtures were 500 and 200 while
|
|
153
|
+
the cap was 64; raising it to 512 made both of them legal payloads, and both
|
|
154
|
+
tests went green while asserting nothing at all. A guard test whose fixture
|
|
155
|
+
does not move with the threshold stops testing the guard on the day the
|
|
156
|
+
threshold moves — which is the one day you want it to fail.
|
|
157
|
+
"""
|
|
158
|
+
over = [1.0] * (MAX_FIGURE_LIST + 1)
|
|
159
|
+
ex = StepExecutor(data=prices(), handlers={"data.describe": lambda p, w: {"px": over}})
|
|
148
160
|
with pytest.raises(ValueError, match="stays on your machine"):
|
|
149
161
|
ex.execute("data.describe", {})
|
|
150
162
|
|
|
151
163
|
|
|
152
164
|
def test_a_series_is_caught_however_it_is_nested():
|
|
153
|
-
|
|
165
|
+
nested = list(range(MAX_FIGURE_LIST + 1))
|
|
166
|
+
ex = StepExecutor(data=prices(), handlers={"data.describe": lambda p, w: {"a": {"b": nested}}})
|
|
154
167
|
with pytest.raises(ValueError, match="series"):
|
|
155
168
|
ex.execute("data.describe", {})
|
|
156
169
|
|
|
@@ -709,3 +722,31 @@ def test_cost_ladder_emits_the_curve_only_when_it_was_measured():
|
|
|
709
722
|
# ABSENCE, never a flat line at zero.
|
|
710
723
|
bare = ex.execute("compute.cost_ladder", {})
|
|
711
724
|
assert "cost_curve" not in bare
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def test_the_two_figure_caps_are_equal_even_though_they_are_duplicated():
|
|
728
|
+
"""`study/report.py` repeats `MAX_FIGURE_LIST` rather than importing it, so
|
|
729
|
+
that `alphaengine.study` never pulls the client in — a real guarantee, and
|
|
730
|
+
the reason the duplication exists.
|
|
731
|
+
|
|
732
|
+
Deliberate duplication still drifts by accident. Both were 64; the wire cap
|
|
733
|
+
moved to 512 on 2026-08-08 and a report path left behind would have refused
|
|
734
|
+
exactly the curves the change exists to allow, from the one code path that
|
|
735
|
+
runs with no server to correct it.
|
|
736
|
+
"""
|
|
737
|
+
from alphaengine.study.report import MAX_FIGURE_LIST as REPORT_CAP
|
|
738
|
+
|
|
739
|
+
assert MAX_FIGURE_LIST == REPORT_CAP
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def test_the_core_does_not_truncate_a_sequence_before_the_executor_buckets_it():
|
|
743
|
+
"""MAX_PERIODS is upstream of every story figure and silently outranks them.
|
|
744
|
+
|
|
745
|
+
`information_coefficient` truncates `ic_by_period` to MAX_PERIODS before
|
|
746
|
+
`StepExecutor` ever sees it, so a core cap below `IC_POINTS` holds the chart
|
|
747
|
+
at the core's number and nothing in the executor says why.
|
|
748
|
+
"""
|
|
749
|
+
from alphaengine.core.signals import MAX_PERIODS
|
|
750
|
+
|
|
751
|
+
assert MAX_PERIODS >= IC_POINTS
|
|
752
|
+
assert MAX_PERIODS <= MAX_FIGURE_LIST
|
|
@@ -69,15 +69,20 @@ def test_report_refuses_a_series_before_it_leaves_the_machine():
|
|
|
69
69
|
series hidden under any key at all. It is duplicated on the server on
|
|
70
70
|
purpose: a check that runs only on the client is not a check, and one that
|
|
71
71
|
runs only on the server tells you too late and without naming the field.
|
|
72
|
+
|
|
73
|
+
SIZED OFF THE CAP. The fixture was 500 elements against a cap of 64; when
|
|
74
|
+
the cap moved to 512 the payload became legal and this test would have gone
|
|
75
|
+
on passing while asserting that the guard does nothing.
|
|
72
76
|
"""
|
|
73
77
|
import pytest
|
|
74
78
|
|
|
75
|
-
from alphaengine.study.report import _guard
|
|
79
|
+
from alphaengine.study.report import MAX_FIGURE_LIST, _guard
|
|
76
80
|
|
|
77
81
|
_guard({"performance": {"sharpe": 1.2}}) # figures pass
|
|
78
82
|
|
|
83
|
+
over = list(range(MAX_FIGURE_LIST + 1))
|
|
79
84
|
with pytest.raises(ValueError, match="is a series"):
|
|
80
|
-
_guard({"performance": {"totally_not_returns":
|
|
85
|
+
_guard({"performance": {"totally_not_returns": over}})
|
|
81
86
|
|
|
82
87
|
|
|
83
88
|
def test_report_needs_a_key_and_says_so():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|