alphaengine 0.7.0__tar.gz → 0.9.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.
Files changed (95) hide show
  1. {alphaengine-0.7.0 → alphaengine-0.9.0}/AGENTS.md +8 -5
  2. {alphaengine-0.7.0 → alphaengine-0.9.0}/PKG-INFO +99 -15
  3. {alphaengine-0.7.0 → alphaengine-0.9.0}/README.md +95 -14
  4. alphaengine-0.9.0/docs/assets/session.png +0 -0
  5. alphaengine-0.9.0/docs/figure_contract.md +46 -0
  6. alphaengine-0.9.0/docs/workflows/evaluate_process.json +18 -0
  7. alphaengine-0.9.0/docs/workflows/research_week.json +29 -0
  8. alphaengine-0.9.0/docs/workflows/stress_study.json +14 -0
  9. {alphaengine-0.7.0 → alphaengine-0.9.0}/pyproject.toml +11 -5
  10. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/__init__.py +4 -2
  11. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/_version.py +14 -9
  12. alphaengine-0.9.0/src/alphaengine/charts.py +56 -0
  13. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/cli.py +211 -372
  14. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/client/executor.py +204 -6
  15. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/client/session.py +40 -0
  16. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/commands.py +22 -0
  17. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/__init__.py +20 -1
  18. alphaengine-0.9.0/src/alphaengine/core/active.py +95 -0
  19. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/allocate.py +30 -6
  20. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/backtest.py +32 -1
  21. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/covariance.py +14 -0
  22. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/factors.py +15 -1
  23. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/pairs.py +9 -0
  24. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/performance.py +38 -0
  25. alphaengine-0.9.0/src/alphaengine/core/process.py +450 -0
  26. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/risk.py +24 -0
  27. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/screen.py +8 -0
  28. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/signals.py +28 -3
  29. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/stress.py +39 -5
  30. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/technical.py +36 -0
  31. alphaengine-0.9.0/src/alphaengine/core/trace.py +136 -0
  32. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/validation.py +56 -3
  33. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/repl.py +1 -0
  34. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/study/report.py +4 -0
  35. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/study/schema.py +7 -1
  36. alphaengine-0.9.0/src/alphaengine/tui/__init__.py +1 -0
  37. alphaengine-0.9.0/src/alphaengine/tui/app.py +379 -0
  38. alphaengine-0.9.0/src/alphaengine/tui/dispatch.py +369 -0
  39. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_cli.py +18 -0
  40. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_client.py +73 -1
  41. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_commands.py +2 -2
  42. alphaengine-0.9.0/tests/test_math_audit.py +253 -0
  43. alphaengine-0.9.0/tests/test_process.py +201 -0
  44. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_sweep.py +27 -0
  45. alphaengine-0.9.0/tests/test_trace.py +45 -0
  46. alphaengine-0.9.0/tests/test_tui.py +127 -0
  47. alphaengine-0.7.0/docs/assets/mark.png +0 -0
  48. alphaengine-0.7.0/docs/assets/session.png +0 -0
  49. {alphaengine-0.7.0 → alphaengine-0.9.0}/.github/workflows/ci.yml +0 -0
  50. {alphaengine-0.7.0 → alphaengine-0.9.0}/.github/workflows/publish.yml +0 -0
  51. {alphaengine-0.7.0 → alphaengine-0.9.0}/.gitignore +0 -0
  52. {alphaengine-0.7.0 → alphaengine-0.9.0}/LICENSE +0 -0
  53. {alphaengine-0.7.0 → alphaengine-0.9.0}/SECURITY.md +0 -0
  54. {alphaengine-0.7.0 → alphaengine-0.9.0}/docs/assets/banner.png +0 -0
  55. {alphaengine-0.7.0 → alphaengine-0.9.0}/docs/assets/data_boundary.png +0 -0
  56. {alphaengine-0.7.0 → alphaengine-0.9.0}/scripts/gen_docs.py +0 -0
  57. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/__main__.py +0 -0
  58. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/agent/__init__.py +0 -0
  59. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/agent/answer.py +0 -0
  60. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/agent/driver.py +0 -0
  61. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/auth.py +0 -0
  62. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/book.py +0 -0
  63. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/client/__init__.py +0 -0
  64. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/client/agent.py +0 -0
  65. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/complete.py +0 -0
  66. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/connectors/__init__.py +0 -0
  67. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/cross_section.py +0 -0
  68. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/panel.py +0 -0
  69. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/profile.py +0 -0
  70. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/series_shapes.py +0 -0
  71. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/walkforward.py +0 -0
  72. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/demo.py +0 -0
  73. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/demo_book.py +0 -0
  74. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/demo_returns.py +0 -0
  75. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/demo_signal.py +0 -0
  76. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/demo_universe.py +0 -0
  77. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/events.py +0 -0
  78. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/loaders.py +0 -0
  79. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/model.py +0 -0
  80. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/py.typed +0 -0
  81. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/study/__init__.py +0 -0
  82. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/sweep/__init__.py +0 -0
  83. {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/sweep/runner.py +0 -0
  84. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_agent.py +0 -0
  85. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_answer.py +0 -0
  86. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_book.py +0 -0
  87. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_events.py +0 -0
  88. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_executor_ops.py +0 -0
  89. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_goldens.py +0 -0
  90. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_loaders.py +0 -0
  91. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_model.py +0 -0
  92. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_quant_maths.py +0 -0
  93. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_screen.py +0 -0
  94. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_signals.py +0 -0
  95. {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_smoke.py +0 -0
@@ -56,13 +56,16 @@ animated parameter-surface canvas at boot and while Working) plus
56
56
  that is a version bump (`_version.py`).
57
57
  - **Compute ops on the executor** include the 0.6 workflow set (`backtest`,
58
58
  `score_backtest`, `cpcv`, `factors`, `pairs`, `cointegrated_pairs`,
59
- `walk_forward`, `book_overlap`) and the 0.7 CS / book set
59
+ `walk_forward`, `book_overlap`), the 0.7 CS / book set
60
60
  (`panel_transform`, `signal_icir`, `fama_macbeth`, `quantile_book`,
61
- `ewma_cov`, `denoise_cov`, `hrp`, `risk_parity`, `vol_target`). Panels and
62
- covariance matrices stay in the workspace; figures go over the wire. The
61
+ `ewma_cov`, `denoise_cov`, `hrp`, `risk_parity`, `vol_target`), and the
62
+ 0.8 process / Grinold set (`ou_calibrate`, `ou_simulate`, `gbm_calibrate`,
63
+ `jump_calibrate`, `garch`, `dgp_stress`, `grinold_alpha`, `breadth_ir`,
64
+ `detone_cov`). Panels, covariance matrices, GARCH variance paths, OU paths
65
+ and alpha vectors stay in the workspace; figures go over the wire. The
63
66
  portal must offer an op in a workflow graph or it sits unused; the CLI
