alphaengine 0.4.0__tar.gz → 0.5.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.4.0 → alphaengine-0.5.0}/PKG-INFO +7 -3
- {alphaengine-0.4.0 → alphaengine-0.5.0}/README.md +6 -2
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/_version.py +16 -1
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/cli.py +137 -190
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/client/session.py +30 -57
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/commands.py +38 -2
- {alphaengine-0.4.0 → alphaengine-0.5.0}/.github/workflows/ci.yml +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/.github/workflows/publish.yml +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/.gitignore +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/LICENSE +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/SECURITY.md +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/pyproject.toml +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/scripts/gen_docs.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/__main__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/agent/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/agent/answer.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/agent/driver.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/auth.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/client/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/client/agent.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/client/executor.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/backtest.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/factors.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/pairs.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/performance.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/profile.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/risk.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/screen.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/series_shapes.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/signals.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/stress.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/technical.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/core/validation.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/demo.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/demo_book.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/demo_returns.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/demo_signal.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/demo_universe.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/loaders.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/model.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/py.typed +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/study/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/study/report.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/study/schema.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/sweep/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/src/alphaengine/sweep/runner.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/tests/test_agent.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/tests/test_answer.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/tests/test_cli.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/tests/test_client.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/tests/test_commands.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/tests/test_goldens.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/tests/test_loaders.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/tests/test_screen.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/tests/test_signals.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.0}/tests/test_smoke.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.5.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.5.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
|
|
@@ -150,6 +150,8 @@ distinction is the whole of the data boundary below.
|
|
|
150
150
|
|---|---|---|
|
|
151
151
|
| `demo` | run the built-in example offline, with no account and no data | shell + session |
|
|
152
152
|
| `runs [--limit N]` | your own week: what ran, what it decided, what it filed | shell + session |
|
|
153
|
+
| `gaps` | what your record says is UNANSWERED, and what closes each one | shell + session |
|
|
154
|
+
| `tonight [--budget N]` | what would run unattended tonight, without running any of it | shell + session |
|
|
153
155
|
| `workflows` | what the server offers, what each needs, and which reproduce | shell + session |
|
|
154
156
|
| `key [quantos \| anthropic \| openai]` | enter a credential now, or see which rungs are unlocked | session |
|
|
155
157
|
| `commands [verb]` | this directory, or one command in full | shell + session |
|
|
@@ -205,12 +207,11 @@ distinction is the whole of the data boundary below.
|
|
|
205
207
|
| `--data FILE` | a local CSV: wide, long, or a single series |
|
|
206
208
|
| `--universe NAME` | a universe registered in the portal, with its stored closes |
|
|
207
209
|
| `--symbol TICKER` | one name out of a loaded universe; its closes become the return series |
|
|
208
|
-
| `--sleeve NAME` | the sleeve this run belongs to, and the budget it is bound by |
|
|
209
|
-
| `--thesis ID` | a draft thesis to propose this run's shortlist as a sleeve for |
|
|
210
210
|
| `--label TEXT` | what to call the artifact this run produces |
|
|
211
211
|
| `--input K=V` | a workflow input; repeatable |
|
|
212
212
|
| `--quiet` | only the result, no step narration |
|
|
213
213
|
| `--limit N` | how many rows to show (default 25) |
|
|
214
|
+
| `--budget N` | how many runs a night is worth (default 3) |
|
|
214
215
|
|
|
215
216
|
### Examples
|
|
216
217
|
|
|
@@ -226,6 +227,9 @@ alphaengine monitor
|
|
|
226
227
|
alphaengine demo
|
|
227
228
|
alphaengine runs
|
|
228
229
|
alphaengine runs --limit 50
|
|
230
|
+
alphaengine gaps
|
|
231
|
+
alphaengine tonight
|
|
232
|
+
alphaengine tonight --budget 5
|
|
229
233
|
alphaengine workflows
|
|
230
234
|
alphaengine commands
|
|
231
235
|
alphaengine run screen_universe --universe sp500
|
|
@@ -109,6 +109,8 @@ distinction is the whole of the data boundary below.
|
|
|
109
109
|
|---|---|---|
|
|
110
110
|
| `demo` | run the built-in example offline, with no account and no data | shell + session |
|
|
111
111
|
| `runs [--limit N]` | your own week: what ran, what it decided, what it filed | shell + session |
|
|
112
|
+
| `gaps` | what your record says is UNANSWERED, and what closes each one | shell + session |
|
|
113
|
+
| `tonight [--budget N]` | what would run unattended tonight, without running any of it | shell + session |
|
|
112
114
|
| `workflows` | what the server offers, what each needs, and which reproduce | shell + session |
|
|
113
115
|
| `key [quantos \| anthropic \| openai]` | enter a credential now, or see which rungs are unlocked | session |
|
|
114
116
|
| `commands [verb]` | this directory, or one command in full | shell + session |
|
|
@@ -164,12 +166,11 @@ distinction is the whole of the data boundary below.
|
|
|
164
166
|
| `--data FILE` | a local CSV: wide, long, or a single series |
|
|
165
167
|
| `--universe NAME` | a universe registered in the portal, with its stored closes |
|
|
166
168
|
| `--symbol TICKER` | one name out of a loaded universe; its closes become the return series |
|
|
167
|
-
| `--sleeve NAME` | the sleeve this run belongs to, and the budget it is bound by |
|
|
168
|
-
| `--thesis ID` | a draft thesis to propose this run's shortlist as a sleeve for |
|
|
169
169
|
| `--label TEXT` | what to call the artifact this run produces |
|
|
170
170
|
| `--input K=V` | a workflow input; repeatable |
|
|
171
171
|
| `--quiet` | only the result, no step narration |
|
|
172
172
|
| `--limit N` | how many rows to show (default 25) |
|
|
173
|
+
| `--budget N` | how many runs a night is worth (default 3) |
|
|
173
174
|
|
|
174
175
|
### Examples
|
|
175
176
|
|
|
@@ -185,6 +186,9 @@ alphaengine monitor
|
|
|
185
186
|
alphaengine demo
|
|
186
187
|
alphaengine runs
|
|
187
188
|
alphaengine runs --limit 50
|
|
189
|
+
alphaengine gaps
|
|
190
|
+
alphaengine tonight
|
|
191
|
+
alphaengine tonight --budget 5
|
|
188
192
|
alphaengine workflows
|
|
189
193
|
alphaengine commands
|
|
190
194
|
alphaengine run screen_universe --universe sp500
|
|
@@ -29,4 +29,19 @@ is what a changed figure costs. The API may still move underneath it.
|
|
|
29
29
|
#
|
|
30
30
|
# Not one of those is a bug fix, and every one changes what a saved study
|
|
31
31
|
# reproduces. That costs the MINOR position while the leading digit is 0.
|
|
32
|
-
|
|
32
|
+
#
|
|
33
|
+
# 0.5.0 IS THE RULE READ THE OTHER WAY: no computed value moved, and the SURFACE
|
|
34
|
+
# broke. `--sleeve` and `--thesis` are removed along with `Session.sleeves()`,
|
|
35
|
+
# `.theses()` and `.propose_sleeve()`. Their three routes were part of the thesis
|
|
36
|
+
# object model, which came out of the platform with the agent desk it was built
|
|
37
|
+
# around, so the flags could only have failed at the END of a run — after the
|
|
38
|
+
# work, naming a server error rather than a missing feature.
|
|
39
|
+
#
|
|
40
|
+
# A removed flag is a breaking change even though every figure is byte-identical,
|
|
41
|
+
# and while the leading digit is 0 the MINOR position carries that too.
|
|
42
|
+
#
|
|
43
|
+
# Added in the same release, and the reason the removal is not a loss: `gaps` and
|
|
44
|
+
# `tonight`. The record already knew which workflow closes each open gap and
|
|
45
|
+
# nothing traversed those edges; these two read that derivation, so the terminal
|
|
46
|
+
# can now answer "what have I NOT tried" and "what will run while I sleep".
|
|
47
|
+
__version__ = "0.5.0"
|
|
@@ -227,6 +227,11 @@ QUESTION_VERBS: dict[str, str] = {
|
|
|
227
227
|
"size": "size_position",
|
|
228
228
|
"monitor": "monitor_sleeve",
|
|
229
229
|
}
|
|
230
|
+
#: The same table read backwards, so a gap naming a WORKFLOW can print the verb
|
|
231
|
+
#: a person actually types. Derived rather than written out twice, because two
|
|
232
|
+
#: hand-maintained copies of this map is precisely the drift `commands.py`
|
|
233
|
+
#: exists to prevent.
|
|
234
|
+
QUESTION_FOR_WORKFLOW: dict[str, str] = {w: v for v, w in QUESTION_VERBS.items()}
|
|
230
235
|
#: The held-back-rows marker. Probed like the rest: cp1252 renders a literal
|
|
231
236
|
#: ellipsis as mojibake in exactly the place a user reads their result.
|
|
232
237
|
ELLIPSIS = "…" if _UNICODE_OK else "..."
|
|
@@ -577,6 +582,105 @@ def cmd_workflows(args: argparse.Namespace) -> int:
|
|
|
577
582
|
return 0
|
|
578
583
|
|
|
579
584
|
|
|
585
|
+
def cmd_gaps(args: argparse.Namespace) -> int:
|
|
586
|
+
"""WHAT THE RECORD SAYS IS UNANSWERED, and what closes each one.
|
|
587
|
+
|
|
588
|
+
`runs` shows what you already tried. This shows what you have NOT — the
|
|
589
|
+
other half of the same question, and the one you want at 9am rather than at
|
|
590
|
+
the end of a task.
|
|
591
|
+
|
|
592
|
+
Every line names its remedy, because a gap that cannot name one is an
|
|
593
|
+
observation rather than a next step. Both the gap and the remedy are
|
|
594
|
+
DERIVED from the record: a model asked to find gaps can decline to find
|
|
595
|
+
them, and can invent them.
|
|
596
|
+
"""
|
|
597
|
+
from .client import Offline, ServerError
|
|
598
|
+
|
|
599
|
+
session, url = _session(args.url, args.key)
|
|
600
|
+
try:
|
|
601
|
+
out = session.gaps()
|
|
602
|
+
except Offline:
|
|
603
|
+
_explain_offline(url)
|
|
604
|
+
return 2
|
|
605
|
+
except ServerError as exc:
|
|
606
|
+
refused(exc)
|
|
607
|
+
if is_auth_error(exc) and offer_key():
|
|
608
|
+
say(dim(" Key set. Run that again."))
|
|
609
|
+
return 2
|
|
610
|
+
|
|
611
|
+
rows = list(out.get("queued") or []) + list(out.get("skipped") or [])
|
|
612
|
+
if not rows:
|
|
613
|
+
say("")
|
|
614
|
+
reason = str(out.get("reason") or "")
|
|
615
|
+
say(dim(f" {reason}") if reason else dim(" Nothing open. The record answers what it was asked."))
|
|
616
|
+
return 0
|
|
617
|
+
|
|
618
|
+
say("")
|
|
619
|
+
say(f" {bold(str(len(rows)))} open on this account")
|
|
620
|
+
for r in rows:
|
|
621
|
+
workflow = str(r.get("workflow") or "")
|
|
622
|
+
# THE SENTENCE IS THE FINDING. Printed whole rather than clipped to a
|
|
623
|
+
# label, because "nobody diagnosed this data before the run read it" is
|
|
624
|
+
# what tells somebody what to do and "data_never_diagnosed" is not.
|
|
625
|
+
say(f" {dim(DOT)} {str(r.get('because') or r.get('code') or '').strip()}")
|
|
626
|
+
blocked = r.get("reason")
|
|
627
|
+
if blocked:
|
|
628
|
+
say(f" {yellow('blocked')} {dim(str(blocked))}")
|
|
629
|
+
else:
|
|
630
|
+
say(f" {dim('->')} alphaengine {QUESTION_FOR_WORKFLOW.get(workflow, workflow)}")
|
|
631
|
+
say("")
|
|
632
|
+
return 0
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def cmd_tonight(args: argparse.Namespace) -> int:
|
|
636
|
+
"""WHAT WOULD RUN TONIGHT, without running any of it.
|
|
637
|
+
|
|
638
|
+
The same derivation the scheduler uses, so this is what WILL happen rather
|
|
639
|
+
than a description of it.
|
|
640
|
+
|
|
641
|
+
Everything the budget cut is printed too. A plan that shows only what it
|
|
642
|
+
kept reads as "this is everything that mattered", which is the one thing a
|
|
643
|
+
plan must never imply.
|
|
644
|
+
"""
|
|
645
|
+
from .client import Offline, ServerError
|
|
646
|
+
|
|
647
|
+
session, url = _session(args.url, args.key)
|
|
648
|
+
try:
|
|
649
|
+
out = session.plan(getattr(args, "budget", 0) or 0)
|
|
650
|
+
except Offline:
|
|
651
|
+
_explain_offline(url)
|
|
652
|
+
return 2
|
|
653
|
+
except ServerError as exc:
|
|
654
|
+
refused(exc)
|
|
655
|
+
if is_auth_error(exc) and offer_key():
|
|
656
|
+
say(dim(" Key set. Run that again."))
|
|
657
|
+
return 2
|
|
658
|
+
|
|
659
|
+
queued = list(out.get("queued") or [])
|
|
660
|
+
skipped = list(out.get("skipped") or [])
|
|
661
|
+
if not queued and not skipped:
|
|
662
|
+
say("")
|
|
663
|
+
reason = str(out.get("reason") or "")
|
|
664
|
+
say(dim(f" {reason}") if reason else dim(" Nothing queued. The record has no open gap to close."))
|
|
665
|
+
return 0
|
|
666
|
+
|
|
667
|
+
say("")
|
|
668
|
+
if queued:
|
|
669
|
+
say(f" {bold('queued')} {dim(f'{len(queued)} of tonight')}")
|
|
670
|
+
for q in queued:
|
|
671
|
+
inputs = q.get("inputs") or {}
|
|
672
|
+
on = str(inputs.get("universe") or inputs.get("symbol") or "")
|
|
673
|
+
say(f" {green(DOT)} {str(q.get('workflow') or '').ljust(18)}{dim(on)}")
|
|
674
|
+
say(f" {dim(str(q.get('because') or ''))}")
|
|
675
|
+
if skipped:
|
|
676
|
+
say("")
|
|
677
|
+
say(f" {dim('skipped')}")
|
|
678
|
+
for s in skipped:
|
|
679
|
+
say(f" {dim(DOT)} {str(s.get('workflow') or '').ljust(18)}{dim(str(s.get('reason') or ''))}")
|
|
680
|
+
say("")
|
|
681
|
+
return 0
|
|
682
|
+
|
|
683
|
+
|
|
580
684
|
def cmd_runs(args: argparse.Namespace) -> int:
|
|
581
685
|
"""YOUR OWN WEEK, from where the work happens.
|
|
582
686
|
|
|
@@ -818,24 +922,6 @@ def resolve_data(args: argparse.Namespace, session: Any = None) -> tuple[Any, An
|
|
|
818
922
|
|
|
819
923
|
universe = getattr(args, "universe", None)
|
|
820
924
|
|
|
821
|
-
# ── A SLEEVE BRINGS ITS OWN DATA ───────────────────────────────────────
|
|
822
|
-
#
|
|
823
|
-
# `--sleeve macro` is enough; `--sleeve macro --universe "sp100 universe"`
|
|
824
|
-
# was the shape before this, and saying which book and which data as two
|
|
825
|
-
# separate arguments on every command is what kept them separate concepts.
|
|
826
|
-
# A sleeve IS a book plus what it runs on.
|
|
827
|
-
#
|
|
828
|
-
# AN EXPLICIT `--universe` STILL WINS, and silently is not an option — the
|
|
829
|
-
# line below says which universe was used and why, because a run that
|
|
830
|
-
# quietly substituted the sleeve's default for the one somebody typed is
|
|
831
|
-
# the kind of helpfulness nobody can debug.
|
|
832
|
-
if universe is None and getattr(args, "sleeve", None):
|
|
833
|
-
sleeve = _sleeve_row(session, args.sleeve) or {}
|
|
834
|
-
from_sleeve = sleeve.get("universe_id")
|
|
835
|
-
if from_sleeve:
|
|
836
|
-
universe = str(from_sleeve)
|
|
837
|
-
say(dim(f" sleeve {sleeve.get('name')} {DOT} runs on its registered universe"))
|
|
838
|
-
|
|
839
925
|
if universe:
|
|
840
926
|
data = _narrow_to_universe(session, universe, data)
|
|
841
927
|
|
|
@@ -967,167 +1053,24 @@ def _narrow_to_universe(session: Any, wanted: str, data: Any) -> Any:
|
|
|
967
1053
|
return kept
|
|
968
1054
|
|
|
969
1055
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
except Exception as exc: # noqa: BLE001 - offline or refused, said plainly
|
|
989
|
-
raise DataShapeError(f"--sleeve needs the portal, and it could not be reached: {exc}") from exc
|
|
990
|
-
|
|
991
|
-
if not rows:
|
|
992
|
-
raise DataShapeError(
|
|
993
|
-
"your account is not in a pod, so it has no sleeves. Drop --sleeve "
|
|
994
|
-
"and the run works exactly the same; a sleeve only adds the budget "
|
|
995
|
-
"it is bound by and the book it rolls into."
|
|
996
|
-
)
|
|
997
|
-
# THE KEY IS `id`, NOT `workspace_id`. The server's sleeve serializer returns
|
|
998
|
-
# `{id, pod_id, name, stage, benchmark, initial_capital, pm_user_id, ...}` —
|
|
999
|
-
# the workspace IS the sleeve, so its primary key is `id`, and the name
|
|
1000
|
-
# `workspace_id` only appears on the routes that TAKE one. Reading the
|
|
1001
|
-
# parameter name off the route and assuming it names the response field is
|
|
1002
|
-
# the same mistake that made `--universe` read a top-level `symbols` key the
|
|
1003
|
-
# server has never sent. Pinned by a contract test in the platform repo that
|
|
1004
|
-
# imports this function and the real serializer together.
|
|
1005
|
-
match = next(
|
|
1006
|
-
(s for s in rows if wanted in (str(s.get("id")), str(s.get("name")))),
|
|
1007
|
-
None,
|
|
1008
|
-
)
|
|
1009
|
-
if match is None:
|
|
1010
|
-
known = ", ".join(str(s.get("name")) for s in rows) or "none"
|
|
1011
|
-
raise DataShapeError(f"no sleeve called {wanted!r}. Yours: {known}")
|
|
1012
|
-
return dict(match)
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
def _resolve_sleeve(session: Any, wanted: str | None) -> str | None:
|
|
1016
|
-
"""The workspace id a run is opened against, or None."""
|
|
1017
|
-
row = _sleeve_row(session, wanted)
|
|
1018
|
-
return str(row.get("id")) if row else None
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
def _offer_sleeve(session: Any, run: Any, wanted: str | None) -> None:
|
|
1022
|
-
"""Resolve `--thesis` and propose, or say why it cannot.
|
|
1023
|
-
|
|
1024
|
-
BY TITLE AS WELL AS BY ID, because a person types a name. The same reasoning
|
|
1025
|
-
as `--sleeve` and `--universe`: requiring a uuid is how a correct mechanism
|
|
1026
|
-
ends up never receiving input.
|
|
1027
|
-
"""
|
|
1028
|
-
if not wanted or session is None:
|
|
1029
|
-
return
|
|
1030
|
-
try:
|
|
1031
|
-
rows = session.theses()
|
|
1032
|
-
except Exception as exc: # noqa: BLE001 - offline or refused, said plainly
|
|
1033
|
-
say(dim(f" Could not read your theses: {exc}"))
|
|
1034
|
-
return
|
|
1035
|
-
|
|
1036
|
-
match = next((t for t in rows if wanted in (str(t.get("id")), str(t.get("title")))), None)
|
|
1037
|
-
if match is None:
|
|
1038
|
-
known = ", ".join(str(t.get("title")) for t in rows[:6]) or "none"
|
|
1039
|
-
say(yellow(f" No thesis called {wanted!r}. Yours: {known}"))
|
|
1040
|
-
return
|
|
1041
|
-
if match.get("workspace_id"):
|
|
1042
|
-
say(yellow(f" {match.get('title')!r} already belongs to a sleeve."))
|
|
1043
|
-
say(dim(" A thesis expresses one claim in one book. Write a new draft."))
|
|
1044
|
-
return
|
|
1045
|
-
|
|
1046
|
-
_propose_sleeve(session, run, match)
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
def _propose_sleeve(session: Any, run: Any, thesis: dict[str, Any]) -> None:
|
|
1050
|
-
"""Offer the closed screen as a sleeve for the thesis it was run against.
|
|
1051
|
-
|
|
1052
|
-
── THE VERB, FROM THE QUANT'S DESK ────────────────────────────────────────
|
|
1053
|
-
|
|
1054
|
-
The screen ran here, against data that never left this machine. What crosses
|
|
1055
|
-
is the shortlist and the argument for it — as a PROPOSAL, which creates no
|
|
1056
|
-
sleeve and no position. A person decides in the portal, because deciding is
|
|
1057
|
-
the human act the whole object exists to require.
|
|
1058
|
-
|
|
1059
|
-
That split is the product: the quant runs the research where the data is,
|
|
1060
|
-
the PM decides where the authority is, and something durable travels between
|
|
1061
|
-
them instead of a screenshot.
|
|
1062
|
-
|
|
1063
|
-
A FAILURE HERE NEVER FAILS THE RUN. The figures are already on screen and
|
|
1064
|
-
the run is sealed server-side; losing the proposal costs a hand-off, and
|
|
1065
|
-
taking the run down with it would trade the work for the paperwork.
|
|
1066
|
-
"""
|
|
1067
|
-
figures = (run.artifact or {}).get("figures") or {}
|
|
1068
|
-
flat: dict[str, Any] = {}
|
|
1069
|
-
for value in figures.values():
|
|
1070
|
-
if isinstance(value, dict):
|
|
1071
|
-
flat.update(value)
|
|
1072
|
-
rows = flat.get("rows") or []
|
|
1073
|
-
if not rows:
|
|
1074
|
-
return
|
|
1075
|
-
|
|
1076
|
-
ranked = [
|
|
1077
|
-
{
|
|
1078
|
-
"ticker": str(r.get("symbol") or r.get("ticker") or ""),
|
|
1079
|
-
"rank": i + 1,
|
|
1080
|
-
"score": r.get("score"),
|
|
1081
|
-
"target_weight": round(1.0 / min(len(rows), 25), 6),
|
|
1082
|
-
}
|
|
1083
|
-
for i, r in enumerate(rows[:25])
|
|
1084
|
-
]
|
|
1085
|
-
caveats: list[str] = []
|
|
1086
|
-
universe, evaluated = flat.get("universe_size"), flat.get("n_evaluated")
|
|
1087
|
-
if isinstance(universe, int) and isinstance(evaluated, int) and evaluated < universe:
|
|
1088
|
-
caveats.append(
|
|
1089
|
-
f"{evaluated} of {universe} names could be measured; the rest had too "
|
|
1090
|
-
"little history and are absent from this ranking, which flatters it."
|
|
1091
|
-
)
|
|
1092
|
-
if flat.get("truncated") or len(rows) > len(ranked):
|
|
1093
|
-
caveats.append(
|
|
1094
|
-
f"{len(rows)} names cleared the screen and the {len(ranked)} highest are "
|
|
1095
|
-
"proposed. The cut is a position-count decision, not a quality one."
|
|
1096
|
-
)
|
|
1097
|
-
|
|
1098
|
-
title = str(thesis.get("title") or "this thesis")
|
|
1099
|
-
try:
|
|
1100
|
-
filed = session.propose_sleeve(
|
|
1101
|
-
thesis_id=str(thesis.get("id")),
|
|
1102
|
-
rows=ranked,
|
|
1103
|
-
rationale=(
|
|
1104
|
-
f"{len(ranked)} names express {title!r}, ranked by "
|
|
1105
|
-
f"{flat.get('rank_by') or 'score'}. Equal weight across the shortlist."
|
|
1106
|
-
),
|
|
1107
|
-
caveats=caveats,
|
|
1108
|
-
payload={
|
|
1109
|
-
"rank_by": flat.get("rank_by"),
|
|
1110
|
-
"n_ranked": len(rows),
|
|
1111
|
-
"universe_size": universe,
|
|
1112
|
-
"n_evaluated": evaluated,
|
|
1113
|
-
},
|
|
1114
|
-
run_id=getattr(run, "run_id", None),
|
|
1115
|
-
)
|
|
1116
|
-
except Exception as exc: # noqa: BLE001 - named, never fatal
|
|
1117
|
-
say(dim(f" The sleeve could not be proposed: {exc}"))
|
|
1118
|
-
return
|
|
1119
|
-
|
|
1120
|
-
say("")
|
|
1121
|
-
say(f" {green('Proposed.')} {len(ranked)} names for {bold(title)}")
|
|
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
|
-
)
|
|
1130
|
-
say(dim(f" proposal {filed.get('id')}"))
|
|
1056
|
+
# ── `--sleeve` AND `--thesis` ARE GONE (0.5.0) ─────────────────────────────
|
|
1057
|
+
#
|
|
1058
|
+
# Four helpers lived here: `_sleeve_row`, `_resolve_sleeve`, `_offer_sleeve` and
|
|
1059
|
+
# `_propose_sleeve`. They resolved a sleeve by name to open a run against, and
|
|
1060
|
+
# turned a closed screen into a PROPOSAL — the one thing this package ever
|
|
1061
|
+
# created.
|
|
1062
|
+
#
|
|
1063
|
+
# Their three routes (`/api/me/sleeves`, `/api/me/theses`,
|
|
1064
|
+
# `/api/me/proposals/from-os`) belonged to the thesis object model, which came
|
|
1065
|
+
# out with the retired agent desk it was built around. A CLI flag whose endpoint
|
|
1066
|
+
# does not exist is worse than an absent one: it fails at the END of a run, after
|
|
1067
|
+
# the work, naming a server error rather than a missing feature.
|
|
1068
|
+
#
|
|
1069
|
+
# THE SPLIT THEY EXISTED FOR IS UNCHANGED and now runs through a smaller object.
|
|
1070
|
+
# The quant still runs the research where the data is and the PM still decides
|
|
1071
|
+
# where the authority is; what travels between them is the run itself, handed
|
|
1072
|
+
# over with an addressee (`delivered_to`) and decided on the run page where its
|
|
1073
|
+
# figures, its caveats and its review pass already are.
|
|
1131
1074
|
|
|
1132
1075
|
|
|
1133
1076
|
def _publish_signals(session: Any, run: Any, *, workspace_id: str | None, label: str | None) -> None:
|
|
@@ -1324,7 +1267,7 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1324
1267
|
session, url = _session(args.url, args.key)
|
|
1325
1268
|
try:
|
|
1326
1269
|
data, backtest_fn = resolve_data(args, session)
|
|
1327
|
-
workspace_id =
|
|
1270
|
+
workspace_id = None
|
|
1328
1271
|
except (ProjectError, ValueError) as exc:
|
|
1329
1272
|
say(red(str(exc)))
|
|
1330
1273
|
return 2
|
|
@@ -1397,7 +1340,6 @@ def cmd_run(args: argparse.Namespace) -> int:
|
|
|
1397
1340
|
code = _report(run)
|
|
1398
1341
|
if run.status == "closed":
|
|
1399
1342
|
_publish_signals(session, run, workspace_id=workspace_id, label=args.label)
|
|
1400
|
-
_offer_sleeve(session, run, getattr(args, "thesis", None))
|
|
1401
1343
|
return code
|
|
1402
1344
|
|
|
1403
1345
|
|
|
@@ -3017,7 +2959,7 @@ def cmd_repl(args: argparse.Namespace) -> int:
|
|
|
3017
2959
|
session, url = _session(args.url, args.key)
|
|
3018
2960
|
try:
|
|
3019
2961
|
data, backtest_fn = resolve_data(args, session)
|
|
3020
|
-
workspace_id =
|
|
2962
|
+
workspace_id = None
|
|
3021
2963
|
except (ProjectError, ValueError) as exc:
|
|
3022
2964
|
say(red(str(exc)))
|
|
3023
2965
|
return 2
|
|
@@ -3114,6 +3056,14 @@ def cmd_repl(args: argparse.Namespace) -> int:
|
|
|
3114
3056
|
# leave to find out.
|
|
3115
3057
|
cmd_runs(argparse.Namespace(url=args.url, key=args.key, limit=25))
|
|
3116
3058
|
continue
|
|
3059
|
+
if verb == "gaps":
|
|
3060
|
+
# AND THE OTHER HALF: what has NOT been tried. Same reason it is in
|
|
3061
|
+
# the session — the moment you want it is mid-flight.
|
|
3062
|
+
cmd_gaps(argparse.Namespace(url=args.url, key=args.key))
|
|
3063
|
+
continue
|
|
3064
|
+
if verb == "tonight":
|
|
3065
|
+
cmd_tonight(argparse.Namespace(url=args.url, key=args.key, budget=0))
|
|
3066
|
+
continue
|
|
3117
3067
|
if verb == "status":
|
|
3118
3068
|
say(dim("no run yet") if last is None else f"{last.run_id} {last.status}")
|
|
3119
3069
|
continue
|
|
@@ -3310,14 +3260,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3310
3260
|
"--symbol",
|
|
3311
3261
|
help="one name out of a loaded universe; its closes become the return series sizing runs on",
|
|
3312
3262
|
)
|
|
3313
|
-
common.add_argument(
|
|
3314
|
-
"--sleeve",
|
|
3315
|
-
help="name or id of a sleeve this run belongs to; binds it to that sleeve's risk budget",
|
|
3316
|
-
)
|
|
3317
|
-
common.add_argument(
|
|
3318
|
-
"--thesis",
|
|
3319
|
-
help="a draft thesis to propose this run's shortlist as a sleeve for",
|
|
3320
|
-
)
|
|
3321
3263
|
|
|
3322
3264
|
p = argparse.ArgumentParser(
|
|
3323
3265
|
prog="alphaengine",
|
|
@@ -3330,6 +3272,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3330
3272
|
sub.add_parser("demo", parents=[common], help="run the built-in example, offline")
|
|
3331
3273
|
rl = sub.add_parser("runs", parents=[common], help="your own week: what ran, and what it decided")
|
|
3332
3274
|
rl.add_argument("--limit", type=int, default=25, help="how many to show (default 25)")
|
|
3275
|
+
sub.add_parser("gaps", parents=[common], help="what your record says is unanswered")
|
|
3276
|
+
tn = sub.add_parser("tonight", parents=[common], help="what would run unattended, without running it")
|
|
3277
|
+
tn.add_argument("--budget", type=int, default=0, help="how many runs a night is worth")
|
|
3333
3278
|
sub.add_parser("logout", parents=[common], help="remove stored credentials")
|
|
3334
3279
|
d = sub.add_parser("commands", parents=[common], help="every command, grouped")
|
|
3335
3280
|
d.add_argument("verb", nargs="?", help="expand one command in full")
|
|
@@ -3398,6 +3343,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
3398
3343
|
"version": cmd_version,
|
|
3399
3344
|
"workflows": cmd_workflows,
|
|
3400
3345
|
"runs": cmd_runs,
|
|
3346
|
+
"gaps": cmd_gaps,
|
|
3347
|
+
"tonight": cmd_tonight,
|
|
3401
3348
|
"demo": cmd_demo,
|
|
3402
3349
|
"logout": cmd_logout,
|
|
3403
3350
|
"commands": cmd_commands,
|
|
@@ -422,68 +422,41 @@ class Session:
|
|
|
422
422
|
body["source_run_id"] = source_run_id
|
|
423
423
|
return self._post("/api/me/signals", body)
|
|
424
424
|
|
|
425
|
-
def
|
|
426
|
-
"""
|
|
425
|
+
def gaps(self) -> Figures:
|
|
426
|
+
"""WHAT YOUR RECORD SAYS IS UNANSWERED, across the whole account.
|
|
427
427
|
|
|
428
|
-
A
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
"""
|
|
432
|
-
return list(self._get("/api/me/theses").get("theses") or [])
|
|
433
|
-
|
|
434
|
-
def propose_sleeve(
|
|
435
|
-
self,
|
|
436
|
-
*,
|
|
437
|
-
thesis_id: str,
|
|
438
|
-
rows: list[Figures],
|
|
439
|
-
rationale: str,
|
|
440
|
-
caveats: list[str] | None = None,
|
|
441
|
-
payload: Figures | None = None,
|
|
442
|
-
run_id: str | None = None,
|
|
443
|
-
book_id: str | None = None,
|
|
444
|
-
) -> Figures:
|
|
445
|
-
"""File a proposed sleeve for a thesis. THE ONLY THING THIS MAKES.
|
|
428
|
+
A run's own `/gaps` answers this for one run — the view a quant has at
|
|
429
|
+
the moment of finishing something. This is the same derivation read
|
|
430
|
+
across everything, which is the view somebody has at the START of a day.
|
|
446
431
|
|
|
447
|
-
|
|
432
|
+
Every gap names the workflow that closes it, and both are DERIVED: a gap
|
|
433
|
+
a model was invited to find is one it can decline to find, and worse, one
|
|
434
|
+
it can invent.
|
|
435
|
+
"""
|
|
436
|
+
return self._get("/api/harness/gaps")
|
|
448
437
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
is the shortlist and the argument for it. Somebody then reads it and
|
|
452
|
-
decides — in the portal, because deciding is a human act and a
|
|
453
|
-
long-lived key pressing accept would be the machine approving its own
|
|
454
|
-
work through one extra hop.
|
|
438
|
+
def plan(self, budget: int = 0) -> Figures:
|
|
439
|
+
"""WHAT WOULD RUN TONIGHT, without running any of it.
|
|
455
440
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
what
|
|
459
|
-
|
|
460
|
-
"""
|
|
461
|
-
body: Figures = {
|
|
462
|
-
"thesis_id": thesis_id,
|
|
463
|
-
"rows": rows,
|
|
464
|
-
"rationale": rationale,
|
|
465
|
-
"caveats": list(caveats or []),
|
|
466
|
-
}
|
|
467
|
-
if payload:
|
|
468
|
-
body["payload"] = payload
|
|
469
|
-
if run_id:
|
|
470
|
-
body["source_run_id"] = run_id
|
|
471
|
-
if book_id:
|
|
472
|
-
body["book_id"] = book_id
|
|
473
|
-
return self._post("/api/me/proposals/from-os", body)
|
|
474
|
-
|
|
475
|
-
def sleeves(self) -> list[Figures]:
|
|
476
|
-
"""The sleeves you can run against, if your account is in a pod.
|
|
477
|
-
|
|
478
|
-
A sleeve is a workspace with a pod, a stage and a PM. Naming one on a
|
|
479
|
-
run is what lets the platform resolve the risk budget that binds it and
|
|
480
|
-
roll the result into the pod's book — without it every run is orphaned
|
|
481
|
-
from the object model and every sleeve reports zero.
|
|
482
|
-
|
|
483
|
-
An account with no pod has no sleeves, and that is a working product
|
|
484
|
-
rather than an error: a solo quant is a first-class user here.
|
|
441
|
+
The same derivation the scheduler uses, so this shows what will happen
|
|
442
|
+
rather than a description of it. `skipped` carries what the budget cut
|
|
443
|
+
AND what is blocked on an input only you can state — a plan that
|
|
444
|
+
silently truncates reads as "this is everything that mattered".
|
|
485
445
|
"""
|
|
486
|
-
|
|
446
|
+
q = f"?budget={int(budget)}" if budget else ""
|
|
447
|
+
return self._get(f"/api/harness/plan{q}")
|
|
448
|
+
|
|
449
|
+
# ── THE THESIS AND SLEEVE METHODS ARE GONE (0.5.0) ─────────────────────
|
|
450
|
+
#
|
|
451
|
+
# `theses()`, `propose_sleeve()` and `sleeves()` backed `--thesis` and
|
|
452
|
+
# `--sleeve`. Their routes — /api/me/theses, /api/me/proposals/from-os and
|
|
453
|
+
# /api/me/sleeves — were part of the thesis object model, which came out with
|
|
454
|
+
# the retired agent desk it was built around.
|
|
455
|
+
#
|
|
456
|
+
# A breaking change, and a deliberate one: they created a PROPOSAL from a
|
|
457
|
+
# screen, which is the one thing this package ever made, and the loop that
|
|
458
|
+
# object served now runs through assignments and the deliver pin instead.
|
|
459
|
+
# See the 0.5.0 note in the README.
|
|
487
460
|
|
|
488
461
|
# ── transport ──────────────────────────────────────────────────────────
|
|
489
462
|
def _request(self, method: str, path: str, body: Figures | None = None) -> Figures:
|
|
@@ -87,12 +87,11 @@ FLAGS: dict[str, Flag] = {
|
|
|
87
87
|
"symbol": Flag(
|
|
88
88
|
"--symbol", "TICKER", "one name out of a loaded universe; its closes become the return series"
|
|
89
89
|
),
|
|
90
|
-
"sleeve": Flag("--sleeve", "NAME", "the sleeve this run belongs to, and the budget it is bound by"),
|
|
91
|
-
"thesis": Flag("--thesis", "ID", "a draft thesis to propose this run's shortlist as a sleeve for"),
|
|
92
90
|
"label": Flag("--label", "TEXT", "what to call the artifact this run produces"),
|
|
93
91
|
"input": Flag("--input", "K=V", "a workflow input; repeatable"),
|
|
94
92
|
"quiet": Flag("--quiet", "", "only the result, no step narration"),
|
|
95
93
|
"limit": Flag("--limit", "N", "how many rows to show (default 25)"),
|
|
94
|
+
"budget": Flag("--budget", "N", "how many runs a night is worth (default 3)"),
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
|
|
@@ -213,6 +212,43 @@ COMMANDS: tuple[Command, ...] = _question_commands() + (
|
|
|
213
212
|
),
|
|
214
213
|
examples=("alphaengine runs", "alphaengine runs --limit 50"),
|
|
215
214
|
),
|
|
215
|
+
Command(
|
|
216
|
+
verb="gaps",
|
|
217
|
+
group="start",
|
|
218
|
+
args="",
|
|
219
|
+
scope="both",
|
|
220
|
+
purpose="what your record says is UNANSWERED, and what closes each one",
|
|
221
|
+
body=(
|
|
222
|
+
"`runs` shows what you already tried. This shows what you have not: "
|
|
223
|
+
"the other half of the same question, and the one you want at 9am "
|
|
224
|
+
"rather than at the end of a task.\n\n"
|
|
225
|
+
"EVERY LINE NAMES ITS REMEDY. A gap that cannot name one is an "
|
|
226
|
+
"observation rather than a next step, so the ones with no workflow "
|
|
227
|
+
"behind them are not printed as work.\n\n"
|
|
228
|
+
"Derived from the record, never asked of a model: a model invited to "
|
|
229
|
+
"find gaps can decline to find them, and can invent them. What you "
|
|
230
|
+
"see is a query over runs that actually happened."
|
|
231
|
+
),
|
|
232
|
+
examples=("alphaengine gaps",),
|
|
233
|
+
),
|
|
234
|
+
Command(
|
|
235
|
+
verb="tonight",
|
|
236
|
+
group="start",
|
|
237
|
+
args="[--budget N]",
|
|
238
|
+
scope="both",
|
|
239
|
+
purpose="what would run unattended tonight, without running any of it",
|
|
240
|
+
body=(
|
|
241
|
+
"The same derivation the scheduler uses, so this is what WILL happen "
|
|
242
|
+
"rather than a description of it.\n\n"
|
|
243
|
+
"EVERYTHING SKIPPED IS PRINTED. A plan showing only what it kept "
|
|
244
|
+
"reads as 'this is everything that mattered', which is the one thing "
|
|
245
|
+
"a plan must never imply. Two reasons appear: past tonight's budget, "
|
|
246
|
+
"and blocked on an input only you can state. A monitor's tolerances "
|
|
247
|
+
"are not something the planner may invent."
|
|
248
|
+
),
|
|
249
|
+
examples=("alphaengine tonight", "alphaengine tonight --budget 5"),
|
|
250
|
+
flags=("budget",),
|
|
251
|
+
),
|
|
216
252
|
Command(
|
|
217
253
|
verb="workflows",
|
|
218
254
|
group="start",
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|