alphaengine 0.9.0__tar.gz → 0.9.2__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.
Files changed (94) hide show
  1. {alphaengine-0.9.0 → alphaengine-0.9.2}/PKG-INFO +4 -1
  2. {alphaengine-0.9.0 → alphaengine-0.9.2}/README.md +3 -0
  3. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/_version.py +9 -1
  4. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/cli.py +83 -2
  5. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/client/agent.py +13 -2
  6. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/client/session.py +19 -11
  7. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/commands.py +37 -1
  8. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/risk.py +6 -1
  9. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/model.py +6 -7
  10. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/tui/app.py +239 -32
  11. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/tui/dispatch.py +135 -12
  12. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_client.py +25 -0
  13. alphaengine-0.9.2/tests/test_tui.py +289 -0
  14. alphaengine-0.9.0/tests/test_tui.py +0 -127
  15. {alphaengine-0.9.0 → alphaengine-0.9.2}/.github/workflows/ci.yml +0 -0
  16. {alphaengine-0.9.0 → alphaengine-0.9.2}/.github/workflows/publish.yml +0 -0
  17. {alphaengine-0.9.0 → alphaengine-0.9.2}/.gitignore +0 -0
  18. {alphaengine-0.9.0 → alphaengine-0.9.2}/AGENTS.md +0 -0
  19. {alphaengine-0.9.0 → alphaengine-0.9.2}/LICENSE +0 -0
  20. {alphaengine-0.9.0 → alphaengine-0.9.2}/SECURITY.md +0 -0
  21. {alphaengine-0.9.0 → alphaengine-0.9.2}/docs/assets/banner.png +0 -0
  22. {alphaengine-0.9.0 → alphaengine-0.9.2}/docs/assets/data_boundary.png +0 -0
  23. {alphaengine-0.9.0 → alphaengine-0.9.2}/docs/assets/session.png +0 -0
  24. {alphaengine-0.9.0 → alphaengine-0.9.2}/docs/figure_contract.md +0 -0
  25. {alphaengine-0.9.0 → alphaengine-0.9.2}/docs/workflows/evaluate_process.json +0 -0
  26. {alphaengine-0.9.0 → alphaengine-0.9.2}/docs/workflows/research_week.json +0 -0
  27. {alphaengine-0.9.0 → alphaengine-0.9.2}/docs/workflows/stress_study.json +0 -0
  28. {alphaengine-0.9.0 → alphaengine-0.9.2}/pyproject.toml +0 -0
  29. {alphaengine-0.9.0 → alphaengine-0.9.2}/scripts/gen_docs.py +0 -0
  30. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/__init__.py +0 -0
  31. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/__main__.py +0 -0
  32. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/agent/__init__.py +0 -0
  33. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/agent/answer.py +0 -0
  34. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/agent/driver.py +0 -0
  35. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/auth.py +0 -0
  36. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/book.py +0 -0
  37. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/charts.py +0 -0
  38. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/client/__init__.py +0 -0
  39. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/client/executor.py +0 -0
  40. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/complete.py +0 -0
  41. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/connectors/__init__.py +0 -0
  42. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/__init__.py +0 -0
  43. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/active.py +0 -0
  44. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/allocate.py +0 -0
  45. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/backtest.py +0 -0
  46. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/covariance.py +0 -0
  47. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/cross_section.py +0 -0
  48. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/factors.py +0 -0
  49. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/pairs.py +0 -0
  50. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/panel.py +0 -0
  51. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/performance.py +0 -0
  52. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/process.py +0 -0
  53. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/profile.py +0 -0
  54. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/screen.py +0 -0
  55. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/series_shapes.py +0 -0
  56. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/signals.py +0 -0
  57. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/stress.py +0 -0
  58. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/technical.py +0 -0
  59. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/trace.py +0 -0
  60. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/validation.py +0 -0
  61. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/core/walkforward.py +0 -0
  62. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/demo.py +0 -0
  63. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/demo_book.py +0 -0
  64. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/demo_returns.py +0 -0
  65. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/demo_signal.py +0 -0
  66. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/demo_universe.py +0 -0
  67. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/events.py +0 -0
  68. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/loaders.py +0 -0
  69. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/py.typed +0 -0
  70. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/repl.py +0 -0
  71. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/study/__init__.py +0 -0
  72. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/study/report.py +0 -0
  73. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/study/schema.py +0 -0
  74. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/sweep/__init__.py +0 -0
  75. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/sweep/runner.py +0 -0
  76. {alphaengine-0.9.0 → alphaengine-0.9.2}/src/alphaengine/tui/__init__.py +0 -0
  77. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_agent.py +0 -0
  78. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_answer.py +0 -0
  79. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_book.py +0 -0
  80. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_cli.py +0 -0
  81. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_commands.py +0 -0
  82. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_events.py +0 -0
  83. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_executor_ops.py +0 -0
  84. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_goldens.py +0 -0
  85. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_loaders.py +0 -0
  86. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_math_audit.py +0 -0
  87. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_model.py +0 -0
  88. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_process.py +0 -0
  89. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_quant_maths.py +0 -0
  90. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_screen.py +0 -0
  91. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_signals.py +0 -0
  92. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_smoke.py +0 -0
  93. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_sweep.py +0 -0
  94. {alphaengine-0.9.0 → alphaengine-0.9.2}/tests/test_trace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: alphaengine