64
- remains useful without them. Short names in a panel are skipped and counted,
65
- they do not shrink the rest of the book.
67
+ `process` command runs the DGP stress offline without a portal. Short names
68
+ in a panel are skipped and counted, they do not shrink the rest of the book.
66
69
  - `sweep(..., jobs=N)` defaults to 1. Greater than 1 is opt-in and must keep
67
70
  trial index identity, including failures.
68
71
  - The `connectors` extra is lazy: parquet via `pyarrow`, HTTP via `httpx` to a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: alphaengine
3
- Version: 0.7.0
3
+ Version: 0.9.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
@@ -36,8 +36,11 @@ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
36
36
  Requires-Dist: pytest>=8.0; extra == 'dev'
37
37
  Requires-Dist: ruff==0.16.1; extra == 'dev'
38
38
  Requires-Dist: statsmodels>=0.14; extra == 'dev'
39
+ Requires-Dist: textual>=1.0; extra == 'dev'
39
40
  Provides-Extra: factors
40
41
  Requires-Dist: statsmodels>=0.14; extra == 'factors'
42
+ Provides-Extra: tui
43
+ Requires-Dist: textual>=1.0; extra == 'tui'
41
44
  Description-Content-Type: text/markdown
42
45
 
43
46
  <p align="center">
@@ -60,28 +63,47 @@ Description-Content-Type: text/markdown
60
63
  <img src="https://img.shields.io/badge/deps-numpy%20%2B%20scipy-C4893A?style=flat-square" alt="Two dependencies: numpy and scipy">
61
64
  </p>
62
65
 
63
- <p align="center">
64
- <img src="docs/assets/mark.png" alt="AlphaEngine mark" width="96">
65
- </p>
66
+ ```bash
67
+ pip install alphaengine # library and one-shot commands
68
+ pip install "alphaengine[tui]" # the full-screen session
69
+ alphaengine demo # offline. no account, no data of yours
70
+ alphaengine # the session
71
+ alphaengine process # fit a DGP to the demo walk and stress it
72
+ ```
73
+
74
+ If the shell cannot find `alphaengine`, this always works:
66
75
 
67
76
  ```bash
68
- pip install alphaengine
69
- alphaengine demo # the whole offline half, no account, no data of your own
77
+ python -m alphaengine demo
78
+ python -m alphaengine
70
79
  ```
71
80
 
