NexusTrade Python SDK
Author trading strategies in typed Python. Backtest them on the engine that runs them live.
Quickstart · Authoring · Polling · Agents · Lake SQL · Auth · Errors
pip install nexustrade
The base install is stdlib-only — no third-party dependencies, importable anywhere.
pip install 'nexustrade[lake]' # DuckDB/pandas analysis of lake results
pip install 'nexustrade[stats]' # spec curves, Newey-West, bootstrap
Quickstart
from nexustrade import NexusTradeClient, always, backtest, buy, portfolio, stock_asset, strategy
client = NexusTradeClient(api_key="sk-...", base_url="https://nexustrade.io/api/v1")
book = portfolio("Example", [
strategy("Buy SPY", always(), buy(stock_asset("SPY"), 100)),
])
operation = client.create_backtest(
backtest(book, start_date="2024-01-01", end_date="2024-12-31"),
idempotency_key="example-v1",
)
result = client.wait_for_backtest(operation["id"])
print(result["result"])
Authoring strategies
Every builder is generated from the same indicator specification the NexusTrade engine runs, so a book is valid by construction rather than by convention. Indicators compose with ordinary Python operators.
import nexustrade as nt
book = nt.portfolio("Momentum", [
nt.strategy(
"Rotate into strength",
nt.always(),
nt.dynamic_rebalance(
universe_config=nt.universe("SP500"),
pipeline=[
nt.filter(nt.Price(nt.CANDIDATE) > nt.SMA(nt.CANDIDATE, 200)),
nt.select_top(nt.RSI(nt.CANDIDATE, 14), 10),
],
weight_indicator=nt.RSI(nt.CANDIDATE, 14),
limit=10,
deployment_percent=80,
),
),
], initial_value=100_000)
What you can build — 170+ generated builders
| Group | Examples |
|---|---|
| Price & volume | Price OpeningPrice HighOfDay VWAP Volume GapPercentage |
| Technicals | SMA EMA RSI BollingerBand AverageTrueRange CrossAbove |
| Position state | PositionValue PositionPercentChange PositionMaxDrawdown |
| Portfolio state | PortfolioValue BuyingPower MaxDrawdown InitialValue |
| Fundamentals | Fundamental Economic DaysUntilEarnings IsIndexMember IsIndustry |
| Options | OptionDaysToExpiration OptionCollateral OptionUnrealizedPnL open_option close_option |
| Actions | buy sell alert dynamic_rebalance rebalance_option |
| Selection | filter select_top select_percentile universe |
| Logic | always at_least at_most exactly fewer_than multi |
Full list: python -c "import nexustrade; print(nexustrade.__all__)"
Jobs run on the engine — you poll
create_* enqueues work and returns immediately. It does not block until
results exist. There are no webhooks today.
sequenceDiagram
participant You
participant SDK
participant Engine
You->>SDK: create_backtest(book)
SDK->>Engine: POST (enqueue)
Engine-->>SDK: id, status=queued
SDK-->>You: operation (returns immediately)
loop wait_for_backtest — backoff 2s→15s
SDK->>Engine: GET /operations/{id}
Engine-->>SDK: status update
end
SDK-->>You: result (when completed)
Note over You,Engine: Poll timeout raises operation_timeout.<br/>The job keeps running — call wait again with the same id.
Every job kind reports the same envelope, so one poller serves all of them:
{
"id": "op_...",
"kind": "backtest", # backtest | optimization | walk_forward
"status": "queued", # queued | running | completed | failed | cancelled
"result": {...}, # present only once terminal
"error": {"code": ..., "message": ..., "retryable": ...},
}
finished = client.wait_for_backtest(operation["id"]) # blocks on deterministic backoff
| Option | Default | Meaning |
|---|---|---|
timeout_seconds |
900 |
Give up waiting (the job keeps running) |
poll_interval_seconds |
2 |
First interval; backs off 1.5× |
max_poll_interval_seconds |
15 |
Interval ceiling |
raise_on_failure |
True |
Raise on failed/cancelled instead of returning |
A timeout raises operation_timeout and does not cancel the job — call the
waiter again with the same id rather than resubmitting.
Batches. create_backtests submits many in one request and returns one
operation each; wait_for_backtests(operations) waits on all of them. Prefer it
over a loop: one request, one idempotency key, one rate-limit slot.
Optimization and walk-forward follow the identical shape:
study = client.create_walk_forward(
nt.walk_forward(book, global_start_date="2022-01-01",
global_end_date="2024-12-31", fold_count=4),
idempotency_key="wf-v1",
)
client.wait_for_walk_forward(study["id"])
Your own data
A custom data source is a time series you own — sentiment counts, a proprietary
factor, anything the platform does not already carry. Create one, then reference
it from a strategy with CustomIndicator.
series = client.create_custom_indicator(
{
"name": "WSB NVDA Mentions",
"scope": "asset",
"description": "Daily r/wallstreetbets mentions",
"points": [
{"timestamp": "2024-04-01", "value": 152, "ticker": "NVDA"},
{"timestamp": "2024-04-02", "value": 90, "ticker": "NVDA"},
],
},
idempotency_key="wsb-mentions-v1",
)
busy = CustomIndicator(stock_asset("NVDA"), series["customIndicatorId"]) > 100
book = portfolio("Attention", [
strategy("Buy the buzz", busy, buy(stock_asset("NVDA"), 25)),
])
scope is "global" (one series) or "asset" (one series per ticker, so every
point needs a ticker). It cannot be changed after creation.
Size is not a constraint. points is unlimited. A batch that fits the
request goes with it; a larger one is uploaded to storage and validated before
the call returns. Either way the returned indicator reflects what actually
landed, and an upload that fails validation raises rather than reporting
success.
Growing a series. Append to the same id every run:
client.append_custom_indicator_points(
series["customIndicatorId"],
[{"timestamp": "2024-04-03", "value": 118, "ticker": "NVDA"}],
idempotency_key="wsb-mentions-2024-04-03",
)
Creating a fresh series per run splits the history into fragments no strategy can read. Re-sending an identical batch is safe — the duplicate is not written twice.
| Call | Purpose |
|---|---|
create_custom_indicator(spec, idempotency_key=...) |
Create, optionally seeded |
append_custom_indicator_points(id, points, idempotency_key=...) |
Add points |
list_custom_indicators() / get_custom_indicator(id) |
Discover ids and coverage |
Points accept timestamp, value, ticker, asset_type, and available_at
— snake_case or camelCase, with date/datetime objects allowed. Set
available_at when a value became knowable later than it is dated: an earnings
figure stamped to quarter-end but published weeks after. An unrecognized field
raises rather than being silently dropped.
To hand over a file you already have on disk,
create_custom_indicator_upload / complete_custom_indicator_upload /
wait_for_custom_indicator_upload expose the three steps directly. CSV, JSON,
and JSONL up to 100 MB.
Agent runs
Every other job is fire-and-poll. Agents are not — three states
(pending_plan_approval, pending_action_approval, awaiting_user_input)
cannot advance without you. Iterate the run and answer when it blocks:
sequenceDiagram
participant You
participant Run as AgentRun
participant Engine
You->>Run: create_agent(prompt)
Run->>Engine: POST /agents
Engine-->>Run: run id
loop for event in run
Run->>Engine: GET events (cursor)
Engine-->>Run: new events
alt event.needs_approval
Run-->>You: plan or action awaiting approval
You->>Run: approve() or reject()
Run->>Engine: POST approval
else event.needs_input
Run-->>You: awaiting user input
You->>Run: say("...")
Run->>Engine: POST message
else
Run-->>You: event.text
end
end
Run-->>You: terminal
Note over You,Engine: Without approve/say, the run stalls and bills.<br/>Reattach later with attach_agent(run.id).
run = client.create_agent("Find momentum names in the S&P 500",
idempotency_key="momentum-scan-v1")
for event in run:
print(event.text)
if event.needs_approval:
run.approve()
if event.needs_input:
run.say("Focus on tech")
Lake SQL
Read-only SQL over the NexusTrade market-data lake. Results are durable Parquet parts rather than an implicitly materialized array, so a large result is explicit rather than an out-of-memory surprise.
flowchart LR
A[create_lake_query] --> B[wait_for_lake_query]
B --> C[get_lake_query_manifest]
C --> D[download_lake_query_part]
D --> E[Stream Parquet within your memory budget]
The [lake] extra wraps this pipeline in one call:
import nexustrade as nt
result = nt.lake.sql(
"SELECT ticker, date, closingPrice FROM lake.daily_ohlc WHERE ticker = ?",
["AAPL"],
max_rows=10_000,
)
frame = result.to_pandas() # memory-bounded
for batch in result.iter_batches(): # or stream within your own budget
...
Requires the [lake] extra. NexusTrade resolves lake.* server-side and picks a
compatible backing engine; your SQL does not change when it does.
Authentication
Create a key at nexustrade.io/developers
(Profile → API Keys). Keys start with sk- and are shown once.
client = NexusTradeClient(api_key="sk-...", base_url="https://nexustrade.io/api/v1")
# or set NEXUSTRADE_API_KEY / NEXUSTRADE_API_BASE_URL and:
client = NexusTradeClient.from_environment()
Both variables are also read from a .env file at or above the current
directory, so a local project works with no exports and no python-dotenv:
# .env
NEXUSTRADE_API_KEY=sk-...
NEXUSTRADE_API_BASE_URL=https://nexustrade.io/api/v1
The real environment always wins — a .env value is used only when the variable
is absent, so a stale file can never override what you exported. Nothing is
written back to os.environ. Opt out with NEXUSTRADE_DISABLE_DOTENV=1.
| Scope | Grants |
|---|---|
read |
get_backtest, get_optimization, get_walk_forward |
write |
create_portfolio, create_backtest(s), create_optimization, create_walk_forward |
lake |
Lake catalog, query lifecycle, manifests, result parts |
A key missing the scope gets 403 insufficient_scope.
OAuth is not accepted here. NexusTrade's OAuth flow serves the MCP server. These endpoints take
sk-API keys only; a bearer JWT is rejected with401 invalid_token.
Transport hardening. HTTPS is required (except loopback). The client refuses cross-origin redirects, so the credential cannot be replayed to another host, and refuses to follow a redirect on any non-GET request, so a redirect can never re-submit a paid job.
Idempotency
Every mutation takes a key. Reusing the same key with the same request returns the original resource instead of launching a second paid job — so a retry after a network failure is free.
client.create_backtest(handle, idempotency_key="momentum-2024-v1")
Errors
from nexustrade import NexusTradeApiError
try:
client.create_backtest(handle, idempotency_key="run-1")
except NexusTradeApiError as error:
if error.code == "rate_limit_exceeded":
...
raise
| Status | Code | Meaning |
|---|---|---|
| 401 | invalid_token |
Missing, malformed, or expired key (or an OAuth JWT) |
| 403 | insufficient_scope |
Key lacks read, write, or lake |
| 400 | invalid_request, invalid_portfolio |
Malformed input |
| 400 | invalid_idempotency_key |
Must match [A-Za-z0-9._:-]{1,160} |
| 409 | idempotency_conflict |
Key reused with a different payload |
| 409 | idempotency_in_progress |
Same key, first call still running. Re-poll, do not resubmit |
| 404 | not_found, operation_not_found |
Unknown or not yours |
| 429 | rate_limit_exceeded |
Back off and retry |
status is 0 when no HTTP status describes the failure: transport_error
(never reached the API), unsafe_redirect, or an invalid_response envelope
check on an otherwise-successful reply.
Timeouts
HttpTransport(timeout_seconds=...) (default 30) is urllib's per-socket-operation
timeout, so a slow-but-progressing response is not cut off mid-stream. Neither it
nor the poll timeout bounds how long a job takes.
Scope
Portfolio drafting, backtesting, optimization, walk-forward studies, and
read-only SQL over the market-data lake, versioned under /api/v1/nexustrade.
The screener and live trading remain outside this surface.
Using this SDK with a coding agent
See AGENTS.md — the conventions, invariants, and recipes an agent needs to write correct NexusTrade strategies on the first pass.
License
MIT
Release files for nexustrade 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nexustrade-1.0.0.tar.gz | 103.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nexustrade-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size:167.3 kB
Release files / nexustrade-1.0.0.tar.gz
| Download URL | nexustrade-1.0.0.tar.gz |
|---|---|
| Size | 103.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7f78abbc459a3bf87b572ca77476a7044df89b9c249588e9a4b0416102b8378d
|
|
BLAKE2b-256 checksum How to use checksums |
d3ec5cea0ee01da0930e284d0148b014dbafaef0284df8316ddc8dcae812a904
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.20
|
Release files / nexustrade-1.0.0-py3-none-any.whl
| Download URL | nexustrade-1.0.0-py3-none-any.whl |
|---|---|
| Size | 63.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0ef8c248ab9b28070775b39081b8e15b50a0ed88391d2a921185c9462e813e10
|
|
BLAKE2b-256 checksum How to use checksums |
7c480f87b7eb53b76d22a1ba178e99562ce873bc6d6e357957e7dc52915b1287
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.20
|