nexustrade 0.1.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.
- nexustrade-0.1.0/.gitignore +7 -0
- nexustrade-0.1.0/AGENTS.md +232 -0
- nexustrade-0.1.0/CLAUDE.md +7 -0
- nexustrade-0.1.0/LICENSE +21 -0
- nexustrade-0.1.0/PKG-INFO +312 -0
- nexustrade-0.1.0/README.md +261 -0
- nexustrade-0.1.0/nexustrade/__init__.py +172 -0
- nexustrade-0.1.0/nexustrade/client.py +754 -0
- nexustrade-0.1.0/nexustrade/env.py +147 -0
- nexustrade-0.1.0/nexustrade/lake.py +801 -0
- nexustrade-0.1.0/nexustrade/portfolio.py +2801 -0
- nexustrade-0.1.0/nexustrade/py.typed +1 -0
- nexustrade-0.1.0/nexustrade/stats.py +465 -0
- nexustrade-0.1.0/pyproject.toml +83 -0
- nexustrade-0.1.0/requirements-lake.lock +355 -0
- nexustrade-0.1.0/requirements-stats.lock +246 -0
- nexustrade-0.1.0/tests/__init__.py +0 -0
- nexustrade-0.1.0/tests/conformance/client-cases.json +720 -0
- nexustrade-0.1.0/tests/test_client.py +545 -0
- nexustrade-0.1.0/tests/test_conformance.py +97 -0
- nexustrade-0.1.0/tests/test_env.py +141 -0
- nexustrade-0.1.0/tests/test_lake_compat.py +275 -0
- nexustrade-0.1.0/tests/test_lake_module.py +214 -0
- nexustrade-0.1.0/tests/test_package_exports.py +165 -0
- nexustrade-0.1.0/tests/test_stats.py +201 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# AGENTS.md — NexusTrade Python SDK
|
|
2
|
+
|
|
3
|
+
Instructions for coding agents (Claude Code, Cursor, Codex, and friends) writing
|
|
4
|
+
NexusTrade strategies with this package. Humans: [README.md](README.md) is the
|
|
5
|
+
friendlier read.
|
|
6
|
+
|
|
7
|
+
## What this package is
|
|
8
|
+
|
|
9
|
+
A typed client plus ~170 **generated** builders for authoring trading
|
|
10
|
+
strategies. The builders are generated from the same indicator specification the
|
|
11
|
+
NexusTrade engine executes, so a book assembled from them is structurally valid
|
|
12
|
+
before it ever leaves the process.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
author a portfolio → submit a job → poll until terminal → read result
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install nexustrade
|
|
22
|
+
export NEXUSTRADE_API_KEY=sk-...
|
|
23
|
+
export NEXUSTRADE_API_BASE_URL=https://nexustrade.io/api/v1
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import nexustrade as nt
|
|
28
|
+
client = nt.NexusTradeClient.from_environment()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Keys come from https://nexustrade.io/developers. Never hardcode one into a file
|
|
32
|
+
you write; read it from the environment.
|
|
33
|
+
|
|
34
|
+
## Rules that matter
|
|
35
|
+
|
|
36
|
+
**1. Use the builders. Never hand-write the JSON.**
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# Right — validated shape, correct wire names
|
|
40
|
+
nt.buy(nt.stock_asset("SPY"), 100)
|
|
41
|
+
|
|
42
|
+
# Wrong — silently diverges from the engine's schema
|
|
43
|
+
{"type": "Buy", "targetAsset": {"symbol": "SPY"}, "amount": 100}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If you cannot find a builder, list them (`nt.__all__`) rather than inventing a
|
|
47
|
+
dict. A hand-written payload that the API accepts can still mean something
|
|
48
|
+
different from what you intended.
|
|
49
|
+
|
|
50
|
+
**2. Every mutation needs an idempotency key, and it must be deterministic.**
|
|
51
|
+
|
|
52
|
+
Jobs cost money. A retry with the *same* key returns the original operation; a
|
|
53
|
+
retry with a new key launches a second paid job.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
# Right — same logical run reuses the key across retries
|
|
57
|
+
nt.create_backtest(handle, idempotency_key="momentum-2024-v1")
|
|
58
|
+
|
|
59
|
+
# Wrong — every retry is a new billable job
|
|
60
|
+
nt.create_backtest(handle, idempotency_key=f"run-{time.time()}")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Reusing a key with a *different* payload is a `409 idempotency_conflict`. Version
|
|
64
|
+
the key when the request changes: `momentum-2024-v2`.
|
|
65
|
+
|
|
66
|
+
**3. `create_*` does not wait. Poll.**
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
operation = nt.create_backtest(handle, idempotency_key="k")
|
|
70
|
+
# operation["result"] is ABSENT here — the job has not run yet.
|
|
71
|
+
finished = nt.wait_for_backtest(operation["id"])
|
|
72
|
+
print(finished["result"])
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A timeout does not cancel the job. Call the waiter again with the same id;
|
|
76
|
+
do not resubmit.
|
|
77
|
+
|
|
78
|
+
**4. Batch when you have several.**
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
operations = nt.create_backtests([h1, h2, h3], idempotency_key="sweep-v1")
|
|
82
|
+
results = nt.wait_for_backtests(operations)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
One request, one key, one rate-limit slot — instead of three of each.
|
|
86
|
+
|
|
87
|
+
**5. Percent semantics.** `buy(asset, 100)` is **100% of portfolio**, not 100
|
|
88
|
+
shares. Deployment and allocation parameters are percentages unless a builder
|
|
89
|
+
says otherwise.
|
|
90
|
+
|
|
91
|
+
**6. Credentials come from the environment, or a `.env` file.** Both
|
|
92
|
+
`NEXUSTRADE_API_KEY` and `NEXUSTRADE_API_BASE_URL` are read from the process
|
|
93
|
+
environment first, then from a `.env` at or above the working directory. Never
|
|
94
|
+
hardcode a key into a file you write, and never print one. Exported values win
|
|
95
|
+
over the file, so a `.env` cannot silently override a deployment's real config.
|
|
96
|
+
|
|
97
|
+
**7. The base install is stdlib-only.** `nt.lake.*` needs `pip install
|
|
98
|
+
'nexustrade[lake]'`; `nt.spec_curve` and friends need `[stats]`. Missing extras
|
|
99
|
+
raise an `AttributeError` that names the extra to install — read it rather than
|
|
100
|
+
guessing.
|
|
101
|
+
|
|
102
|
+
## Recipes
|
|
103
|
+
|
|
104
|
+
<details open>
|
|
105
|
+
<summary><b>Buy and hold</b></summary>
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import nexustrade as nt
|
|
109
|
+
|
|
110
|
+
book = nt.portfolio("Buy and hold SPY", [
|
|
111
|
+
nt.strategy("Buy", nt.always(), nt.buy(nt.stock_asset("SPY"), 100)),
|
|
112
|
+
])
|
|
113
|
+
```
|
|
114
|
+
</details>
|
|
115
|
+
|
|
116
|
+
<details>
|
|
117
|
+
<summary><b>Condition on an indicator</b></summary>
|
|
118
|
+
|
|
119
|
+
Indicators support Python comparison operators; the result is a `Condition`.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
oversold = nt.RSI(nt.stock_asset("AAPL"), 14) < 30
|
|
123
|
+
|
|
124
|
+
book = nt.portfolio("Dip buyer", [
|
|
125
|
+
nt.strategy("Buy the dip", oversold, nt.buy(nt.stock_asset("AAPL"), 25)),
|
|
126
|
+
nt.strategy(
|
|
127
|
+
"Take profit",
|
|
128
|
+
nt.PositionPercentChange(nt.stock_asset("AAPL")) > 10,
|
|
129
|
+
nt.sell(nt.stock_asset("AAPL"), 100),
|
|
130
|
+
),
|
|
131
|
+
])
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Combine with `nt.multi`, `nt.at_least`, `nt.at_most`, `nt.exactly`.
|
|
135
|
+
</details>
|
|
136
|
+
|
|
137
|
+
<details>
|
|
138
|
+
<summary><b>Rank and rotate a universe</b></summary>
|
|
139
|
+
|
|
140
|
+
`CANDIDATE` is the placeholder for "each name being evaluated". Use it inside a
|
|
141
|
+
pipeline; use a concrete asset outside one.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
book = nt.portfolio("Momentum", [
|
|
145
|
+
nt.strategy("Rotate", nt.always(), nt.dynamic_rebalance(
|
|
146
|
+
universe_config=nt.universe("SP500"),
|
|
147
|
+
pipeline=[
|
|
148
|
+
nt.filter(nt.Price(nt.CANDIDATE) > nt.SMA(nt.CANDIDATE, 200)),
|
|
149
|
+
nt.select_top(nt.RSI(nt.CANDIDATE, 14), 10),
|
|
150
|
+
],
|
|
151
|
+
weight_indicator=nt.RSI(nt.CANDIDATE, 14),
|
|
152
|
+
limit=10,
|
|
153
|
+
deployment_percent=80,
|
|
154
|
+
)),
|
|
155
|
+
], initial_value=100_000)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`deployment_percent=80` invests 80% of the portfolio across the selection and
|
|
159
|
+
leaves the rest in cash. It is a **total** cap, not a per-name one.
|
|
160
|
+
</details>
|
|
161
|
+
|
|
162
|
+
<details>
|
|
163
|
+
<summary><b>Backtest, optimize, walk forward</b></summary>
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
bt = nt.backtest(book, start_date="2024-01-01", end_date="2024-12-31")
|
|
167
|
+
|
|
168
|
+
opt = nt.optimization(book, start_date="2022-01-01", end_date="2024-12-31")
|
|
169
|
+
|
|
170
|
+
wf = nt.walk_forward(book, global_start_date="2022-01-01",
|
|
171
|
+
global_end_date="2024-12-31", fold_count=4)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Note walk-forward uses `global_start_date` / `global_end_date` / `fold_count`,
|
|
175
|
+
not `start_date` / `end_date`. Each handle goes to its matching
|
|
176
|
+
`create_*` + `wait_for_*` pair.
|
|
177
|
+
</details>
|
|
178
|
+
|
|
179
|
+
<details>
|
|
180
|
+
<summary><b>Query the data lake</b></summary>
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
result = nt.lake.sql(
|
|
184
|
+
"SELECT ticker, date, closingPrice FROM lake.daily_ohlc WHERE ticker = ?",
|
|
185
|
+
["AAPL"],
|
|
186
|
+
max_rows=10_000,
|
|
187
|
+
)
|
|
188
|
+
frame = result.to_pandas()
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Always parameterize with `?` rather than interpolating into the SQL string.
|
|
192
|
+
Requires the `[lake]` extra.
|
|
193
|
+
</details>
|
|
194
|
+
|
|
195
|
+
## Errors
|
|
196
|
+
|
|
197
|
+
All failures raise `NexusTradeApiError` with a stable `.status`, `.code`, and
|
|
198
|
+
`.message`. Branch on `.code`, never on message text.
|
|
199
|
+
|
|
200
|
+
| Code | What to do |
|
|
201
|
+
| --- | --- |
|
|
202
|
+
| `invalid_token` | Key missing/expired, or an OAuth JWT was used. Only `sk-` keys work here. |
|
|
203
|
+
| `insufficient_scope` | The key lacks `read` / `write` / `lake`. Do not retry. |
|
|
204
|
+
| `invalid_portfolio` | The book is malformed — fix the builders, do not retry as-is. |
|
|
205
|
+
| `idempotency_conflict` | Same key, different payload. Version the key. |
|
|
206
|
+
| `rate_limit_exceeded` | Back off and retry. |
|
|
207
|
+
| `operation_timeout` | Job still running. Re-poll the same id; never resubmit. |
|
|
208
|
+
|
|
209
|
+
`status == 0` means no HTTP status applies: the request never reached the API
|
|
210
|
+
(`transport_error`), or the reply failed an envelope check.
|
|
211
|
+
|
|
212
|
+
## Out of scope
|
|
213
|
+
|
|
214
|
+
Not in this SDK. Do not attempt to reach them through it:
|
|
215
|
+
|
|
216
|
+
- **Screener** — MCP only.
|
|
217
|
+
- **Live trading and order placement** — deliberately excluded.
|
|
218
|
+
- **Agent runs** — the NexusTrade agent API is not exposed here yet.
|
|
219
|
+
|
|
220
|
+
## Verifying your work
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
# Assemble the book and inspect the JSON before spending money on a backtest.
|
|
224
|
+
import json
|
|
225
|
+
print(json.dumps(book, indent=2))
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
If you are editing this repository rather than consuming it:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
PYTHONPATH=. python3 -m unittest discover -s tests -t .
|
|
232
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Claude Code reads this file automatically. The guidance for working in this
|
|
4
|
+
repository lives in **[AGENTS.md](AGENTS.md)** — a single source shared by every
|
|
5
|
+
agent tool, so the two can never drift.
|
|
6
|
+
|
|
7
|
+
Read AGENTS.md before writing NexusTrade strategy code.
|
nexustrade-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Starks Technology, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nexustrade
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed Python SDK for NexusTrade portfolio authoring, backtesting, and optimization
|
|
5
|
+
Project-URL: Homepage, https://nexustrade.io
|
|
6
|
+
Project-URL: Repository, https://github.com/austin-starks/nexustrade-py
|
|
7
|
+
Author: Starks Technology
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Starks Technology, LLC
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Keywords: algorithmic-trading,backtesting,nexustrade,portfolio,trading
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
39
|
+
Classifier: Typing :: Typed
|
|
40
|
+
Requires-Python: >=3.10
|
|
41
|
+
Provides-Extra: lake
|
|
42
|
+
Requires-Dist: duckdb==1.3.2; extra == 'lake'
|
|
43
|
+
Requires-Dist: pandas==2.3.3; extra == 'lake'
|
|
44
|
+
Requires-Dist: pyarrow==20.0.0; extra == 'lake'
|
|
45
|
+
Provides-Extra: stats
|
|
46
|
+
Requires-Dist: numpy==2.2.6; extra == 'stats'
|
|
47
|
+
Requires-Dist: pandas==2.3.3; extra == 'stats'
|
|
48
|
+
Requires-Dist: scipy==1.15.3; extra == 'stats'
|
|
49
|
+
Requires-Dist: statsmodels==0.14.6; extra == 'stats'
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
<div align="center">
|
|
53
|
+
|
|
54
|
+
<img src="https://nexustrade.io/logo192.jpeg" alt="NexusTrade" width="88" height="88">
|
|
55
|
+
|
|
56
|
+
# NexusTrade Python SDK
|
|
57
|
+
|
|
58
|
+
**Author trading strategies in typed Python. Backtest them on the engine that runs them live.**
|
|
59
|
+
|
|
60
|
+
[](https://pypi.org/project/nexustrade/)
|
|
61
|
+
[](https://pypi.org/project/nexustrade/)
|
|
62
|
+
[](LICENSE)
|
|
63
|
+
[](https://peps.python.org/pep-0561/)
|
|
64
|
+
|
|
65
|
+
[Quickstart](#quickstart) · [Authoring](#authoring-strategies) · [Polling](#jobs-run-on-the-engine--you-poll) · [Lake SQL](#lake-sql) · [Auth](#authentication) · [Errors](#errors)
|
|
66
|
+
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install nexustrade
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The base install is **stdlib-only** — no third-party dependencies, importable anywhere.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install 'nexustrade[lake]' # DuckDB/pandas analysis of lake results
|
|
79
|
+
pip install 'nexustrade[stats]' # spec curves, Newey-West, bootstrap
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Quickstart
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from nexustrade import NexusTradeClient, always, backtest, buy, portfolio, stock_asset, strategy
|
|
86
|
+
|
|
87
|
+
nt = NexusTradeClient(api_key="sk-...", base_url="https://nexustrade.io/api/v1")
|
|
88
|
+
|
|
89
|
+
book = portfolio("Example", [
|
|
90
|
+
strategy("Buy SPY", always(), buy(stock_asset("SPY"), 100)),
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
operation = nt.create_backtest(
|
|
94
|
+
backtest(book, start_date="2024-01-01", end_date="2024-12-31"),
|
|
95
|
+
idempotency_key="example-v1",
|
|
96
|
+
)
|
|
97
|
+
result = nt.wait_for_backtest(operation["id"])
|
|
98
|
+
print(result["result"])
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Authoring strategies
|
|
102
|
+
|
|
103
|
+
Every builder is generated from the same indicator specification the NexusTrade
|
|
104
|
+
engine runs, so a book is **valid by construction** rather than by convention.
|
|
105
|
+
Indicators compose with ordinary Python operators.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import nexustrade as nt
|
|
109
|
+
|
|
110
|
+
book = nt.portfolio("Momentum", [
|
|
111
|
+
nt.strategy(
|
|
112
|
+
"Rotate into strength",
|
|
113
|
+
nt.always(),
|
|
114
|
+
nt.dynamic_rebalance(
|
|
115
|
+
universe_config=nt.universe("SP500"),
|
|
116
|
+
pipeline=[
|
|
117
|
+
nt.filter(nt.Price(nt.CANDIDATE) > nt.SMA(nt.CANDIDATE, 200)),
|
|
118
|
+
nt.select_top(nt.RSI(nt.CANDIDATE, 14), 10),
|
|
119
|
+
],
|
|
120
|
+
weight_indicator=nt.RSI(nt.CANDIDATE, 14),
|
|
121
|
+
limit=10,
|
|
122
|
+
deployment_percent=80,
|
|
123
|
+
),
|
|
124
|
+
),
|
|
125
|
+
], initial_value=100_000)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
<details>
|
|
129
|
+
<summary><b>What you can build</b> — 170+ generated builders</summary>
|
|
130
|
+
|
|
131
|
+
| Group | Examples |
|
|
132
|
+
| --- | --- |
|
|
133
|
+
| **Price & volume** | `Price` `OpeningPrice` `HighOfDay` `VWAP` `Volume` `GapPercentage` |
|
|
134
|
+
| **Technicals** | `SMA` `EMA` `RSI` `BollingerBand` `AverageTrueRange` `CrossAbove` |
|
|
135
|
+
| **Position state** | `PositionValue` `PositionPercentChange` `PositionMaxDrawdown` |
|
|
136
|
+
| **Portfolio state** | `PortfolioValue` `BuyingPower` `MaxDrawdown` `InitialValue` |
|
|
137
|
+
| **Fundamentals** | `Fundamental` `Economic` `DaysUntilEarnings` `IsIndexMember` `IsIndustry` |
|
|
138
|
+
| **Options** | `OptionDaysToExpiration` `OptionCollateral` `OptionUnrealizedPnL` `open_option` `close_option` |
|
|
139
|
+
| **Actions** | `buy` `sell` `deposit` `withdraw` `alert` `dynamic_rebalance` `rebalance_option` |
|
|
140
|
+
| **Selection** | `filter` `select_top` `select_percentile` `universe` |
|
|
141
|
+
| **Logic** | `always` `at_least` `at_most` `exactly` `fewer_than` `multi` |
|
|
142
|
+
|
|
143
|
+
Full list: `python -c "import nexustrade; print(nexustrade.__all__)"`
|
|
144
|
+
|
|
145
|
+
</details>
|
|
146
|
+
|
|
147
|
+
## Jobs run on the engine — you poll
|
|
148
|
+
|
|
149
|
+
`create_*` enqueues work and returns immediately. It does **not** block until
|
|
150
|
+
results exist. There are no webhooks today.
|
|
151
|
+
|
|
152
|
+
Every job kind reports the same envelope, so one poller serves all of them:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
{
|
|
156
|
+
"id": "op_...",
|
|
157
|
+
"kind": "backtest", # backtest | optimization | walk_forward
|
|
158
|
+
"status": "queued", # queued | running | completed | failed | cancelled
|
|
159
|
+
"result": {...}, # present only once terminal
|
|
160
|
+
"error": {"code": ..., "message": ..., "retryable": ...},
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
finished = nt.wait_for_backtest(operation["id"]) # blocks on deterministic backoff
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| Option | Default | Meaning |
|
|
169
|
+
| --- | --- | --- |
|
|
170
|
+
| `timeout_seconds` | `900` | Give up waiting (the job keeps running) |
|
|
171
|
+
| `poll_interval_seconds` | `2` | First interval; backs off 1.5× |
|
|
172
|
+
| `max_poll_interval_seconds` | `15` | Interval ceiling |
|
|
173
|
+
| `raise_on_failure` | `True` | Raise on `failed`/`cancelled` instead of returning |
|
|
174
|
+
|
|
175
|
+
A timeout raises `operation_timeout` and does **not** cancel the job — call the
|
|
176
|
+
waiter again with the same id rather than resubmitting.
|
|
177
|
+
|
|
178
|
+
**Batches.** `create_backtests` submits many in one request and returns one
|
|
179
|
+
operation each; `wait_for_backtests(operations)` waits on all of them. Prefer it
|
|
180
|
+
over a loop: one request, one idempotency key, one rate-limit slot.
|
|
181
|
+
|
|
182
|
+
**Optimization and walk-forward** follow the identical shape:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
study = nt.create_walk_forward(
|
|
186
|
+
nt.walk_forward(book, global_start_date="2022-01-01",
|
|
187
|
+
global_end_date="2024-12-31", fold_count=4),
|
|
188
|
+
idempotency_key="wf-v1",
|
|
189
|
+
)
|
|
190
|
+
nt.wait_for_walk_forward(study["id"])
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Lake SQL
|
|
194
|
+
|
|
195
|
+
Read-only SQL over the NexusTrade market-data lake. Results are durable Parquet
|
|
196
|
+
parts rather than an implicitly materialized array, so a large result is
|
|
197
|
+
explicit rather than an out-of-memory surprise.
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
import nexustrade as nt
|
|
201
|
+
|
|
202
|
+
result = nt.lake.sql(
|
|
203
|
+
"SELECT ticker, date, closingPrice FROM lake.daily_ohlc WHERE ticker = ?",
|
|
204
|
+
["AAPL"],
|
|
205
|
+
max_rows=10_000,
|
|
206
|
+
)
|
|
207
|
+
frame = result.to_pandas() # memory-bounded
|
|
208
|
+
for batch in result.iter_batches(): # or stream within your own budget
|
|
209
|
+
...
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Requires the `[lake]` extra. NexusTrade resolves `lake.*` server-side and picks a
|
|
213
|
+
compatible backing engine; your SQL does not change when it does.
|
|
214
|
+
|
|
215
|
+
## Authentication
|
|
216
|
+
|
|
217
|
+
Create a key at **[nexustrade.io/developers](https://nexustrade.io/developers)**
|
|
218
|
+
(Profile → API Keys). Keys start with `sk-` and are shown once.
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
nt = NexusTradeClient(api_key="sk-...", base_url="https://nexustrade.io/api/v1")
|
|
222
|
+
# or set NEXUSTRADE_API_KEY / NEXUSTRADE_API_BASE_URL and:
|
|
223
|
+
nt = NexusTradeClient.from_environment()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Both variables are also read from a **`.env` file** at or above the current
|
|
227
|
+
directory, so a local project works with no exports and no `python-dotenv`:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
# .env
|
|
231
|
+
NEXUSTRADE_API_KEY=sk-...
|
|
232
|
+
NEXUSTRADE_API_BASE_URL=https://nexustrade.io/api/v1
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
The real environment always wins — a `.env` value is used only when the variable
|
|
236
|
+
is absent, so a stale file can never override what you exported. Nothing is
|
|
237
|
+
written back to `os.environ`. Opt out with `NEXUSTRADE_DISABLE_DOTENV=1`.
|
|
238
|
+
|
|
239
|
+
| Scope | Grants |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| `read` | `get_backtest`, `get_optimization`, `get_walk_forward` |
|
|
242
|
+
| `write` | `create_portfolio`, `create_backtest(s)`, `create_optimization`, `create_walk_forward` |
|
|
243
|
+
| `lake` | Lake catalog, query lifecycle, manifests, result parts |
|
|
244
|
+
|
|
245
|
+
A key missing the scope gets `403 insufficient_scope`.
|
|
246
|
+
|
|
247
|
+
> **OAuth is not accepted here.** NexusTrade's OAuth flow serves the MCP server.
|
|
248
|
+
> These endpoints take `sk-` API keys only; a bearer JWT is rejected with
|
|
249
|
+
> `401 invalid_token`.
|
|
250
|
+
|
|
251
|
+
**Transport hardening.** HTTPS is required (except loopback). The client refuses
|
|
252
|
+
cross-origin redirects, so the credential cannot be replayed to another host, and
|
|
253
|
+
refuses to follow a redirect on any non-GET request, so a redirect can never
|
|
254
|
+
re-submit a paid job.
|
|
255
|
+
|
|
256
|
+
## Idempotency
|
|
257
|
+
|
|
258
|
+
Every mutation takes a key. Reusing the same key with the same request returns
|
|
259
|
+
the original resource instead of launching a second paid job — so a retry after
|
|
260
|
+
a network failure is free.
|
|
261
|
+
|
|
262
|
+
```python
|
|
263
|
+
nt.create_backtest(handle, idempotency_key="momentum-2024-v1")
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Errors
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from nexustrade import NexusTradeApiError
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
nt.create_backtest(handle, idempotency_key="run-1")
|
|
273
|
+
except NexusTradeApiError as error:
|
|
274
|
+
if error.code == "rate_limit_exceeded":
|
|
275
|
+
...
|
|
276
|
+
raise
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
| Status | Code | Meaning |
|
|
280
|
+
| --- | --- | --- |
|
|
281
|
+
| 401 | `invalid_token` | Missing, malformed, or expired key (or an OAuth JWT) |
|
|
282
|
+
| 403 | `insufficient_scope` | Key lacks `read`, `write`, or `lake` |
|
|
283
|
+
| 400 | `invalid_request`, `invalid_portfolio` | Malformed input |
|
|
284
|
+
| 400 | `invalid_idempotency_key` | Must match `[A-Za-z0-9._:-]{1,160}` |
|
|
285
|
+
| 409 | `idempotency_conflict` | Key reused with a different payload |
|
|
286
|
+
| 404 | `not_found`, `operation_not_found` | Unknown or not yours |
|
|
287
|
+
| 429 | `rate_limit_exceeded` | Back off and retry |
|
|
288
|
+
|
|
289
|
+
`status` is `0` when no HTTP status describes the failure: `transport_error`
|
|
290
|
+
(never reached the API), `unsafe_redirect`, or an `invalid_response` envelope
|
|
291
|
+
check on an otherwise-successful reply.
|
|
292
|
+
|
|
293
|
+
## Timeouts
|
|
294
|
+
|
|
295
|
+
`HttpTransport(timeout_seconds=...)` (default 30) is urllib's per-socket-operation
|
|
296
|
+
timeout, so a slow-but-progressing response is not cut off mid-stream. Neither it
|
|
297
|
+
nor the poll timeout bounds how long a *job* takes.
|
|
298
|
+
|
|
299
|
+
## Scope
|
|
300
|
+
|
|
301
|
+
Portfolio drafting, backtesting, optimization, walk-forward studies, and
|
|
302
|
+
read-only SQL over the market-data lake, versioned under `/api/v1/nexustrade`.
|
|
303
|
+
The screener and live trading remain outside this surface.
|
|
304
|
+
|
|
305
|
+
## Using this SDK with a coding agent
|
|
306
|
+
|
|
307
|
+
See **[AGENTS.md](AGENTS.md)** — the conventions, invariants, and recipes an
|
|
308
|
+
agent needs to write correct NexusTrade strategies on the first pass.
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT
|