72
- <p align="center">
73
- <img src="docs/assets/session.png" alt="The session canvas: a living parameter surface, then demo, login, load" width="92%">
74
- </p>
81
+ ```
82
+ ┌──────────┬─────────────────────────────────────┬──────────────────┐
83
+ │ ● maths │ Ask in plain English. │ sharpe │
84
+ │ always │ │ mean / std √252 │
85
+ │ ○ flows │ > which names are overbought? │ ▁▂▃▅▇█▇▅▃ │
86
+ │ ○ ask │ │ 0 0.0012 │
87
+ │ │ chose screen_universe │ 1 -0.0040 │
88
+ │ loaded │ │ │
89
+ │ sp500 │ │ │
90
+ └──────────┴─────────────────────────────────────┴──────────────────┘
91
+ ```
75
92
 
76
- The session is the product. Sign in, load something, then ask:
93
+ There is one session, and it is this screen. A sentence picks a workflow.
94
+ `run <name>` follows that workflow exactly. The math pane opens when a run
95
+ has a statistic to show.
77
96
 
78
97
  ```bash
79
98
  alphaengine
80
- ❯ login
81
- ❯ load prices.csv
82
- ❯ screen
99
+ > login
100
+ > load prices.csv
101
+ > screen
83
102
  ```
84
103
 
104
+ Prices stay on this machine. A math line's series can travel on its own trace
105
+ so it can be charted. The figures posted with the step stay summaries.
106
+
85
107
  ---
86
108
 
87
109
  ## What it answers
@@ -196,6 +218,46 @@ distinction is the whole of the data boundary below.
196
218
 
197
219
  ---
198
220
 
221
+ ## What's new in 0.9.0
222
+
223
+ `next_open` fills wait for a prior close. The first bar of a next-open backtest
224
+ no longer trades that bar's own signal at that bar's open. Close fills are
225
+ unchanged, and the published goldens do not move.
226
+
227
+ The session is the full-screen terminal (`pip install "alphaengine[tui]"`).
228
+ Each statistic keeps its formula, inputs, and series. Those series post to
229
+ `/api/harness/runs/{id}/traces`. A portal that rejects the trace does not fail
230
+ the run.
231
+
232
+ ## What's new in 0.8.0
233
+
234
+ Named processes, Grinold, and a frozen chart contract. Existing goldens
235
+ (deflated Sharpe, PBO, performance, screen) are byte-identical. New figures
236
+ are a public contract from this release. The study schema is **1.1**: optional
237
+ `process` and `charts` fields; 1.0 studies still load.
238
+
239
+ | You have | You get | Module |
240
+ |---|---|---|
241
+ | A spread or a close | OU / GBM / jump / GARCH(1,1) calibration, then Monte Carlo stress under that law | `core.process` |
242
+ | An IC and a cross-section | Grinold alpha, breadth, transfer coefficient | `core.active` |
243
+ | A return panel | Detoned covariance and a PCA variance-explained table | `core.covariance` |
244
+ | Figures that already travel | `{kind, key, title}` hints a portal maps to a chart | `charts`, [figure contract](docs/figure_contract.md) |
245
+
246
+ The terminal boot is the README banner: tracked `ALPHAENGINE`, the subtitle,
247
+ a teal-left / amber-right mesh (plateau, valley, knife-edge ridge), and
248
+ `your data never leaves.` `alphaengine process` fits a DGP offline, no account.
249
+
250
+ ```python
251
+ from alphaengine.core import ou_calibrate, garch_calibrate, dgp_stress, grinold_alpha
252
+
253
+ ou = ou_calibrate(spread) # kappa, theta, half_life; or not mean-reverting
254
+ g = garch_calibrate(returns) # omega, alpha, beta, persistence in (0, 1)
255
+ s = dgp_stress(close, dgp="ou") # n_trials = n_paths, source monte_carlo
256
+ a = grinold_alpha(panel, ic=0.05) # alpha vector stays here; scalars travel
257
+ ```
258
+
259
+ ---
260
+
199
261
  ## What's new in 0.7.0
200
262
 
201
263
  The daily modelling morning, and the overnight book, without a third
@@ -237,6 +299,7 @@ w = hrp_weights(cov, names=names) # no matrix inverse; weights sum to one
237
299
  | Command | Does | Where |
238
300
  |---|---|---|
239
301
  | `demo` | run the built-in example offline, with no account and no data | shell + session |
302
+ | `process [ou \| gbm \| jump \| garch]` | fit a named process to a series and stress it, offline | shell + session |
240
303
  | `runs [--limit N]` | your own week: what ran, what it decided, what it filed | shell + session |
241
304
  | `gaps` | what your record says is UNANSWERED, and what closes each one | shell + session |
242
305
  | `tonight [--budget N]` | what would run unattended tonight, without running any of it | shell + session |
@@ -320,6 +383,8 @@ alphaengine overlap
320
383
  alphaengine size
321
384
  alphaengine monitor
322
385
  alphaengine demo
386
+ alphaengine process
387
+ alphaengine process ou
323
388
  alphaengine runs
