alphaengine 0.4.0__tar.gz → 0.6.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.6.0/AGENTS.md +55 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/PKG-INFO +18 -6
- {alphaengine-0.4.0 → alphaengine-0.6.0}/README.md +15 -4
- {alphaengine-0.4.0 → alphaengine-0.6.0}/pyproject.toml +2 -1
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/_version.py +28 -1
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/auth.py +16 -1
- alphaengine-0.6.0/src/alphaengine/book.py +158 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/cli.py +537 -257
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/client/agent.py +41 -6
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/client/executor.py +201 -1
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/client/session.py +37 -56
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/commands.py +101 -11
- alphaengine-0.6.0/src/alphaengine/complete.py +71 -0
- alphaengine-0.6.0/src/alphaengine/connectors/__init__.py +75 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/technical.py +6 -4
- alphaengine-0.6.0/src/alphaengine/core/walkforward.py +139 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/demo.py +8 -0
- alphaengine-0.6.0/src/alphaengine/events.py +195 -0
- alphaengine-0.6.0/src/alphaengine/model.py +500 -0
- alphaengine-0.6.0/src/alphaengine/repl.py +75 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/sweep/runner.py +37 -18
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_agent.py +8 -0
- alphaengine-0.6.0/tests/test_book.py +40 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_cli.py +27 -0
- alphaengine-0.6.0/tests/test_events.py +57 -0
- alphaengine-0.6.0/tests/test_executor_ops.py +62 -0
- alphaengine-0.6.0/tests/test_model.py +96 -0
- alphaengine-0.4.0/src/alphaengine/model.py +0 -248
- {alphaengine-0.4.0 → alphaengine-0.6.0}/.github/workflows/ci.yml +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/.github/workflows/publish.yml +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/.gitignore +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/LICENSE +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/SECURITY.md +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/scripts/gen_docs.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/__main__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/agent/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/agent/answer.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/agent/driver.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/client/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/backtest.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/factors.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/pairs.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/performance.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/profile.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/risk.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/screen.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/series_shapes.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/signals.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/stress.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/core/validation.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/demo_book.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/demo_returns.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/demo_signal.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/demo_universe.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/loaders.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/py.typed +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/study/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/study/report.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/study/schema.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/src/alphaengine/sweep/__init__.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_answer.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_client.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_commands.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_goldens.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_loaders.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_screen.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_signals.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_smoke.py +0 -0
- {alphaengine-0.4.0 → alphaengine-0.6.0}/tests/test_sweep.py +0 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## Cursor Cloud specific instructions
|
|
4
|
+
|
|
5
|
+
AlphaEngine is a pure-Python CLI + library (`alphaengine`) for validated quant
|
|
6
|
+
research. There is no GUI; all interaction is via the terminal or
|
|
7
|
+
`import alphaengine`.
|
|
8
|
+
|
|
9
|
+
### Environment
|
|
10
|
+
|
|
11
|
+
- Dependencies live in `.venv` (Python 3.12). Use `.venv/bin/<tool>` or
|
|
12
|
+
`source .venv/bin/activate`. The console script is `.venv/bin/alphaengine`.
|
|
13
|
+
- Installed editable with `dev` and `factors` extras so statsmodels is present.
|
|
14
|
+
|
|
15
|
+
### Lint / test / build / run
|
|
16
|
+
|
|
17
|
+
- Lint: `.venv/bin/python -m ruff check src tests` and
|
|
18
|
+
`.venv/bin/python -m ruff format --check src tests`
|
|
19
|
+
- Types: `.venv/bin/python -m mypy src`
|
|
20
|
+
- Tests: `.venv/bin/python -m pytest` and
|
|
21
|
+
`.venv/bin/python -m pytest tests -m golden -q`
|
|
22
|
+
- Docs guard: `.venv/bin/python scripts/gen_docs.py --check`
|
|
23
|
+
(regenerate with `--write`). A benign `SyntaxWarning: invalid escape sequence '\|'`
|
|
24
|
+
is pre-existing.
|
|
25
|
+
- Offline app: `.venv/bin/alphaengine demo`
|
|
26
|
+
|
|
27
|
+
### Non-obvious gotchas
|
|
28
|
+
|
|
29
|
+
- **Three rungs.** `demo` and the importable library are fully offline. Workflow
|
|
30
|
+
CLI verbs (`diagnose`, `screen`, …, `run <workflow>`) need a portal
|
|
31
|
+
`ae_live_` key (`QUANTOS_API_KEY`). Plain-English mode also needs a **model**
|
|
32
|
+
key. Any OpenAI-compatible key works: Anthropic, OpenAI (`OPENAI_BASE_URL` for
|
|
33
|
+
gateways), Gemini, Groq, OpenRouter, Azure, or
|
|
34
|
+
`ALPHAENGINE_API_KEY`+`ALPHAENGINE_BASE_URL`. `alphaengine models` lists what
|
|
35
|
+
this machine can actually use. Keys may persist in
|
|
36
|
+
`~/.config/alphaengine/credentials.json` (mode 0600) if the user says yes at
|
|
37
|
+
`key <provider>`; env always wins. LLM keys are never sent to QuantOS.
|
|
38
|
+
- **A stop / `marginal` verdict exits 0.** Only a step that could not execute
|
|
39
|
+
exits non-zero. Unauthenticated workflow calls exit 2.
|
|
40
|
+
- **Data never leaves.** Executor and study-report guards refuse lists longer
|
|
41
|
+
than 512 by length. Model telemetry (choices, why, post-guard answers, prompt
|
|
42
|
+
hashes) may POST to `/api/harness/runs/{id}/events` when logged in; a 404 is
|
|
43
|
+
swallowed and the same allowlist is always written to
|
|
44
|
+
`~/.local/share/alphaengine/runs/<id>.jsonl`. Never send prompts, series, or
|
|
45
|
+
keys. `alphaengine trace` reads the local dump.
|
|
46
|
+
- **Goldens are a public contract.** Do not "fix" a golden to land a speedup;
|
|
47
|
+
that is a version bump (`_version.py`).
|
|
48
|
+
- **New compute ops** (`backtest`, `score_backtest`, `cpcv`, `factors`, `pairs`,
|
|
49
|
+
`cointegrated_pairs`, `walk_forward`, `book_overlap`) are on the executor.
|
|
50
|
+
The portal must offer them in a workflow graph or they sit unused; the CLI
|
|
51
|
+
remains useful without them.
|
|
52
|
+
- `sweep(..., jobs=N)` defaults to 1. Greater than 1 is opt-in and must keep
|
|
53
|
+
trial index identity, including failures.
|
|
54
|
+
- The `connectors` extra is lazy: parquet via `pyarrow`, HTTP via `httpx` to a
|
|
55
|
+
URL the caller names. `import alphaengine` must not import httpx.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: alphaengine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -26,6 +26,7 @@ Requires-Dist: numpy>=1.24
|
|
|
26
26
|
Requires-Dist: scipy>=1.10
|
|
27
27
|
Provides-Extra: agents
|
|
28
28
|
Requires-Dist: anthropic>=0.40; extra == 'agents'
|
|
29
|
+
Requires-Dist: openai>=1.0; extra == 'agents'
|
|
29
30
|
Provides-Extra: connectors
|
|
30
31
|
Requires-Dist: httpx>=0.27; extra == 'connectors'
|
|
31
32
|
Requires-Dist: pyarrow>=15.0; extra == 'connectors'
|
|
@@ -150,9 +151,14 @@ distinction is the whole of the data boundary below.
|
|
|
150
151
|
|---|---|---|
|
|
151
152
|
| `demo` | run the built-in example offline, with no account and no data | shell + session |
|
|
152
153
|
| `runs [--limit N]` | your own week: what ran, what it decided, what it filed | shell + session |
|
|
154
|
+
| `gaps` | what your record says is UNANSWERED, and what closes each one | shell + session |
|
|
155
|
+
| `tonight [--budget N]` | what would run unattended tonight, without running any of it | shell + session |
|
|
153
156
|
| `workflows` | what the server offers, what each needs, and which reproduce | shell + session |
|
|
154
|
-
| `key [quantos \| anthropic \| openai]` | enter a credential now, or see which rungs are unlocked | session |
|
|
157
|
+
| `key [quantos \| anthropic \| openai \| gemini \| groq \| azure \| openrouter \| gateway]` | enter a credential now, or see which rungs are unlocked | session |
|
|
155
158
|
| `commands [verb]` | this directory, or one command in full | shell + session |
|
|
159
|
+
| `models` | which model providers this machine can actually use | shell + session |
|
|
160
|
+
| `model [<provider[:name]>]` | pin the model for this session, or show the pin | session |
|
|
161
|
+
| `trace [run_id]` | local model/run events for a run, hashed prompts only | shell + session |
|
|
156
162
|
|
|
157
163
|
### Do the work
|
|
158
164
|
|
|
@@ -174,8 +180,9 @@ distinction is the whole of the data boundary below.
|
|
|
174
180
|
|
|
175
181
|
| Command | Does | Where |
|
|
176
182
|
|---|---|---|
|
|
183
|
+
| `book [<name> \| status]` | show or load sleeves on the multi-strategy book | session |
|
|
177
184
|
| `universe <name>` | load a universe you registered in the portal, with its closes | session |
|
|
178
|
-
| `data <file
|
|
185
|
+
| `data <file>` | load a local CSV or parquet without leaving the session | session |
|
|
179
186
|
| `project <module>` | load `data` and `backtest_fn` from a module of yours | session |
|
|
180
187
|
|
|
181
188
|
### Session
|
|
@@ -205,12 +212,12 @@ distinction is the whole of the data boundary below.
|
|
|
205
212
|
| `--data FILE` | a local CSV: wide, long, or a single series |
|
|
206
213
|
| `--universe NAME` | a universe registered in the portal, with its stored closes |
|
|
207
214
|
| `--symbol TICKER` | one name out of a loaded universe; its closes become the return series |
|
|
208
|
-
| `--sleeve NAME` | the sleeve this run belongs to, and the budget it is bound by |
|
|
209
|
-
| `--thesis ID` | a draft thesis to propose this run's shortlist as a sleeve for |
|
|
210
215
|
| `--label TEXT` | what to call the artifact this run produces |
|
|
211
216
|
| `--input K=V` | a workflow input; repeatable |
|
|
212
217
|
| `--quiet` | only the result, no step narration |
|
|
218
|
+
| `--stream` | print the answer once the citation guard has passed |
|
|
213
219
|
| `--limit N` | how many rows to show (default 25) |
|
|
220
|
+
| `--budget N` | how many runs a night is worth (default 3) |
|
|
214
221
|
|
|
215
222
|
### Examples
|
|
216
223
|
|
|
@@ -226,11 +233,16 @@ alphaengine monitor
|
|
|
226
233
|
alphaengine demo
|
|
227
234
|
alphaengine runs
|
|
228
235
|
alphaengine runs --limit 50
|
|
236
|
+
alphaengine gaps
|
|
237
|
+
alphaengine tonight
|
|
238
|
+
alphaengine tonight --budget 5
|
|
229
239
|
alphaengine workflows
|
|
230
240
|
alphaengine commands
|
|
231
241
|
alphaengine run screen_universe --universe sp500
|
|
232
242
|
alphaengine run size_position --data returns.csv
|
|
233
243
|
alphaengine run validate_study --project research.momentum
|
|
244
|
+
alphaengine models
|
|
245
|
+
alphaengine trace
|
|
234
246
|
alphaengine logout
|
|
235
247
|
alphaengine version
|
|
236
248
|
```
|
|
@@ -109,9 +109,14 @@ distinction is the whole of the data boundary below.
|
|
|
109
109
|
|---|---|---|
|
|
110
110
|
| `demo` | run the built-in example offline, with no account and no data | shell + session |
|
|
111
111
|
| `runs [--limit N]` | your own week: what ran, what it decided, what it filed | shell + session |
|
|
112
|
+
| `gaps` | what your record says is UNANSWERED, and what closes each one | shell + session |
|
|
113
|
+
| `tonight [--budget N]` | what would run unattended tonight, without running any of it | shell + session |
|
|
112
114
|
| `workflows` | what the server offers, what each needs, and which reproduce | shell + session |
|
|
113
|
-
| `key [quantos \| anthropic \| openai]` | enter a credential now, or see which rungs are unlocked | session |
|
|
115
|
+
| `key [quantos \| anthropic \| openai \| gemini \| groq \| azure \| openrouter \| gateway]` | enter a credential now, or see which rungs are unlocked | session |
|
|
114
116
|
| `commands [verb]` | this directory, or one command in full | shell + session |
|
|
117
|
+
| `models` | which model providers this machine can actually use | shell + session |
|
|
118
|
+
| `model [<provider[:name]>]` | pin the model for this session, or show the pin | session |
|
|
119
|
+
| `trace [run_id]` | local model/run events for a run, hashed prompts only | shell + session |
|
|
115
120
|
|
|
116
121
|
### Do the work
|
|
117
122
|
|
|
@@ -133,8 +138,9 @@ distinction is the whole of the data boundary below.
|
|
|
133
138
|
|
|
134
139
|
| Command | Does | Where |
|
|
135
140
|
|---|---|---|
|
|
141
|
+
| `book [<name> \| status]` | show or load sleeves on the multi-strategy book | session |
|
|
136
142
|
| `universe <name>` | load a universe you registered in the portal, with its closes | session |
|
|
137
|
-
| `data <file
|
|
143
|
+
| `data <file>` | load a local CSV or parquet without leaving the session | session |
|
|
138
144
|
| `project <module>` | load `data` and `backtest_fn` from a module of yours | session |
|
|
139
145
|
|
|
140
146
|
### Session
|
|
@@ -164,12 +170,12 @@ distinction is the whole of the data boundary below.
|
|
|
164
170
|
| `--data FILE` | a local CSV: wide, long, or a single series |
|
|
165
171
|
| `--universe NAME` | a universe registered in the portal, with its stored closes |
|
|
166
172
|
| `--symbol TICKER` | one name out of a loaded universe; its closes become the return series |
|
|
167
|
-
| `--sleeve NAME` | the sleeve this run belongs to, and the budget it is bound by |
|
|
168
|
-
| `--thesis ID` | a draft thesis to propose this run's shortlist as a sleeve for |
|
|
169
173
|
| `--label TEXT` | what to call the artifact this run produces |
|
|
170
174
|
| `--input K=V` | a workflow input; repeatable |
|
|
171
175
|
| `--quiet` | only the result, no step narration |
|
|
176
|
+
| `--stream` | print the answer once the citation guard has passed |
|
|
172
177
|
| `--limit N` | how many rows to show (default 25) |
|
|
178
|
+
| `--budget N` | how many runs a night is worth (default 3) |
|
|
173
179
|
|
|
174
180
|
### Examples
|
|
175
181
|
|
|
@@ -185,11 +191,16 @@ alphaengine monitor
|
|
|
185
191
|
alphaengine demo
|
|
186
192
|
alphaengine runs
|
|
187
193
|
alphaengine runs --limit 50
|
|
194
|
+
alphaengine gaps
|
|
195
|
+
alphaengine tonight
|
|
196
|
+
alphaengine tonight --budget 5
|
|
188
197
|
alphaengine workflows
|
|
189
198
|
alphaengine commands
|
|
190
199
|
alphaengine run screen_universe --universe sp500
|
|
191
200
|
alphaengine run size_position --data returns.csv
|
|
192
201
|
alphaengine run validate_study --project research.momentum
|
|
202
|
+
alphaengine models
|
|
203
|
+
alphaengine trace
|
|
193
204
|
alphaengine logout
|
|
194
205
|
alphaengine version
|
|
195
206
|
```
|
|
@@ -68,6 +68,7 @@ factors = [
|
|
|
68
68
|
# Sharpe should not need an LLM dependency to get one.
|
|
69
69
|
agents = [
|
|
70
70
|
"anthropic>=0.40",
|
|
71
|
+
"openai>=1.0",
|
|
71
72
|
]
|
|
72
73
|
# Pulling data from a provider the customer already pays for.
|
|
73
74
|
connectors = [
|
|
@@ -178,5 +179,5 @@ ignore_missing_imports = true
|
|
|
178
179
|
# import is an error, so this is the difference between a build that passes on a
|
|
179
180
|
# machine that happens to have them and one that passes anywhere.
|
|
180
181
|
[[tool.mypy.overrides]]
|
|
181
|
-
module = ["scipy.*", "statsmodels.*"]
|
|
182
|
+
module = ["scipy.*", "statsmodels.*", "pyarrow.*", "httpx"]
|
|
182
183
|
ignore_missing_imports = true
|
|
@@ -29,4 +29,31 @@ is what a changed figure costs. The API may still move underneath it.
|
|
|
29
29
|
#
|
|
30
30
|
# Not one of those is a bug fix, and every one changes what a saved study
|
|
31
31
|
# reproduces. That costs the MINOR position while the leading digit is 0.
|
|
32
|
-
|
|
32
|
+
#
|
|
33
|
+
# 0.5.0 IS THE RULE READ THE OTHER WAY: no computed value moved, and the SURFACE
|
|
34
|
+
# broke. `--sleeve` and `--thesis` are removed along with `Session.sleeves()`,
|
|
35
|
+
# `.theses()` and `.propose_sleeve()`. Their three routes were part of the thesis
|
|
36
|
+
# object model, which came out of the platform with the agent desk it was built
|
|
37
|
+
# around, so the flags could only have failed at the END of a run — after the
|
|
38
|
+
# work, naming a server error rather than a missing feature.
|
|
39
|
+
#
|
|
40
|
+
# A removed flag is a breaking change even though every figure is byte-identical,
|
|
41
|
+
# and while the leading digit is 0 the MINOR position carries that too.
|
|
42
|
+
#
|
|
43
|
+
# Added in the same release, and the reason the removal is not a loss: `gaps` and
|
|
44
|
+
# `tonight`. The record already knew which workflow closes each open gap and
|
|
45
|
+
# nothing traversed those edges; these two read that derivation, so the terminal
|
|
46
|
+
# can now answer "what have I NOT tried" and "what will run while I sleep".
|
|
47
|
+
#
|
|
48
|
+
# 0.6.0 IS THE SAME RULE READ AS A FEATURE RELEASE. No computed value moved.
|
|
49
|
+
# The artefact grew a research OS around the maths that already shipped: any
|
|
50
|
+
# BYOK model (Anthropic, OpenAI-compatible, Gemini, Groq, OpenRouter, Azure,
|
|
51
|
+
# a private gateway), the workflow ops that were library-only (`backtest`,
|
|
52
|
+
# CPCV, factors, pairs, walk-forward, book overlap), allowlisted traces that
|
|
53
|
+
# stay on disk when the portal has no route, a session with history and slash
|
|
54
|
+
# verbs, and a screen that paints the parameter surface it is judging.
|
|
55
|
+
#
|
|
56
|
+
# A new published wheel is a new thing people `pip install`. While the leading
|
|
57
|
+
# digit is 0 the MINOR position carries that, the same way 0.5.0 carried a
|
|
58
|
+
# surface change that left every figure byte-identical.
|
|
59
|
+
__version__ = "0.6.0"
|
|
@@ -55,7 +55,22 @@ __all__ = ["config_path", "load_stored", "save_key", "clear_stored", "apply_stor
|
|
|
55
55
|
#: `QUANTOS_API_KEY` is ours. The provider keys are here because a user who has
|
|
56
56
|
#: said "sign me in" means all of it, and leaving one of the three to be pasted
|
|
57
57
|
#: every session defeats the point.
|
|
58
|
-
MANAGED = (
|
|
58
|
+
MANAGED = (
|
|
59
|
+
"QUANTOS_API_KEY",
|
|
60
|
+
"ANTHROPIC_API_KEY",
|
|
61
|
+
"OPENAI_API_KEY",
|
|
62
|
+
"GEMINI_API_KEY",
|
|
63
|
+
"GOOGLE_API_KEY",
|
|
64
|
+
"GROQ_API_KEY",
|
|
65
|
+
"OPENROUTER_API_KEY",
|
|
66
|
+
"AZURE_OPENAI_API_KEY",
|
|
67
|
+
"AZURE_OPENAI_ENDPOINT",
|
|
68
|
+
"ALPHAENGINE_API_KEY",
|
|
69
|
+
"ALPHAENGINE_BASE_URL",
|
|
70
|
+
"ALPHAENGINE_MODEL",
|
|
71
|
+
"ALPHAENGINE_PROVIDER",
|
|
72
|
+
"OPENAI_BASE_URL",
|
|
73
|
+
)
|
|
59
74
|
|
|
60
75
|
|
|
61
76
|
def config_path() -> Path:
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""A multi-strategy book: named sleeves, figures, never an OMS.
|
|
2
|
+
|
|
3
|
+
WHAT THIS IS
|
|
4
|
+
The overlap/size/monitor questions, asked of a STACK of return series rather
|
|
5
|
+
than one candidate vs one book series. A desk that runs six sleeves needs to
|
|
6
|
+
know how a seventh sits on the whole stack, and whether any sleeve is
|
|
7
|
+
already outside its lines.
|
|
8
|
+
|
|
9
|
+
WHAT THIS IS NOT
|
|
10
|
+
Orders, shares, brokers, or target weights that become fills. `save_signals`
|
|
11
|
+
still files ticker/rank/score/weight. A book here is a research object.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
from .core.risk import compute_var_cvar
|
|
22
|
+
from .core.stress import overlap_stats
|
|
23
|
+
from .core.validation import min_track_record_length
|
|
24
|
+
|
|
25
|
+
__all__ = ["Book", "MAX_SLEEVES"]
|
|
26
|
+
|
|
27
|
+
#: A correlation matrix over 2000 names is a data export. Cap the book so the
|
|
28
|
+
#: figures that leave it stay figures.
|
|
29
|
+
MAX_SLEEVES = 16
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _as_returns(values: Any) -> list[float]:
|
|
33
|
+
if isinstance(values, dict) and "returns" in values:
|
|
34
|
+
values = values["returns"]
|
|
35
|
+
return [float(x) for x in list(values)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Book:
|
|
40
|
+
"""Named sleeves of return series, plus optional current weights."""
|
|
41
|
+
|
|
42
|
+
sleeves: dict[str, list[float]] = field(default_factory=dict)
|
|
43
|
+
weights: dict[str, float] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
def add(self, name: str, returns: Any, *, weight: float | None = None) -> None:
|
|
46
|
+
if len(self.sleeves) >= MAX_SLEEVES and name not in self.sleeves:
|
|
47
|
+
raise ValueError(f"a book holds at most {MAX_SLEEVES} sleeves; remove one first")
|
|
48
|
+
self.sleeves[str(name)] = _as_returns(returns)
|
|
49
|
+
if weight is not None:
|
|
50
|
+
self.weights[str(name)] = float(weight)
|
|
51
|
+
|
|
52
|
+
def drop(self, name: str) -> None:
|
|
53
|
+
self.sleeves.pop(name, None)
|
|
54
|
+
self.weights.pop(name, None)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def names(self) -> list[str]:
|
|
58
|
+
return list(self.sleeves)
|
|
59
|
+
|
|
60
|
+
def combined_returns(self) -> list[float]:
|
|
61
|
+
"""Equal-weight (or stated-weight) stack, aligned from the most recent end."""
|
|
62
|
+
if not self.sleeves:
|
|
63
|
+
return []
|
|
64
|
+
series = {k: np.asarray(v, dtype=float) for k, v in self.sleeves.items()}
|
|
65
|
+
n = min(int(a.size) for a in series.values())
|
|
66
|
+
if n <= 0:
|
|
67
|
+
return []
|
|
68
|
+
stacked = np.column_stack([a[-n:] for a in series.values()])
|
|
69
|
+
w = np.array([self.weights.get(k, 1.0) for k in series], dtype=float)
|
|
70
|
+
if float(w.sum()) == 0:
|
|
71
|
+
w = np.ones_like(w)
|
|
72
|
+
w = w / w.sum()
|
|
73
|
+
return [float(x) for x in (stacked @ w).tolist()]
|
|
74
|
+
|
|
75
|
+
def overlap_matrix(
|
|
76
|
+
self, candidate: Any | None = None, *, candidate_name: str = "candidate"
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
"""Pairwise correlation/beta, bounded. Candidate optional."""
|
|
79
|
+
names = list(self.sleeves)
|
|
80
|
+
series = dict(self.sleeves)
|
|
81
|
+
if candidate is not None:
|
|
82
|
+
series[candidate_name] = _as_returns(candidate)
|
|
83
|
+
names = [candidate_name, *names]
|
|
84
|
+
names = names[:MAX_SLEEVES]
|
|
85
|
+
pairs: list[dict[str, Any]] = []
|
|
86
|
+
for i, a in enumerate(names):
|
|
87
|
+
for b in names[i + 1 :]:
|
|
88
|
+
stats = overlap_stats(series[a], series[b])
|
|
89
|
+
pairs.append(
|
|
90
|
+
{
|
|
91
|
+
"a": a,
|
|
92
|
+
"b": b,
|
|
93
|
+
"correlation": stats.get("correlation"),
|
|
94
|
+
"beta_to_book": stats.get("beta_to_book"),
|
|
95
|
+
"n_obs": stats.get("n_obs"),
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
return {
|
|
99
|
+
"n_sleeves": len(names),
|
|
100
|
+
"names": names,
|
|
101
|
+
"pairs": pairs,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
def residual_weight(self, name: str, *, target: float = 1.0) -> dict[str, Any]:
|
|
105
|
+
"""How much of `target` is left after the sleeves already held.
|
|
106
|
+
|
|
107
|
+
Refuses rather than sizing small when the candidate record is shorter
|
|
108
|
+
than MinTRL — a small weight is still a claim.
|
|
109
|
+
"""
|
|
110
|
+
if name not in self.sleeves:
|
|
111
|
+
raise KeyError(name)
|
|
112
|
+
returns = self.sleeves[name]
|
|
113
|
+
mintrl = min_track_record_length(returns)
|
|
114
|
+
if mintrl.get("error") or not mintrl.get("sufficient"):
|
|
115
|
+
return {
|
|
116
|
+
"name": name,
|
|
117
|
+
"refused": True,
|
|
118
|
+
"reason": "record_too_short",
|
|
119
|
+
"min_track_record_length": mintrl,
|
|
120
|
+
}
|
|
121
|
+
held = sum(self.weights.get(k, 0.0) for k in self.sleeves if k != name)
|
|
122
|
+
residual = max(0.0, float(target) - held)
|
|
123
|
+
risk = compute_var_cvar(returns)
|
|
124
|
+
return {
|
|
125
|
+
"name": name,
|
|
126
|
+
"refused": False,
|
|
127
|
+
"held_weight": round(held, 6),
|
|
128
|
+
"residual_weight": round(residual, 6),
|
|
129
|
+
"cvar": risk.get("cvar"),
|
|
130
|
+
"min_track_record_length": mintrl,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def monitor(self) -> dict[str, Any]:
|
|
134
|
+
"""Per-sleeve status. Nothing checked never reads as all-clear."""
|
|
135
|
+
rows: list[dict[str, Any]] = []
|
|
136
|
+
for name, returns in self.sleeves.items():
|
|
137
|
+
if len(returns) < 2:
|
|
138
|
+
rows.append({"name": name, "status": "unchecked", "n_obs": len(returns)})
|
|
139
|
+
continue
|
|
140
|
+
risk = compute_var_cvar(returns)
|
|
141
|
+
mintrl = min_track_record_length(returns)
|
|
142
|
+
status = "ok" if mintrl.get("sufficient") else "undetermined"
|
|
143
|
+
rows.append(
|
|
144
|
+
{
|
|
145
|
+
"name": name,
|
|
146
|
+
"status": status,
|
|
147
|
+
"n_obs": len(returns),
|
|
148
|
+
"cvar": risk.get("cvar"),
|
|
149
|
+
"sufficient": mintrl.get("sufficient"),
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
if not rows or any(r["status"] == "unchecked" for r in rows):
|
|
153
|
+
overall = "unchecked"
|
|
154
|
+
elif any(r["status"] != "ok" for r in rows):
|
|
155
|
+
overall = "undetermined"
|
|
156
|
+
else:
|
|
157
|
+
overall = "ok"
|
|
158
|
+
return {"overall": overall, "sleeves": rows, "n_sleeves": len(rows)}
|