3
- Version: 0.9.0
3
+ Version: 0.9.2
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
@@ -333,6 +333,7 @@ w = hrp_weights(cov, names=names) # no matrix inverse; weights sum to one
333
333
  |---|---|---|
334
334
  | `book [<name> \| status]` | show or load sleeves on the multi-strategy book | session |
335
335
  | `load <file \| module \| universe>` | a CSV, a project module, or a portal universe | session |
336
+ | `thesis [<name> \| clear]` | choose a portal thesis for the next run | session |
336
337
  | `universe <name>` | same as load: a universe registered in the portal | session |
337
338
  | `data <file>` | same as load: a local CSV or parquet | session |
338
339
  | `project <module>` | same as load: a module with data and backtest_fn | session |
@@ -363,6 +364,7 @@ w = hrp_weights(cov, names=names) # no matrix inverse; weights sum to one
363
364
  | `--project MODULE` | a module exposing `data` and `backtest_fn` |
364
365
  | `--data FILE` | a local CSV: wide, long, or a single series |
365
366
  | `--universe NAME` | a universe registered in the portal, with its stored closes |
367
+ | `--thesis NAME` | a thesis on this account; the run is attached to it |
366
368
  | `--symbol TICKER` | one name out of a loaded universe; its closes become the return series |
367
369
  | `--label TEXT` | what to call the artifact this run produces |
368
370
  | `--input K=V` | a workflow input; repeatable |
@@ -398,6 +400,7 @@ alphaengine run size_position --data returns.csv
398
400
  alphaengine run validate_study --project research.momentum
399
401
  alphaengine models
400
402
  alphaengine trace
403
+ alphaengine run screen_universe --thesis momentum
401
404
  alphaengine logout
402
405
  alphaengine version
403
406
  ```
@@ -288,6 +288,7 @@ w = hrp_weights(cov, names=names) # no matrix inverse; weights sum to one
288
288
  |---|---|---|
289
289
  | `book [<name> \| status]` | show or load sleeves on the multi-strategy book | session |
290
290
  | `load <file \| module \| universe>` | a CSV, a project module, or a portal universe | session |
291
+ | `thesis [<name> \| clear]` | choose a portal thesis for the next run | session |
291
292
  | `universe <name>` | same as load: a universe registered in the portal | session |
292
293
  | `data <file>` | same as load: a local CSV or parquet | session |
293
294
  | `project <module>` | same as load: a module with data and backtest_fn | session |
@@ -318,6 +319,7 @@ w = hrp_weights(cov, names=names) # no matrix inverse; weights sum to one
318
319
  | `--project MODULE` | a module exposing `data` and `backtest_fn` |
319
320
  | `--data FILE` | a local CSV: wide, long, or a single series |
320
321
  | `--universe NAME` | a universe registered in the portal, with its stored closes |
322
+ | `--thesis NAME` | a thesis on this account; the run is attached to it |
321
323
  | `--symbol TICKER` | one name out of a loaded universe; its closes become the return series |
322
324
  | `--label TEXT` | what to call the artifact this run produces |
323
325
  | `--input K=V` | a workflow input; repeatable |
@@ -353,6 +355,7 @@ alphaengine run size_position --data returns.csv
353
355
  alphaengine run validate_study --project research.momentum
354
356
  alphaengine models
355
357
  alphaengine trace
358
+ alphaengine run screen_universe --thesis momentum
356
359
  alphaengine logout
357
360
  alphaengine version
358
361
  ```