324
389
  alphaengine runs --limit 50
325
390
  alphaengine gaps
@@ -423,8 +488,11 @@ diffable, and versioned so it still parses in two years.
423
488
  | `alphaengine.core.panel` | cross-sectional rank, z-score, winsorize, neutralize |
424
489
  | `alphaengine.core.signals` | IC, ICIR, quantile returns, decay |
425
490
  | `alphaengine.core.cross_section` | Fama-MacBeth, quantile book with turnover |
426
- | `alphaengine.core.covariance` | EWMA, Ledoit-Wolf, Marchenko-Pastur denoise, detone |
427
- | `alphaengine.core.allocate` | HRP, risk parity, vol target |
491
+ | `alphaengine.core.covariance` | EWMA, Ledoit-Wolf, Marchenko-Pastur denoise, detone, variance explained |
492
+ | `alphaengine.core.allocate` | HRP, risk parity, vol target (EWMA or GARCH) |
493
+ | `alphaengine.core.process` | OU, GBM, jumps, GARCH(1,1), DGP stress |
494
+ | `alphaengine.core.active` | Grinold alpha, breadth, transfer coefficient |
495
+ | `alphaengine.charts` | frozen `{kind, key, title}` hints; see [figure contract](docs/figure_contract.md) |
428
496
  | `alphaengine.sweep` | the grid runner and the sensitivity surface |
429
497
  | `alphaengine.study` | the study artifact and its schema |
430
498
  | `alphaengine.client` | the workflow client and the step executor |
@@ -518,6 +586,22 @@ Management* 42(1), 13 to 28.
518
586
  Fama, E. F., and MacBeth, J. D. (1973). "Risk, Return, and Equilibrium:
519
587
  Empirical Tests." *Journal of Political Economy* 81(3), 607 to 636.
520
588
 
589
+ **Ornstein-Uhlenbeck / Vasicek**
590
+ Vasicek, O. (1977). "An Equilibrium Characterization of the Term Structure."
591
+ *Journal of Financial Economics* 5(2), 177 to 188.
592
+
593
+ **GARCH(1,1)**
594
+ Bollerslev, T. (1986). "Generalized Autoregressive Conditional
595
+ Heteroskedasticity." *Journal of Econometrics* 31(3), 307 to 327.
596
+
597
+ **Jump-diffusion**
598
+ Merton, R. C. (1976). "Option Pricing When Underlying Stock Returns Are
599
+ Discontinuous." *Journal of Financial Economics* 3(1-2), 125 to 144.
600
+
601
+ **The fundamental law of active management**
602
+ Grinold, R. C., and Kahn, R. N. (2000). *Active Portfolio Management.* 2nd ed.
603
+ McGraw-Hill.
604
+
521
605
  **Downside deviation**
522
606
  Sortino, F. A., and Price, L. N. (1994). "Performance Measurement in a Downside
523
607
  Risk Framework." *Journal of Investing* 3(3), 59 to 64.
@@ -18,28 +18,47 @@
18
18
  <img src="https://img.shields.io/badge/deps-numpy%20%2B%20scipy-C4893A?style=flat-square" alt="Two dependencies: numpy and scipy">
19
19
  </p>
20
20
 
21
- <p align="center">
22
- <img src="docs/assets/mark.png" alt="AlphaEngine mark" width="96">
23
- </p>
21
+ ```bash
22
+ pip install alphaengine # library and one-shot commands
23
+ pip install "alphaengine[tui]" # the full-screen session
24
+ alphaengine demo # offline. no account, no data of yours
25
+ alphaengine # the session
26
+ alphaengine process # fit a DGP to the demo walk and stress it
27
+ ```
28
+
29
+ If the shell cannot find `alphaengine`, this always works:
24
30
 
25
31
  ```bash
26
- pip install alphaengine
27
- alphaengine demo # the whole offline half, no account, no data of your own
32
+ python -m alphaengine demo
33
+ python -m alphaengine
28
34
  ```
29
35
 
30
- <p align="center">
31
- <img src="docs/assets/session.png" alt="The session canvas: a living parameter surface, then demo, login, load" width="92%">
32
- </p>
36
+ ```
37
+ ┌──────────┬─────────────────────────────────────┬──────────────────┐
38
+ │ ● maths │ Ask in plain English. │ sharpe │
39
+ │ always │ │ mean / std √252 │
40
+ │ ○ flows │ > which names are overbought? │ ▁▂▃▅▇█▇▅▃ │
41
+ │ ○ ask │ │ 0 0.0012 │
42
+ │ │ chose screen_universe │ 1 -0.0040 │
43
+ │ loaded │ │ │
44
+ │ sp500 │ │ │
45
+ └──────────┴─────────────────────────────────────┴──────────────────┘
46
+ ```
33
47
 
