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.
@@ -0,0 +1,7 @@
1
+ typescript/dist/
2
+ typescript/node_modules/
3
+ python/dist/
4
+ python/build/
5
+ python/*.egg-info/
6
+ .venv-stats/
7
+ **/__pycache__/
@@ -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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/nexustrade.svg)](https://pypi.org/project/nexustrade/)
61
+ [![Python](https://img.shields.io/pypi/pyversions/nexustrade.svg)](https://pypi.org/project/nexustrade/)
62
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
63
+ [![Types](https://img.shields.io/badge/types-py.typed-informational.svg)](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