@@ -76,4 +76,12 @@ is what a changed figure costs. The API may still move underneath it.
76
76
  # Close fills are unchanged. No golden in tests/test_goldens.py moves. The
77
77
  # same release makes the full-screen terminal the only session, and posts each
78
78
  # math line's series on `/traces` without putting those series inside figures.
79
- __version__ = "0.9.0"
79
+ #
80
+ # 0.9.1 IS THE SESSION, NOT THE MATHS. No computed value moved. A signed-in
81
+ # account loads its one stored universe from the portal. `enter model key`
82
+ # takes whichever provider key is pasted. The left pane lists the commands.
83
+ #
84
+ # 0.9.2 IS THE SESSION, NOT THE MATHS. No computed value moved. A command
85
+ # shows that it is working, and a run writes each step as it happens. A thesis
86
+ # on the account can be pinned, and the run opened after that carries its id.
87
+ __version__ = "0.9.2"
@@ -1708,11 +1708,28 @@ def cmd_run(args: argparse.Namespace) -> int:
1708
1708
  # named one explicitly. Without this, a sweep received `grid={}`.
1709
1709
  if "grid" not in inputs and project_grid():
1710
1710
  inputs["grid"] = project_grid()
1711
+ thesis_id = None
1712
+ named_thesis = getattr(args, "thesis", None)
1713
+ if named_thesis:
1714
+ chosen, candidates = match_thesis(session.theses(), str(named_thesis))
1715
+ if chosen is None:
1716
+ if candidates:
1717
+ names = ", ".join(thesis_label(r) for r in candidates)
1718
+ say(yellow(f"Which thesis? {names}"))
1719
+ else:
1720
+ say(yellow(f"No thesis named {named_thesis!r} on this account."))
1721
+ return 2
1722
+ thesis_id = thesis_id_of(chosen)
1723
+ if not thesis_id:
1724
+ say(yellow(f"{thesis_label(chosen)} has no id, so a run cannot be attached to it."))
1725
+ return 2
1726
+ say(dim(f" thesis {thesis_label(chosen)}"))
1711
1727
  run = session.open(
1712
1728
  args.workflow,
1713
1729
  data=data,
1714
1730
  backtest_fn=backtest_fn,
1715
1731
  workspace_id=workspace_id,
1732
+ thesis_id=thesis_id,
1716
1733
  **inputs,
1717
1734
  )
1718
1735
  _drive(run, quiet=args.quiet)
@@ -2235,7 +2252,7 @@ def _extract_flags(line: str) -> tuple[str, list[str]]:
2235
2252
  i = 0
2236
2253
  while i < len(parts):
2237
2254
  tok = parts[i]
2238
- if tok in ("--data", "--universe", "--project", "--symbol") and i + 1 < len(parts):
2255
+ if tok in ("--data", "--universe", "--project", "--symbol", "--thesis") and i + 1 < len(parts):
2239
2256
  flags += [tok, parts[i + 1]]
2240
2257
  i += 2
2241
2258
  continue