34
- The session is the product. Sign in, load something, then ask:
48
+ There is one session, and it is this screen. A sentence picks a workflow.
49
+ `run <name>` follows that workflow exactly. The math pane opens when a run
50
+ has a statistic to show.
35
51
 
36
52
  ```bash
37
53
  alphaengine
38
- ❯ login
39
- ❯ load prices.csv
40
- ❯ screen
54
+ > login
55
+ > load prices.csv
56
+ > screen
41
57
  ```
42
58
 
59
+ Prices stay on this machine. A math line's series can travel on its own trace
60
+ so it can be charted. The figures posted with the step stay summaries.
61
+
43
62
  ---
44
63
 
45
64
  ## What it answers
@@ -154,6 +173,46 @@ distinction is the whole of the data boundary below.
154
173
 
155
174
  ---
156
175
 
176
+ ## What's new in 0.9.0
177
+
178
+ `next_open` fills wait for a prior close. The first bar of a next-open backtest
179
+ no longer trades that bar's own signal at that bar's open. Close fills are
180
+ unchanged, and the published goldens do not move.
181
+
182
+ The session is the full-screen terminal (`pip install "alphaengine[tui]"`).
183
+ Each statistic keeps its formula, inputs, and series. Those series post to
184
+ `/api/harness/runs/{id}/traces`. A portal that rejects the trace does not fail
185
+ the run.
186
+
187
+ ## What's new in 0.8.0
188
+
189
+ Named processes, Grinold, and a frozen chart contract. Existing goldens
190
+ (deflated Sharpe, PBO, performance, screen) are byte-identical. New figures
191
+ are a public contract from this release. The study schema is **1.1**: optional
192
+ `process` and `charts` fields; 1.0 studies still load.
193
+
194
+ | You have | You get | Module |
195
+ |---|---|---|
196
+ | A spread or a close | OU / GBM / jump / GARCH(1,1) calibration, then Monte Carlo stress under that law | `core.process` |
197
+ | An IC and a cross-section | Grinold alpha, breadth, transfer coefficient | `core.active` |
198
+ | A return panel | Detoned covariance and a PCA variance-explained table | `core.covariance` |
199
+ | Figures that already travel | `{kind, key, title}` hints a portal maps to a chart | `charts`, [figure contract](docs/figure_contract.md) |
200
+
201
+ The terminal boot is the README banner: tracked `ALPHAENGINE`, the subtitle,
202
+ a teal-left / amber-right mesh (plateau, valley, knife-edge ridge), and
203
+ `your data never leaves.` `alphaengine process` fits a DGP offline, no account.
204
+
205
+ ```python
206
+ from alphaengine.core import ou_calibrate, garch_calibrate, dgp_stress, grinold_alpha
207
+
208
+ ou = ou_calibrate(spread) # kappa, theta, half_life; or not mean-reverting
209
+ g = garch_calibrate(returns) # omega, alpha, beta, persistence in (0, 1)
210
+ s = dgp_stress(close, dgp="ou") # n_trials = n_paths, source monte_carlo
211
+ a = grinold_alpha(panel, ic=0.05) # alpha vector stays here; scalars travel
212
+ ```
213
+
214
+ ---
215
+
157
216
  ## What's new in 0.7.0
158
217
 
159
218
  The daily modelling morning, and the overnight book, without a third
@@ -195,6 +254,7 @@ w = hrp_weights(cov, names=names) # no matrix inverse; weights sum to one
195
254
  | Command | Does | Where |
196
255
  |---|---|---|
197
256
  | `demo` | run the built-in example offline, with no account and no data | shell + session |
257
+ | `process [ou \| gbm \| jump \| garch]` | fit a named process to a series and stress it, offline | shell + session |
198
258
  | `runs [--limit N]` | your own week: what ran, what it decided, what it filed | shell + session |
199
259
  | `gaps` | what your record says is UNANSWERED, and what closes each one | shell + session |
200
260
  | `tonight [--budget N]` | what would run unattended tonight, without running any of it | shell + session |
@@ -278,6 +338,8 @@ alphaengine overlap
278
338
  alphaengine size
279
339
  alphaengine monitor
280
340
  alphaengine demo
341
+ alphaengine process
342
+ alphaengine process ou
281
343
  alphaengine runs
282
344
  alphaengine runs --limit 50
283
345
  alphaengine gaps
@@ -381,8 +443,11 @@ diffable, and versioned so it still parses in two years.
381
443
  | `alphaengine.core.panel` | cross-sectional rank, z-score, winsorize, neutralize |
382
444
  | `alphaengine.core.signals` | IC, ICIR, quantile returns, decay |
383
445
  | `alphaengine.core.cross_section` | Fama-MacBeth, quantile book with turnover |
