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.
- {alphaengine-0.7.0 → alphaengine-0.9.0}/AGENTS.md +8 -5
- {alphaengine-0.7.0 → alphaengine-0.9.0}/PKG-INFO +99 -15
- {alphaengine-0.7.0 → alphaengine-0.9.0}/README.md +95 -14
- alphaengine-0.9.0/docs/assets/session.png +0 -0
- alphaengine-0.9.0/docs/figure_contract.md +46 -0
- alphaengine-0.9.0/docs/workflows/evaluate_process.json +18 -0
- alphaengine-0.9.0/docs/workflows/research_week.json +29 -0
- alphaengine-0.9.0/docs/workflows/stress_study.json +14 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/pyproject.toml +11 -5
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/__init__.py +4 -2
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/_version.py +14 -9
- alphaengine-0.9.0/src/alphaengine/charts.py +56 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/cli.py +211 -372
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/client/executor.py +204 -6
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/client/session.py +40 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/commands.py +22 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/__init__.py +20 -1
- alphaengine-0.9.0/src/alphaengine/core/active.py +95 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/allocate.py +30 -6
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/backtest.py +32 -1
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/covariance.py +14 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/factors.py +15 -1
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/pairs.py +9 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/performance.py +38 -0
- alphaengine-0.9.0/src/alphaengine/core/process.py +450 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/risk.py +24 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/screen.py +8 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/signals.py +28 -3
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/stress.py +39 -5
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/technical.py +36 -0
- alphaengine-0.9.0/src/alphaengine/core/trace.py +136 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/validation.py +56 -3
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/repl.py +1 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/study/report.py +4 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/study/schema.py +7 -1
- alphaengine-0.9.0/src/alphaengine/tui/__init__.py +1 -0
- alphaengine-0.9.0/src/alphaengine/tui/app.py +379 -0
- alphaengine-0.9.0/src/alphaengine/tui/dispatch.py +369 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_cli.py +18 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_client.py +73 -1
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_commands.py +2 -2
- alphaengine-0.9.0/tests/test_math_audit.py +253 -0
- alphaengine-0.9.0/tests/test_process.py +201 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_sweep.py +27 -0
- alphaengine-0.9.0/tests/test_trace.py +45 -0
- alphaengine-0.9.0/tests/test_tui.py +127 -0
- alphaengine-0.7.0/docs/assets/mark.png +0 -0
- alphaengine-0.7.0/docs/assets/session.png +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/.github/workflows/ci.yml +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/.github/workflows/publish.yml +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/.gitignore +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/LICENSE +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/SECURITY.md +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/docs/assets/banner.png +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/docs/assets/data_boundary.png +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/scripts/gen_docs.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/__main__.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/agent/__init__.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/agent/answer.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/agent/driver.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/auth.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/book.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/client/__init__.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/client/agent.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/complete.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/connectors/__init__.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/cross_section.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/panel.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/profile.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/series_shapes.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/core/walkforward.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/demo.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/demo_book.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/demo_returns.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/demo_signal.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/demo_universe.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/events.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/loaders.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/model.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/py.typed +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/study/__init__.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/sweep/__init__.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/src/alphaengine/sweep/runner.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_agent.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_answer.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_book.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_events.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_executor_ops.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_goldens.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_loaders.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_model.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_quant_maths.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_screen.py +0 -0
- {alphaengine-0.7.0 → alphaengine-0.9.0}/tests/test_signals.py +0 -0
- {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`)
|
|
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`)
|
|
62
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
|
|
77
|
+
python -m alphaengine demo
|
|
78
|
+
python -m alphaengine
|
|
70
79
|
```
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
32
|
+
python -m alphaengine demo
|
|
33
|
+
python -m alphaengine
|
|
28
34
|
```
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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.
|
|
Binary file
|
|
@@ -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.
|
|
46
|
-
# standard library, and the client's urllib transport.
|
|
47
|
-
#
|
|
48
|
-
#
|
|
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
|
-
|
|
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.
|
|
67
|
-
# PBO / performance / screen goldens stay byte-identical. The artefact
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
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
|