@@ -2357,6 +2374,58 @@ def _stored_universes(session: Any, question: str = "") -> tuple[str | None, lis
2357
2374
  return (ready[0] if len(ready) == 1 else None), ready
2358
2375
 
2359
2376
 
2377
+ def thesis_label(row: dict[str, Any]) -> str:
2378
+ """The name a person types. The id is what the run carries."""
2379
+ return str(row.get("name") or row.get("title") or row.get("id") or "").strip()
2380
+
2381
+
2382
+ def thesis_text(row: dict[str, Any]) -> str:
2383
+ """The statement, under whichever key the portal sent it."""
2384
+ for key in ("statement", "summary", "body", "text"):
2385
+ value = row.get(key)
2386
+ if isinstance(value, str) and value.strip():
2387
+ return value.strip()
2388
+ return ""
2389
+
2390
+
2391
+ def thesis_id_of(row: dict[str, Any]) -> str:
2392
+ return str(row.get("id") or row.get("thesis_id") or "").strip()
2393
+
2394
+
2395
+ def match_thesis(rows: list[Any], query: str) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
2396
+ """One thesis, or the candidates when the name matches more than one.
2397
+
2398
+ Exact name or id wins. A unique substring is enough. Several matches are
2399
+ returned so the caller can name them instead of guessing.
2400
+ """
2401
+ usable = [r for r in rows if isinstance(r, dict)]
2402
+ q = query.strip().casefold()
2403
+ exact = [r for r in usable if thesis_label(r).casefold() == q or thesis_id_of(r).casefold() == q]
2404
+ if len(exact) == 1:
2405
+ return exact[0], []
2406
+ if len(exact) > 1:
2407
+ return None, exact
2408
+ partial = [r for r in usable if q and q in thesis_label(r).casefold()]
2409
+ if len(partial) == 1:
2410
+ return partial[0], []
2411
+ return None, partial
2412
+
2413
+
2414
+ def pop_flag(flags: list[str], name: str) -> tuple[str | None, list[str]]:
2415
+ """Take one ``--name value`` out of a flag list. The rest stay in order."""
2416
+ kept: list[str] = []
2417
+ value: str | None = None
2418
+ i = 0
2419
+ while i < len(flags):
2420
+ if flags[i] == name and i + 1 < len(flags):
2421
+ value = flags[i + 1]
2422
+ i += 2
2423
+ continue
2424
+ kept.append(flags[i])
2425
+ i += 1
2426
+ return value, kept
2427
+
2428
+
2360
2429
  def _split_run(rest: str) -> tuple[str, list[str]]:
2361
2430
  """`screen_universe --universe sp100` -> ("screen_universe", [...]).
2362
2431
 
@@ -3162,6 +3231,9 @@ def _ask(
3162
3231
  data: Any,
3163
3232
  backtest_fn: Any,
3164
3233
  workspace_id: str | None = None,
3234
+ thesis_id: str | None = None,
3235
+ thesis_name: str | None = None,
3236
+ thesis_statement: str | None = None,
3165
3237
  think: Callable[[str], str] | None = None,
3166
3238
  ) -> Any:
3167
3239
  """Natural language → a workflow chosen and driven by the user's own model.
@@ -3212,14 +3284,20 @@ def _ask(
3212
3284
  return None
3213
3285
 
3214
3286
  say(f" {_accent(STAR)} {dim(label)}")
3287
+ if thesis_name:
3288
+ say(dim(f" thesis {thesis_name}"))
3289
+ goal = request
3290
+ if thesis_statement:
3291
+ goal = f"{request}\n\nThesis:\n{thesis_statement}"
3215
3292
 
3216
3293
  # Choosing the workflow is the same shape as choosing a step: an index into
3217
3294
  # a list the server produced. Reusing `AgentDriver.pick` rather than writing
3218
3295
  # a second selector keeps one place where a model's answer is validated.
3219
3296
  driver = AgentDriver(
3220
3297
  think,
3221
- goal=request,
3298
+ goal=goal,
3222
3299
  on_thought=lambda why: say(f" {_accent(STAR)} {_c('2;3', why)}"),
3300
+ on_step=lambda line: say(f" {line}"),
3223
3301
  sink=sink,
3224
3302
  provider=provider or None,
3225
3303
  model=model_name or None,
@@ -3297,6 +3375,7 @@ def _ask(
3297
3375
  data=data,
3298
3376
  backtest_fn=backtest_fn,
3299
3377
  workspace_id=workspace_id,
3378
+ thesis_id=thesis_id,
3300
3379
  **({"grid": project_grid()} if project_grid() else {}),
3301
3380
  )
3302
3381
  sink.bind(run.run_id)
@@ -3520,6 +3599,7 @@ def build_parser() -> argparse.ArgumentParser:
3520
3599
  r = sub.add_parser("run", parents=[common], help="run one workflow to completion")
3521
3600
  r.add_argument("workflow")
3522
3601
  r.add_argument("--label", help="what to call the artifact")
3602
+ r.add_argument("--thesis", help="name or id of a thesis on this account; attached to the run")
3523
3603
  r.add_argument("--input", action="append", help="workflow input as key=value (repeatable)")
3524
3604
  r.add_argument("--quiet", action="store_true", help="only the result")
3525
3605
  r.add_argument("--stream", action="store_true", help="print the answer after the citation guard")
@@ -3532,6 +3612,7 @@ def build_parser() -> argparse.ArgumentParser:
3532
3612
  for verb, workflow in QUESTION_VERBS.items():
3533
3613
  q = sub.add_parser(verb, parents=[common], help=f"run {workflow}")
3534
3614
  q.add_argument("--label", help="what to call the artifact")
3615
+ q.add_argument("--thesis", help="name or id of a thesis on this account; attached to the run")
3535
3616
  q.add_argument("--input", action="append", help="workflow input as key=value (repeatable)")
3536
3617
  q.add_argument("--quiet", action="store_true", help="only the result")
3537
3618
  q.add_argument("--stream", action="store_true", help="print the answer after the citation guard")
@@ -47,6 +47,7 @@ from __future__ import annotations
47
47
 
48
48
  import json
49
49
  import re
50
+ import time
50
51
  from collections.abc import Callable
51
52
  from typing import Any, Protocol
52
53
 
@@ -115,6 +116,7 @@ class AgentDriver:
115
116
  *,
116
117
  goal: str,
117
118
  on_thought: Callable[[str], None] | None = None,
119
+ on_step: Callable[[str], None] | None = None,
118
120
  max_steps: int = 60,
119
121
  sink: Any | None = None,
120
122
  provider: str | None = None,
@@ -126,6 +128,9 @@ class AgentDriver:
126
128
  # A loop you cannot watch is a loop you cannot trust, and that applies
127
129
  # doubly when something non-deterministic is making the choices.
128
130
  self.on_thought = on_thought or (lambda _s: None)
131
+ # A second narrator for the step itself, so a watched run can show the
132
+ # op while it is still running and the duration once it returns.
133
+ self.on_step = on_step or (lambda _s: None)
129
134
  self.max_steps = max_steps
130
135
  self.history: list[str] = []
131
136
  self.sink = sink
@@ -215,21 +220,27 @@ class AgentDriver:
215
220
  break
216
221
 
217
222
  step = run.permitted[self.pick(run.permitted)]
223
+ op = str(step.get("op") or "?")
224
+ self.on_step(f"{n + 1} {op}")
218
225
  before = run.status
226
+ started = time.monotonic()
219
227
  run.step(step)
228
+ took = time.monotonic() - started
220
229
  n += 1
221
230
 
222
231
  still = any(p.get("step_id") == step.get("step_id") for p in run.permitted)
223
232
  if still and before == "open" and run.status == "open":
224
233
  key = str(step.get("step_id"))
225
234
  failures[key] = failures.get(key, 0) + 1
226
- self.history.append(f"{step.get('op')} could not be executed here")
235
+ self.history.append(f"{op} could not be executed here")
236
+ self.on_step(f"{n} {op} could not be executed {took:.1f}s")
227
237
  if failures[key] >= 2:
228
238
  run.status = "abandoned"
229
239
  run.stopped = {"reason": "step_failed", "op": step.get("op")}
230
240
  return run
231
241
  else:
232
- self.history.append(f"{step.get('op')} done")
242
+ self.history.append(f"{op} done")
243
+ self.on_step(f"{n} {op} {took:.1f}s")
233
244
 
234
245
  if run.status == "open" and n >= self.max_steps:
235
246
  raise AgentRefusal(
@@ -335,6 +335,7 @@ class Session:
335
335
  backtest_fn: Any = None,
336
336
  handlers: dict[str, Handler] | None = None,
337
337
  workspace_id: str | None = None,
338
+ thesis_id: str | None = None,
338
339
  **inputs: Any,
339
340
  ) -> Run:
340
341
  """Open a run. `data` and `backtest_fn` stay on this machine.
@@ -345,10 +346,15 @@ class Session:
345
346
  into the pod's book. Without it the run is complete, correct, and
346
347
  attached to nothing — which is why several finished mechanisms on the
347
348
  platform had never once received input.
349
+
350
+ `thesis_id` names a thesis the portal already holds. The statement
351
+ stays on this machine; the id is how the run is attached to it.
348
352
  """
349
353
  body: Figures = {"workflow": workflow, "version": version, "inputs": inputs}
350
354
  if workspace_id:
351
355
  body["workspace_id"] = workspace_id
356
+ if thesis_id:
357
+ body["thesis_id"] = thesis_id
352
358
  directive = self._post("/api/harness/runs", body)
353
359
  run = Run(
354
360
  session=self,
@@ -486,17 +492,19 @@ class Session:
486
492
  q = f"?budget={int(budget)}" if budget else ""
487
493
  return self._get(f"/api/harness/plan{q}")
488
494
 
489
- # ── THE THESIS AND SLEEVE METHODS ARE GONE (0.5.0) ─────────────────────
490
- #
491
- # `theses()`, `propose_sleeve()` and `sleeves()` backed `--thesis` and
492
- # `--sleeve`. Their routes — /api/me/theses, /api/me/proposals/from-os and
493
- # /api/me/sleeves — were part of the thesis object model, which came out with
494
- # the retired agent desk it was built around.
495
- #
496
- # A breaking change, and a deliberate one: they created a PROPOSAL from a
497
- # screen, which is the one thing this package ever made, and the loop that
498
- # object served now runs through assignments and the deliver pin instead.
499
- # See the 0.5.0 note in the README.
495
+ def theses(self) -> list[Figures]:
496
+ """Theses on this account. Names and statements. No market data.
497
+
498
+ ``GET /api/me/theses`` returns ``{"theses": [{"id", "name", "statement"}]}``.
499
+ ``summary`` is accepted as the statement when ``statement`` is absent.
500
+ The id is what a later ``open`` sends back as ``thesis_id``. This call
501
+ does not create a thesis and does not write a proposal.
502
+ """
503
+ return list(self._get("/api/me/theses").get("theses") or [])
504
+
505
+ # `propose_sleeve()` and `sleeves()` backed `--sleeve` and wrote a proposal
506
+ # to `/api/me/proposals/from-os` and `/api/me/sleeves`. Those left with the
507
+ # agent desk in 0.5.0. Selecting a thesis is the read above, not that writer.
500
508
 
501
509
  def post_event(self, run_id: str, event: dict[str, Any]) -> Figures:
502
510
  """Allowlisted model/run event. 404-tolerant at the sink, not here.
@@ -84,6 +84,7 @@ FLAGS: dict[str, Flag] = {
84
84
  "project": Flag("--project", "MODULE", "a module exposing `data` and `backtest_fn`"),
85
85
  "data": Flag("--data", "FILE", "a local CSV: wide, long, or a single series"),
86
86
  "universe": Flag("--universe", "NAME", "a universe registered in the portal, with its stored closes"),
87
+ "thesis": Flag("--thesis", "NAME", "a thesis on this account; the run is attached to it"),
87
88
  "symbol": Flag(
88
89
  "--symbol", "TICKER", "one name out of a loaded universe; its closes become the return series"
89
90
  ),
@@ -352,7 +353,19 @@ COMMANDS: tuple[Command, ...] = _question_commands() + (
352
353
  "alphaengine run size_position --data returns.csv",
353
354
  "alphaengine run validate_study --project research.momentum",
354
355
  ),
355
- flags=("project", "data", "universe", "symbol", "label", "input", "quiet", "stream", "url", "key"),
356
+ flags=(
357
+ "project",
358
+ "data",
359
+ "universe",
360
+ "symbol",
361
+ "thesis",
362
+ "label",
363
+ "input",
364
+ "quiet",
365
+ "stream",
366
+ "url",
367
+ "key",
368
+ ),
356
369
  ),
357
370
  Command(
358
371
  verb="<anything else>",
@@ -454,6 +467,29 @@ COMMANDS: tuple[Command, ...] = _question_commands() + (
454
467
  ),
455
468
  examples=("load prices.csv", "load research.momentum", "load sp500"),
456
469
  ),
470
+ Command(
471
+ verb="thesis",
472
+ group="data",
473
+ args="[<name> | clear]",
474
+ scope="repl",
475
+ purpose="choose a portal thesis for the next run",
476
+ body=(
477
+ "Lists the theses on this account, or pins one. A pinned thesis rides on "
478
+ "every following run, question, and workflow as thesis_id. The statement "
479
+ "stays on this machine and is given to your model as context. It is not "
480
+ "written into the figures.\n\n"
481
+ "`thesis clear` unpins it. One thesis on the account is pinned at sign-in. "
482
+ "Several are named and not guessed.\n\n"
483
+ "The shell spelling is `--thesis <name>` on `run`, `screen`, `diagnose`, "
484
+ "and the other question commands."
485
+ ),
486
+ examples=(
487
+ "thesis",
488
+ "thesis momentum",
489
+ "thesis clear",
490
+ "alphaengine run screen_universe --thesis momentum",
491
+ ),
492
+ ),
457
493
  Command(
458
494
  verb="universe",
459
495
  group="data",
@@ -129,7 +129,12 @@ def compute_var_cvar(
129
129
  record(
130
130
  "parametric_var",
131
131
  "z * std(r, ddof=1) * sqrt(horizon), z = normsinv(confidence), as a positive percent",
132
- inputs={"n_obs": int(n_obs), "confidence": confidence, "horizon_days": int(horizon_days), "z": round(z, 6)},
132
+ inputs={
133
+ "n_obs": int(n_obs),
134
+ "confidence": confidence,
135
+ "horizon_days": int(horizon_days),
136
+ "z": round(z, 6),
137
+ },
133
138
  series={"returns": arr},
134
139
  result=result["parametric"]["var_pct"],
135
140
  )
@@ -328,13 +328,12 @@ def build_think(
328
328
  )
329
329
 
330
330
  raise NoModelConfigured(
331
- "No model key found. The agentic path runs under YOUR account, so it "
332
- "needs your key in this shell:\n"
333
- " ANTHROPIC_API_KEY=... or OPENAI_API_KEY=...\n"
334
- " or GEMINI_API_KEY / GROQ_API_KEY / OPENROUTER_API_KEY /\n"
335
- " AZURE_OPENAI_API_KEY / ALPHAENGINE_API_KEY+ALPHAENGINE_BASE_URL\n"
336
- "Nothing here stores it, and the scripted path (`run <workflow>`) needs "
337
- "no model at all."
331
+ "No model key found. In the session, type `enter model key` and paste it.\n"
332
+ "Or set the key in this shell: ANTHROPIC_API_KEY, OPENAI_API_KEY, "
333
+ "GEMINI_API_KEY, GROQ_API_KEY, OPENROUTER_API_KEY, "
334
+ "AZURE_OPENAI_API_KEY, or ALPHAENGINE_API_KEY with ALPHAENGINE_BASE_URL.\n"
335
+ "Nothing here sends that key to QuantOS, and the scripted path "
336
+ "(`run <workflow>`) needs no model at all."
338
337
  )
339
338
 
340
339