384
- | `alphaengine.core.covariance` | EWMA, Ledoit-Wolf, Marchenko-Pastur denoise, detone |
385
- | `alphaengine.core.allocate` | HRP, risk parity, vol target |
446
+ | `alphaengine.core.covariance` | EWMA, Ledoit-Wolf, Marchenko-Pastur denoise, detone, variance explained |
447
+ | `alphaengine.core.allocate` | HRP, risk parity, vol target (EWMA or GARCH) |
448
+ | `alphaengine.core.process` | OU, GBM, jumps, GARCH(1,1), DGP stress |
449
+ | `alphaengine.core.active` | Grinold alpha, breadth, transfer coefficient |
450
+ | `alphaengine.charts` | frozen `{kind, key, title}` hints; see [figure contract](docs/figure_contract.md) |
386
451
  | `alphaengine.sweep` | the grid runner and the sensitivity surface |
387
452
  | `alphaengine.study` | the study artifact and its schema |
388
453
  | `alphaengine.client` | the workflow client and the step executor |
@@ -476,6 +541,22 @@ Management* 42(1), 13 to 28.
476
541
  Fama, E. F., and MacBeth, J. D. (1973). "Risk, Return, and Equilibrium:
477
542
  Empirical Tests." *Journal of Political Economy* 81(3), 607 to 636.
478
543
 
544
+ **Ornstein-Uhlenbeck / Vasicek**
545
+ Vasicek, O. (1977). "An Equilibrium Characterization of the Term Structure."
546
+ *Journal of Financial Economics* 5(2), 177 to 188.
547
+
548
+ **GARCH(1,1)**
549
+ Bollerslev, T. (1986). "Generalized Autoregressive Conditional
550
+ Heteroskedasticity." *Journal of Econometrics* 31(3), 307 to 327.
551
+
552
+ **Jump-diffusion**
553
+ Merton, R. C. (1976). "Option Pricing When Underlying Stock Returns Are
554
+ Discontinuous." *Journal of Financial Economics* 3(1-2), 125 to 144.
555
+
556
+ **The fundamental law of active management**
557
+ Grinold, R. C., and Kahn, R. N. (2000). *Active Portfolio Management.* 2nd ed.
558
+ McGraw-Hill.
559
+
479
560
  **Downside deviation**
480
561
  Sortino, F. A., and Price, L. N. (1994). "Performance Measurement in a Downside
481
562
  Risk Framework." *Journal of Investing* 3(3), 59 to 64.
