Skip to main content
NexusTrade

NexusTrade Python SDK

Author trading strategies in typed Python. Backtest them on the engine that runs them live.

PyPI Python License Types

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 with 401 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)

Source distribution for nexustrade 1.0.0
File Size Uploaded
nexustrade-1.0.0.tar.gz 103.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nexustrade 1.0.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

1.15.0

2 release files

1.14.0

2 release files

1.13.0

2 release files

1.12.0

2 release files

1.11.0

2 release files

1.9.9

2 release files

1.9.6

2 release files

1.9.3

2 release files

1.9.0

2 release files

1.8.7

2 release files

1.8.1

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

This release

1.0.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page