@@ -0,0 +1,46 @@
1
+ # Figure contract
2
+
3
+ A figure that travels is a **named kind**, not a series. The executor attaches
4
+ `charts: [{kind, key, title}]` hints. The portal maps `kind` + `key` to a
5
+ component. Old portals ignore `charts` and still have the raw keys
6
+ (`ic_by_period`, `best_curve`, …).
7
+
8
+ No hint carries a nested series. Titles are one line, no em dash.
9
+
10
+ Study schema **1.1** may also store `charts` and `process` on the artifact.
11
+ Both are optional. A 1.0 study still loads.
12
+
13
+ ## Kinds
14
+
15
+ | Kind | Payload | Draws as |
16
+ |---|---|---|
17
+ | `curve` | `{i, v}` | line |
18
+ | `period` | `{p, ic}` or `{p, lambda}` | CS time series |
19
+ | `band` | `{i, lo, mid, hi}` | fan / Monte Carlo quantile |
20
+ | `hist` | `{edges, counts}` | histogram |
21
+ | `cost` | `{bps, sharpe}` | cost ladder |
22
+ | `scatter` | `{x, y}` | overlap |
23
+ | `triangle` | `{a, b, v}` | covariance heatmap |
24
+ | `rows` | bounded dict rows | table |
25
+ | `quantiles` | `[float]` | bar |
26
+
27
+ `i` is a position index into the original series, never a timestamp. Timestamps
28
+ stay on the machine that holds the data.
29
+
30
+ Lists longer than 512 are a series, not a figure, and the wire guard refuses
31
+ them. Sketches (`path_sketch`, `vol_path`, `mean_path`, `band`) are bucketed
32
+ below that cap before they travel.
33
+
34
+ ## Who emits what
35
+
36
+ | Op | Typical hints |
37
+ |---|---|
38
+ | `compute.ou_calibrate` | `curve` / `path_sketch` |
39
+ | `compute.ou_simulate` | `curve` / `mean_path`, `band` / `band` |
40
+ | `compute.garch` | `curve` / `vol_path` |
41
+ | `compute.dgp_stress` | `hist` / `sharpe_hist` |
42
+ | `compute.denoise_cov` | `triangle` / `triangle`, `rows` / `variance_explained` |
43
+ | `compute.detone_cov` | same |
44
+ | `compute.grinold_alpha` | scalars only; the alpha vector stays in the workspace |
45
+
46
+ The Python helper is `alphaengine.charts.chart(kind, key, title)`.
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "evaluate_process",
3
+ "schema": "alphaengine.workflow/1",
4
+ "title": "what law describes this path?",
5
+ "reproducible": true,
6
+ "needs": ["data"],
7
+ "selection": "all",
8
+ "note": "Portal harness copies this. Agents still only index permitted; these ops sit unused until registered.",
9
+ "stages": [
10
+ {"op": "data.resolve"},
11
+ {"op": "compute.gbm_calibrate"},
12
+ {"op": "compute.ou_calibrate"},
13
+ {"op": "compute.jump_calibrate"},
14
+ {"op": "compute.garch"},
15
+ {"op": "compute.dgp_stress", "params": {"dgp": "gbm"}},
16
+ {"op": "emit.process_report"}
17
+ ]
18
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "research_week",
3
+ "schema": "alphaengine.workflow/1",
4
+ "title": "gaps the modeling suite closes once the portal registers the ops",
5
+ "gaps": [
6
+ {
7
+ "question": "what law describes this path?",
8
+ "workflow": "evaluate_process",
9
+ "op": "compute.dgp_stress",
10
+ "cli": "alphaengine process"
11
+ },
12
+ {
13
+ "question": "where does it break under a named DGP?",
14
+ "workflow": "stress_study",
15
+ "stage": "dgp_stress",
16
+ "cli": "alphaengine process ou"
17
+ },
18
+ {
19
+ "question": "how much alpha does the IC imply?",
20
+ "workflow": "size_position",
21
+ "op": "compute.grinold_alpha"
22
+ },
23
+ {
24
+ "question": "is the book the market in disguise?",
25
+ "workflow": "size_position",
26
+ "op": "compute.detone_cov"
27
+ }
28
+ ]
29
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "stress_study",
3
+ "schema": "alphaengine.workflow/1",
4
+ "title": "where does it break?",
5
+ "note": "Additive DGP stage for the existing stress_study graph. Copy into the portal harness. The CLI spelling that does not wait on the portal is `alphaengine process` or `stress --input dgp=ou` once this stage is live.",
6
+ "add_stage": {
7
+ "id": "dgp_stress",
8
+ "op": "compute.dgp_stress",
9
+ "input": "dgp",
10
+ "default": "gbm",
11
+ "values": ["ou", "gbm", "jump", "garch"],
12
+ "after": "compute.cost_ladder"
13
+ }
14
+ }
@@ -42,10 +42,10 @@ dependencies = [
42
42
  "scipy>=1.10",
43
43
  ]
44
44
 
45
- # THE TERMINAL ENTRY POINT. Deliberately adds nothing new: argparse from the
46
- # standard library, and the client's urllib transport. A CLI that dragged in a
47
- # TUI framework would break the two-dependency promise above on behalf of people
48
- # who may never run it.
45
+ # THE TERMINAL ENTRY POINT. One-shot commands add nothing: argparse from the
46
+ # standard library, and the client's urllib transport. The full-screen session
47
+ # is the optional `tui` extra, so `import alphaengine` and a library install
48
+ # stay at numpy and scipy.
49
49
  #
50
50
  # INSTALL THIS IN YOUR PROJECT VENV, NOT WITH pipx OR `uv tool install`.
51
51
  # `compute.*` ops execute in-process against your own DataFrames, so tool
@@ -75,6 +75,11 @@ connectors = [
75
75
  "httpx>=0.27",
76
76
  "pyarrow>=15.0",
77
77
  ]
78
+ # The interactive session. Not a core dependency: a researcher who only wants
79
+ # a deflated Sharpe should not install a TUI framework to get one.
80
+ tui = [
81
+ "textual>=1.0",
82
+ ]
78
83
  dev = [
79
84
  "pytest>=8.0",
80
85
  "pytest-cov>=5.0",
@@ -88,6 +93,7 @@ dev = [
88
93
  # this, mypy cannot resolve statsmodels in a clean environment even though
89
94
  # it resolves fine on a machine that happens to have it.
90
95
  "statsmodels>=0.14",
96
+ "textual>=1.0",
91
97
  ]
92
98
 
93
99
  [project.urls]
@@ -179,5 +185,5 @@ ignore_missing_imports = true
179
185
  # import is an error, so this is the difference between a build that passes on a
180
186
  # machine that happens to have them and one that passes anywhere.
181
187
  [[tool.mypy.overrides]]
182
- module = ["scipy.*", "statsmodels.*", "pyarrow.*", "httpx"]
188
+ module = ["scipy.*", "statsmodels.*", "pyarrow.*", "httpx", "textual.*"]
183
189
  ignore_missing_imports = true
@@ -23,8 +23,10 @@ WHAT THIS LIBRARY IS NOT
23
23
 
24
24
  OFFLINE BY CONSTRUCTION
25
25
  Importing this module makes no network call and needs no account. numpy and
26
- scipy, nothing else. Everything above runs on a laptop with the wifi off,
27
- and your data never leaves the machine.
26
+ scipy, nothing else. Everything above runs on a laptop with the wifi off.
27
+ A workflow run can also send a math trace — the formula, its inputs, and
28
+ the series that line was computed on — to the portal on its own channel.
29
+ Study ingest still refuses anything series-shaped.
28
30
  """
29
31
 
30
32
  from typing import Any
@@ -63,12 +63,17 @@ is what a changed figure costs. The API may still move underneath it.
63
63
  # doors, same refusals. The published artefact is a new wheel because 0.6.0
64
64
  # already left the building.
65
65
  #
66
- # 0.7.0 IS NEW PUBLIC FIGURES, AND A SIMPLER OS AROUND THEM. Existing DSR /
67
- # PBO / performance / screen goldens stay byte-identical. The artefact grew
68
- # the daily cross-section (rank, z-score, winsorize, neutralize, ICIR,
69
- # Fama-MacBeth, quantile book with turnover) and the overnight book
70
- # (EWMA / Ledoit-Wolf / Marchenko-Pastur covariance, HRP, risk parity, vol
71
- # target), plus a living parameter-surface canvas and one-verb `login` /
72
- # `load`. New numbers are a public contract from this release; old numbers
73
- # did not move.
74
- __version__ = "0.7.0"
66
+ # 0.8.0 IS THE MODELING SUITE THE REST OF THE LIBRARY WAS MISSING. Existing
67
+ # DSR / PBO / performance / screen goldens stay byte-identical. The artefact
68
+ # grew named processes (OU, GBM, jumps, GARCH), DGP stress with an honest
69
+ # Monte Carlo trial count, Grinold alpha / breadth / TC, a frozen chart
70
+ # vocabulary, and a terminal boot that is the README banner in glyphs.
71
+ # New figures are a public contract from this release; old numbers did not move.
72
+ #
73
+ # 0.9.0: `run_backtest(..., fill_timing="next_open")` no longer fills the first
74
+ # bar with that bar's own signal. A signal is known at the close, and the open
75
+ # of the same bar printed before it. The first fill is now the next bar's open.
76
+ # Close fills are unchanged. No golden in tests/test_goldens.py moves. The
77
+ # same release makes the full-screen terminal the only session, and posts each
78
+ # math line's series on `/traces` without putting those series inside figures.
79
+ __version__ = "0.9.0"
@@ -0,0 +1,56 @@
1
+ """Frozen chart vocabulary the portal draws from figures.
2
+
3
+ A FIGURE IS A NAMED KIND, not a series. The executor attaches
4
+ `charts: [{kind, key, title}]` hints. The portal maps kind+key to a component.
5
+ Old portals ignore `charts` and still have the raw keys (`ic_by_period`,
6
+ `best_curve`, ...).
7
+
8
+ KINDS
9
+ curve {i, v} line
10
+ period {p, ic|lambda} CS time series
11
+ band {i, lo, mid, hi} fan / Monte Carlo quantile
12
+ hist {edges, counts} histogram
13
+ cost {bps, sharpe} cost ladder
14
+ scatter {x, y} overlap
15
+ triangle {a, b, v} covariance heatmap
16
+ rows bounded dict rows table
17
+ quantiles [float] bar
18
+
19
+ No hint may carry a nested series. Titles are one line, no em dash.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ __all__ = ["CHART_KINDS", "chart", "charts"]
25
+
26
+ CHART_KINDS = frozenset(
27
+ {
28
+ "curve",
29
+ "period",
30
+ "band",
31
+ "hist",
32
+ "cost",
33
+ "scatter",
34
+ "triangle",
35
+ "rows",
36
+ "quantiles",
37
+ }
38
+ )
39
+
40
+
41
+ def chart(kind: str, key: str, title: str) -> dict[str, str]:
42
+ """One portal hint. `kind` must be in CHART_KINDS."""
43
+ k = str(kind)
44
+ if k not in CHART_KINDS:
45
+ raise ValueError(f"unknown chart kind {k!r}; expected one of {sorted(CHART_KINDS)}")
46
+ return {"kind": k, "key": str(key), "title": str(title)}
47
+
48
+
49
+ def charts(*items: dict[str, str]) -> list[dict[str, str]]:
50
+ """Hint list. Empty is allowed: the portal then uses raw keys only."""
51
+ out: list[dict[str, str]] = []
52
+ for item in items:
53
+ if not item:
54
+ continue
55
+ out.append(chart(item["kind"], item["key"], item["title"]) if "kind" in item else dict(item))
56
+ return out