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
pip install 'nexustrade[documents]' # PDF inspection, OCR, structured extraction
pip install 'nexustrade[compute]' # exact union used by NexusTrade compute

The public distribution is also the package installed in NexusTrade compute sandboxes. Agent-facing helpers such as nexustrade.host, nexustrade.scanned_table, nexustrade.signal, nexustrade.report, and nexustrade.tigris therefore have one canonical implementation. Host-backed operations still require the short-lived environment supplied by a compute run.

Use nt.prepare_web_pages(pages, max_chars_per_document=1_000_000) for deterministic HTML inspection without a model call. pages maps source IDs to HTML strings/bytes, {"html": ..., "url": ...} objects, or successful host.fetch result rows. Each result has document and error; successful documents contain complete visible_text plus separate title, description and publication hints. The helper removes hidden content and page chrome and decodes HTML entities using the same parser as extract_web_pages and host evidence verification. Select contiguous report quotes from document["visible_text"]; do not concatenate metadata or separate passages. Over-budget text returns an explicit error, so a partial source is never presented as complete.

Both web helpers also accept successful fetch receipts typed text/markdown, including rendered fallback responses. These preserve the returned Markdown verbatim in visible_text and retain content_type; publisher metadata remains unset. Rendered text is evidence from that fetch response, not original HTML.

For document-derived computation, keep extraction and interpretation separate. extract_rows/extract_pdfs preserve source observations. A corpus can recover document-level facts and logical rows in one schema-bound pass:

extracted = nt.extract_pdfs(
    documents,  # PDF bytes or successful host.fetch result objects
    document_schema={"report_date": "string", "filing_type": "string"},
    rows_schema={"asset": "string", "transaction_date": "string"},
    instructions="Return the rows requested by the task, preserving repeated rows.",
)

for source_id, result in extracted.items():
    document = result["document"]  # includes source_id
    rows = result["rows"]          # each includes source_id + _source_row_index

The helper sends the supplied PDF corpus in one schema-bound request by default when it fits the gateway's combined file-input limit. It partitions only at that real byte boundary. By default, a transport or structured-output failure remains one failed logical request; it is not retried as smaller paid model requests. Set min_rows_per_document=1 only when the selected source class guarantees every document contains at least one logical row; this constrains the same request and prevents a schema-valid empty shell. Omit it when a valid document may be empty. In a full JSON Schema row object, explicitly declared required fields retain their non-null types; undeclared fields are made nullable for strict output. Use that standard schema mechanism only for source-guaranteed row anchors. Set rows_retries explicitly only to retry the same peer corpus. Set documents_per_request only to intentionally partition a corpus; use 1 for the compatibility one-document OCR path. Use inspect_document only when a concrete ambiguity actually needs visual review. For complete-corpus work, treat search results as leads rather than an inventory. Use the publisher's listing/API/index boundary; when its metadata does not prove document membership, extract the minimal identity fields with document_schema and exclude mismatches before projecting rows. derive_rows then adds a schema-bound derived object while retaining every raw record unchanged:

projected = nt.derive_rows(
    source_rows,
    instruction={
        "request": user_request,
        "decision": "Derive the requested component predicates from the complete record.",
    },
    derived_schema={
        "type": "object",
        "properties": {
            "eligible": {"type": "boolean"},
            "resolution_status": {"type": "string"},
            "evidence": {"type": "array", "items": {"type": "string"}},
        },
    },
    evidence_requirements={"eligible": "always"},
)

evidence_requirements is optional. Its values are always, truthy, falsey, or nonempty. When a condition applies, the model must cite a scalar value through an RFC 6901 JSON Pointer relative to the same raw row. The SDK resolves the pointer and attaches the immutable value under evidence_refs; free-form model quotations are not the evidence boundary. Malformed multi-row responses are split into smaller batches. When splitting reaches its configured depth, that terminal batch receives up to two schema-repair attempts by default via max_validation_retries. Repair feedback includes bounded valid scalar paths from the same row, never another row or a hidden answer. Gateway transport retries remain a separate policy.

For acceptance-critical semantic filters, code computes the requested Boolean predicate from independent derived fields and may then audit only the proposed inclusions. Preserve each raw record and its candidate predicates; the audit can block direct contradictions or explicit exclusions but cannot invent a stricter proof burden from missing corroboration:

audited = nt.audit_inclusions(
    proposed,
    instruction="The user's exact inclusion and exclusion contract.",
)

A blocked audit result always includes a host-validated record-local evidence reference. Its value is copied by the SDK from the proposed row, not authored by the model. The exact result shape is one {raw, derived} pair per input; derived contains the model-reported required_predicate_contradicted and explicit_exclusion_present components, reason, and evidence_refs. The SDK mechanically adds inclusion_supported as the negation of those two blockers.

When a material task-local decision already has a positive condition, proposed outcome, reason, and same-record evidence pointers, independently verify that the proposal agrees with the complete record before trusting it:

verification = nt.verify_semantic_citations(
    evidence_id="semantic-batch-1",
    request=user_request,
    assertions=[
        {
            "assertionId": "source-row-1",
            "completeRecordEvidence": source_row,
            "criteria": [
                {
                    "criterionId": "requested-state",
                    "positiveCondition": "The requested state is present.",
                    "proposedOutcome": "true",
                    "proposedReason": "The complete record supports it.",
                    "citedPaths": ["/status"],
                }
            ],
        }
    ],
)

The host resolves each RFC 6901 pointer, exposes all scalar evidence from that same record to the native-Luna verifier, and rejects changed IDs, lost decisions, or cross-record citations. Verdicts are supported, contradicted, or insufficient; the verifier never rewrites the proposal.

The model can only return task-specific derived fields keyed to a host-owned input index; it cannot rewrite or drop the raw record.

Quickstart

from nexustrade import NexusTradeClient, always, backtest, buy, portfolio, stock_asset, strategy

client = NexusTradeClient()

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"])

Backtest operations may include warnings: list[str] immediately after submission and again in the terminal result. Treat them as material caveats; they do not change a successful operation into a failure.

Collateral at risk

A terminal operation's result["statistics"] answers how much capital the run had on the line, not only what it returned. Two keys carry it, typed as BacktestCollateralStatistics:

Key Meaning
peakReservedCollateral Largest collateral locked at any tick, in account currency
medianReservedCollateral Median across the ticks that held at least one position

Both are optional and may be None, and that absence is a real answer: a backtest run before the engine reported collateral has no value, which is not the same as a book that locked nothing. Display "not recorded" rather than $0 — a zero here reads as "this strategy risks nothing", the opposite of what an unpopulated field means. Do not substitute the portfolio's value either: a book risking a few thousand dollars would be reported as risking all of it.

Never reconstruct either number from cash - buyingPower. Buying power is clamped at both ends and carries an open credit-spread premium term, so the inversion breaks precisely on the heavily collateralised books this measures.

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 sequence
Anchored levels IndicatorAtEntry LastOrderPrice IndicatorAtMinutesAfterOpen IndicatorWindowAgo

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"])

Deploying a portfolio

Authoring and backtesting a book does not persist it. save writes it to your account; deploy starts running it.

book = portfolio("Momentum", [...])

book.save(idempotency_key="momentum-v1", client=client)   # persists; sets book.id
deployment = book.deploy(client=client)                   # starts paper trading
book.undeploy(client=client)                              # stops it

save and deploy produce different ids, and the distinction matters. save persists a draft and sets book.id to it. deploy mints the real paper portfolio and returns its own portfolioId — deploying creates a portfolio rather than converting the draft into one, so the two ids coexist. Hold on to deployment["portfolioId"] for anything that reads live state; book.id addresses the draft.

deployment["portfolioId"]      # the running portfolio
deployment["deploymentType"]   # paper, unless you deployed an existing live one
deployment["outcome"]          # created | reactivated

Every handle method takes client= as a keyword argument and falls back to NexusTradeClient.from_environment() when omitted. The same operations exist on the client itself — client.deploy(portfolio_id), client.undeploy(...) — when you have an id rather than a handle.

client.list_portfolios(include_paper=True, include_positions=True)
client.get_portfolio(portfolio_id)

Fetched portfolios include a read-only policy snapshot. Trading policy changes are intentionally unavailable through the SDK; edit them in Portfolio Settings. Portfolio authoring and backtest payloads omit this server-owned snapshot even when they start from a fetched handle.

list_portfolios filters with include_paper, include_live, include_inactive, include_chat_portfolios, search, limit, and page. include_positions defaults off when search is set.

A portfolio you create here is always paper, and minting a live one still happens in the web app. Orders and brokerage status are reachable from here; see Live trading.

But deploy can start live trading. Given the id of a portfolio that is already deployed, it reactivates that portfolio as whatever it already is — so client.deploy(id) on a paused live portfolio resumes live trading against the connected brokerage, and include_live=True above will hand you such an id. Check deployment["deploymentType"] before treating a deploy as simulated.

Live trading

Live trading needs a brokerage linked to your account. Linking is an OAuth redirect, so an API key cannot complete it — a human opens the URL.

client.list_brokerages()
# [{"brokerage": "Alpaca", "connected": False,
#   "connectUrl": "https://nexustrade.io/live-trading"}, ...]

client.connect_brokerage("Alpaca")   # prints the URL, waits until connected

connect_brokerage waits by default only when stdout is a terminal. In CI, cron, or run_compute it raises brokerage_not_connected immediately with the URL in the message, rather than stalling for five minutes in front of nobody. Pass wait=True or wait=False to force either.

A live-only listing that comes back empty raises the same error rather than an empty list, since an empty array says nothing about why:

client.list_portfolios(include_live=True, include_paper=False)
# NexusTradeApiError: brokerage_not_connected: No live portfolios, and no
# brokerage is connected. Connect one at https://nexustrade.io/live-trading

Orders

result = client.create_orders(
    portfolio_id,
    [{"asset": {"name": "SPY", "type": "STOCK", "symbol": "SPY"},
      "side": "BUY", "quantity": 10, "orderType": "MARKET"}],
    idempotency_key="rebalance-2024-04-01",
)

# Dollar notional (stock/crypto only — options require contract quantity):
client.create_orders(
    portfolio_id,
    [{"asset": {"name": "AAPL", "type": "STOCK", "symbol": "AAPL"},
      "side": "BUY", "amount": 500, "orderType": "MARKET"}],
    idempotency_key="buy-aapl-500",
)

Paper orders are accepted immediately. Live orders are staged for approval and are never sent to a broker by this call.

if result["requiresApproval"]:
    print("nothing has traded yet — approve at", result["approvalUrl"])

There is no argument, scope, or flag that submits a live order without approval. The brokerage boundary refuses an unapproved live order regardless of what any caller asks for, so this is a property of the system rather than a promise made by this method. At most 50 orders per request.

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",
        "point_kind": "observation",
        "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.

Declare point_kind whenever the time semantics are known: observation for point-in-time samples, period_aggregate plus aggregate_period (1d, 1w, 1mo, or 1q) for closed-period values, and disclosed for values with an explicit publication time on every row. The SDK applies this contract before both inline and large-upload writes. In particular, a same-day date-only observation becomes an explicit same-day UTC instant instead of being shifted to the next calendar day by the conservative date-only ingestion fallback.

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
replace_custom_indicator_points(id, points, idempotency_key=...) Replace points, retain id
archive_custom_indicator(id) / restore_custom_indicator(id) Reversible lifecycle
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",
                      cost_ceiling_usd=20)
for event in run:
    print(event.text)
    if event.needs_approval:
        run.approve()
    if event.needs_input:
        run.say("Focus on tech")

Natural language

Describe the screen instead of writing the SQL. The server generates it, validates it against the same lake.* catalog the engine reads, executes it, and hands back both the rows and the statement.

import nexustrade as nt

screen = nt.nl.screen_stocks(
    "technology stocks with a market cap over 100 billion and a PE under 30"
)
print(screen.rows)
print(screen.sql)  # always check the SQL — it is model-generated

The low-level client methods are there when you want to poll yourself:

started = client.create_nl_screen("large cap biotech with positive free cash flow")
done = client.wait_for_nl_screen(started["id"])

return_query defaults to True because the SQL is the audit trail: without it the rows are a number you cannot re-derive. It is returned on failure whatever you pass, since a rejected query is the most useful thing to read.

Branch on outcome, not on status alone:

outcome Meaning
ROWS Matches found
EMPTY Every filter ran and nothing cleared them all — an answer
CLARIFICATION The question was ambiguous; clarification asks
GENERATION_FAILED The retry budget was spent — the only case worth retrying

This spends LLM credits. The structured nt.lake API below does not.

SEC statements and filing facts

Run Compute can read point-in-time SEC fundamentals without reconstructing filing selection in ad hoc SQL:

import nexustrade as nt

statement = nt.sec.statement(
    ticker="GOOGL",
    periods=10,
    cadence="annual",
    as_of="2026-08-28",
)

candidates = nt.sec.fact_candidates(
    ticker="GOOGL",
    roles=[
        "pretax_income",
        "income_tax_expense",
        "interest_expense",
        "cash_taxes_paid",
        "cash_interest_paid",
        "research_and_development",
        "stock_based_compensation",
        "diluted_shares",
        "depreciation_and_amortization",
        "capital_expenditures",
        "operating_cash_flow",
        "current_operating_assets",
        "current_operating_liabilities",
    ],
    periods=10,
    cadence="annual",
    as_of="2026-08-28",
)

statement["rows"] is ordered newest first and keeps accession, form, filing URL, availability time, and archive provenance. fact_candidates returns the underlying concepts plus a reconciliation result for each role and period. Working-capital components are deliberately not presented as a reported total; the result says when a component set is incomplete or requires review. SEC CompanyFacts does not expose inline-XBRL dimensional contexts, so candidates also say that their consolidated-versus-dimensional scope is not proven.

Financial-model arithmetic

The base install includes dependency-free accounting and valuation helpers. They compute disclosed assumptions; they do not choose forecasts, tax rates, capital structures, or missing inputs.

operating_profit_after_tax = nt.finance.nopat(operating_income, tax_rate)
working_capital_change = nt.finance.change_in_operating_nwc(
    nt.finance.operating_nwc(current_operating_assets, current_operating_liabilities),
    nt.finance.operating_nwc(prior_operating_assets, prior_operating_liabilities),
)
historical_fcff = nt.finance.fcff(
    operating_profit_after_tax,
    depreciation_and_amortization,
    capital_expenditures,
    working_capital_change,
)

terminal_value = nt.finance.gordon_growth_terminal_value(
    forecast_fcff[-1], discount_rate, perpetual_growth_rate
)
enterprise_value = nt.finance.enterprise_value_from_fcff(
    forecast_fcff, discount_rate, terminal_value
)
equity_value = nt.finance.enterprise_to_equity_value(
    enterprise_value,
    cash_and_non_operating_assets,
    debt_and_debt_like_liabilities,
)
per_share = nt.finance.per_share_value(equity_value, diluted_shares)

Other helpers cover CAPM cost of equity, WACC, probability-weighted values, margin of safety, invested capital, net investment, ROIC, incremental ROIC, reinvestment rate, EVA, and conventional IRR. Build one model object from these results. For a compute report, pass that object with research findings, exact source identities, method requirements, and assumptions to nexustrade.report.write(inputs=analysis_outputs, model=model, images=chart_files). The full current model is exported as calculationModel; selected report.ref fields organize the handoff without hiding other computed sections. Keep this object focused on calculation data, assumptions and provenance; retain raw source documents separately. The host Report Generator authors the report, and the same generated document is graded and delivered. Optional local Markdown exports are not report inputs.

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.

Complete method reference

Every public method on NexusTradeClient. A test in this package fails if one is missing here, so this list cannot drift from the code.

Live trading and orders

Method Purpose
list_brokerages() Every connectable brokerage and whether it is linked
get_brokerage(brokerage) Whether one brokerage is linked
connect_brokerage(brokerage, wait=…) Print the connect URL and wait for the link
create_orders(portfolio_id, orders, idempotency_key=…) Stage orders; live ones need approval

Portfolios

Method Purpose
create_portfolio(book, idempotency_key=…) Persist a portfolio definition
list_portfolios(…) List portfolios, with filters and pagination
get_portfolio(portfolio_id) Read one portfolio
update_portfolio(id, operations, idempotency_key=…) Rename or edit strategies deterministically
fork_public_portfolio(shared_id, idempotency_key=…) Fork a public portfolio into the workspace
deploy(portfolio_id, frequency=…) Start paper trading it
undeploy(portfolio_id) Stop it

Backtests

Method Purpose
create_backtest(handle, idempotency_key=…) Submit one backtest
create_backtests(handles, idempotency_key=…) Submit many in one request
get_backtest(backtest_id) Read the operation
wait_for_backtest(backtest_id, …) Block until terminal
wait_for_backtests(operations, …) Block on a whole batch

Optimization and walk-forward

Method Purpose
create_optimization(handle, idempotency_key=…) Submit an optimization
get_optimization(optimization_id) Read the operation
wait_for_optimization(optimization_id, …) Block until terminal
create_systematic_sweep(handle, idempotency_key=…) Submit an explicit-gene sweep
get_systematic_sweep(optimization_id) Read the sweep operation
wait_for_systematic_sweep(optimization_id, …) Block until terminal
create_walk_forward(handle, idempotency_key=…) Submit a walk-forward study
get_walk_forward(study_id) Read the operation
wait_for_walk_forward(study_id, …) Block until terminal

Custom data sources

Method Purpose
create_custom_indicator(spec, idempotency_key=…) Create a series, optionally seeded
list_custom_indicators(include_archived=…) List owned series
get_custom_indicator(id) Read one, with its point count and range
append_custom_indicator_points(id, points, idempotency_key=…) Add points
replace_custom_indicator_points(id, points, idempotency_key=…, allow_shrink=…) Replace the complete series while retaining its id
archive_custom_indicator(id, confirm=…) Soft-archive a series
restore_custom_indicator(id) Restore an archived series
create_custom_indicator_upload(id, …) Open an upload slot (CSV/JSON/JSONL)
complete_custom_indicator_upload(id, job_id) Start validating uploaded bytes
get_custom_indicator_upload(id, job_id) Read the upload operation
wait_for_custom_indicator_upload(id, job_id, …) Block until validated

Agent runs

Method Purpose
create_agent(prompt, idempotency_key=…) Start a run
get_agent(agent_id) Read its status
attach_agent(agent_id, cursor=…) Reattach to a run already in flight

Lake SQL

Method Purpose
create_lake_query(request, idempotency_key=…) Submit read-only SQL
get_lake_query(query_id) Read the operation
wait_for_lake_query(query_id, …) Block until terminal
cancel_lake_query(query_id) Cancel an owned query
create_lake_ask(question) Ask the lake in plain language
get_lake_ask(ask_id) Read the operation
wait_for_lake_ask(ask_id, …) Block until terminal
cancel_lake_ask(ask_id) Cancel an owned ask
get_lake_query_manifest(query_id) Schema, checksums, and part metadata
download_lake_query_part(query_id, part, …) Download one Parquet part
get_lake_catalog() List queryable tables
describe_lake_table(table) Columns and types for one table

Natural language

Method Purpose
create_nl_screen(question, return_query=…) Screen stocks from a plain-language question
get_nl_screen(screen_id) Read the operation
wait_for_nl_screen(screen_id, **options) Block until terminal
cancel_nl_screen(screen_id) Cancel an owned screen

Client construction

Method Purpose
NexusTradeClient(api_key=…, base_url=…) Explicit credentials
NexusTradeClient() Lazy anonymous workspace with strict limits
NexusTradeClient.from_environment() Read them from the environment or .env
export_workspace_session() Export an anonymous workspace for later use
import_workspace_session(token) Resume an existing anonymous workspace

Portfolio handle — returned by the portfolio(...) builder and by get_portfolio / list_portfolios.

Method Purpose
save(idempotency_key=…, client=…) Persist it as a draft, setting .id
backtest(start_date=…, end_date=…, idempotency_key=…, …) Backtest it, preferring the saved id
deploy(frequency=…, client=…) Mint the real paper portfolio (new id)
undeploy(client=…) Deactivate its deployment

Authentication

An API key is optional. With no key, the first API operation lazily creates a real unregistered NexusTrade workspace and applies stricter request, backtest, and AI limits. Anonymous workspaces can create, edit, and fork portfolios, launch backtests, and use the programmatic agent/chat surface. Every optimization operation—including genetic and systematic sweep launches, result reads, reruns, promotion, and out-of-sample workflows—requires a registered API key. Export the workspace's opaque token if the work must survive a new process:

guest = NexusTradeClient()
guest.list_portfolios()
token = guest.export_workspace_session()

resumed = NexusTradeClient(workspace_session=token)

An expired explicit workspace token raises NexusTradeWorkspaceSessionExpiredError; the SDK never creates a replacement workspace that would make saved work appear deleted.

Registered users can create a key at nexustrade.io/developers (Profile → API Keys). Keys start with sk- and are shown once. When both credentials are provided, registered Authorization takes precedence and the workspace header is not sent.

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 full surface requires a registered API key; anonymous workspaces are limited to portfolio authoring/forking, backtests, and programmatic agent/chat calls. The screener and creating a live deployment remain outside this surface. Orders are reachable, but a live order is only ever staged for human approval — never submitted. deploy and undeploy act on whatever an existing id already is, live included.

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.

Reproducible research models

The compute helpers keep evidence and arithmetic separate from investment judgment:

General host.search(query) keeps neutral research search terms. Use host.search(query, prefer_machine_readable=True) for dataset/API discovery; host.queue_search has the same neutral default.

Native research investigators may call host.search(query, allow_broker_fallback=False). Pending or unavailable gateway work then raises explicitly without replacing the primary executor's broker queue or exiting. Return the unresolved query to the primary executor; it is not source absence. Successful research calls still append shared durable receipt/cache rows.

  • nt.sec.resolved_fact(...) selects a complete fact reconciliation with its filing provenance. Partial, ambiguous, and cumulative inputs remain unresolved.
  • nt.sec.latest_statement(annual, quarterly, as_of="2026-06-30", required_fields=["cash", "long_term_debt"]) selects the latest supplied period and publicly available amendment, retaining filing and share provenance. A missing current field raises instead of falling back to an older balance. Fetch both cadences first; this helper cannot discover an unfetched filing. Matching annual/derived-Q4 views of one filing resolve to the annual row.
  • nt.finance.operating_forecast_period(...) calculates FCFF and rolls invested capital from the same operating inputs. Equity compensation stays expensed; additional cash investment and noncash capital changes are separate.
  • nt.finance.fcff_valuation_case(..., terminal_value=...) accepts an end-of-forecast terminal enterprise value, including the NOPAT/RONIC helper's output. Supply either this value or perpetual_growth_rate, never both.
  • Dated DCF/IRR: present_value_cash_flows, enterprise_value_from_fcff, fcff_valuation_case and internal_rate_of_return accept valuation_date and cash_flow_dates as ISO calendar dates. Dates cover future payments only (exclude IRR's initial outlay), use Actual/365, and place terminal value at the last payment. Supply remaining-period amounts, excluding elapsed cash flows; the helper cannot infer the current-year stub from a full-year forecast.
  • nt.finance.forecast_remainder(...) exposes the remaining-period forecast implied by actuals to date. Align additive flows, fiscal periods and units.
  • Optional period_flow(...) and remaining_period_flow(...) retain inclusive operating dates, information cutoff, units, accounting definition, and declared provenance. The composer rejects overlaps and incompatible definitions/units. An uncovered elapsed interval or missing amount returns value=None, never an automatic proration. A model estimate can fill an interval explicitly; its provenance stays attached. The raw numeric primitives remain available.
  • nt.finance.flow_basis(measure=..., origin=..., adjustments=[...]), passed as period_flow(..., basis=...), records what an amount IS beneath its label. definition is free text: giving a reported CFO-minus-capex proxy and a modeled NOPAT+D&A-capex-dNWC forecast the same definition string satisfies the composer's equality check while leaving the accounting gap untouched. remaining_period_flow returns basis_reconciliation with an agreement from nt.finance.BASIS_AGREEMENTSsingle-basis, differing-bases, no-elapsed-flow, undeclared or undeclared-by-all — plus the declared adjustments and their total. No value means the bridge holds, deliberately: the gap is a difference between two measurements of one period and only one of them is in the record, so whether the adjustments explain it is a question for review rather than something a status can answer. There is nothing here to assert; a boolean was gamed by deleting a declaration, and a bridged status by declaring one adjustment worth zero.
  • nt.finance.elapsed_period_fraction(period_start=..., period_end=..., as_of=...) counts INCLUSIVE elapsed days, the same convention remaining_period_flow uses: as_of is elapsed and the remainder begins the next day. A hand-written (as_of - start).days is one day short. Whether a flow may be prorated by time at all remains the analyst's call.
  • nt.finance.observation_as_of(rows, as_of=..., timestamp_field=..., value_field=...) selects the latest observation whose CALENDAR DATE is on or before as_of, and returns the exact observed_at instant beside the value. A daily bar stamped 2026-09-04 20:00:00 is an observation for 2026-09-04 but is after the instant 2026-09-04, so a WHERE date <= '2026-09-04' cutoff drops that whole day. Compare dates to dates in SQL — CAST(date AS DATE) <= CAST(? AS DATE) — and select the row with this helper rather than reading a number out of console output. A timezone offset is converted, not discarded. On an exact timestamp tie the last such row in the supplied order wins.
  • nt.finance.price_comparison(intrinsic_value, market_price) returns the discount, upside and premium together with definitions naming each denominator, and fcff_valuation_case(..., market_price=...) includes it. The three differ only in denominator, so a bare scalar labelled "discount" leaves a report writer guessing which one it is.
  • nt.finance.hurdle_comparison({"base": ..., "bull": ...}, wacc) derives which cases clear a hurdle from the same numbers the table prints, so a sentence like "only the bull case clears" cannot survive a change to the numbers it describes. Equality is a miss.
  • report.ref("scenarios", "base", "per_share_value") binds a structured finding to the current model when report.write(inputs=inputs, model=model) runs. model= is reference resolution, not automatic model export. Include full required sections in the payload, for example statistics={"history": report.ref("history"), "forecast": report.ref("forecast"), "sensitivity": report.ref("valuation", "sensitivity")} for a model with those paths. Preserve year labels, units and grid axes. Re-read the emitted JSON; a code appendix or chart does not supply omitted numerical series to the author. The host authors report prose from the resulting JSON.

For explicit citation linkage, pass source_aliases={fetch_id: bibliography_id} to report.write (or {} when the namespaces intentionally match). This checks references without rewriting durable fetch IDs. The host still verifies excerpts against fetched bodies; a valid reference does not prove a claim is supported.

For example, a September valuation cannot subtract only first-half operations from a full-year flow and value the entire second half as future cash:

from nexustrade import finance, report

basis = dict(as_of="2027-09-04", unit="USD billions",
             definition="NOPAT plus D&A less capex less change in operating NWC")
annual = finance.period_flow(100, period_start="2027-01-01", period_end="2027-12-31",
                             status="forecast", **basis)
h1 = finance.period_flow(60, period_start="2027-01-01", period_end="2027-06-30",
                         status="derived", provenance={"sourceId": "filing-h1"}, **basis)
remaining = finance.remaining_period_flow(annual, [h1], valuation_date="2027-09-04")
assert remaining["value"] is None
assert remaining["missing_intervals"] == [
    {"period_start": "2027-07-01", "period_end": "2027-09-04"}
]
# Obtain or explicitly model that interval on the same accounting basis before
# using the remainder as a dated cash flow. No amount is estimated by this helper.

Keep whole sec.resolved_fact results in the model, including selected candidates, filing identity, and status. Optional durable references serialize those records alongside current values rather than discarding source meaning:

model = {"remaining_fcff": remaining}  # Save this same model to model.json.
inputs = {"statistics": {"remaining_fcff": report.ref(
    "remaining_fcff", "value", provenance_path=("remaining_fcff",)
)}}
report.write_inputs(inputs, model=model, preserve_references=True,
                    model_source="/work/out/model.json")

modelReferences contains inputPath, modelPath, optional modelSource, and the current provenancePath/provenance object. It refreshes when the model changes. Paths are arrays of object keys/list indices. Supply the actual artifact path for a file-backed model; without model_source, paths identify only the in-memory model argument. This is executor-declared lineage, not independently verified source authority. The host must still check source support and accounting meaning. Omitting preserve_references keeps legacy JSON output unchanged.

License

MIT

Source passages for report evidence

Preserve every passage needed to support the report's claims. The host verifies selected text against its authenticated source receipt; there is no excerpt-count or character allowance to optimize against. Select separate contiguous passages for distant claims, even when they use the same source. Never concatenate distant spans into a quote or clip away a table's headers, units, or qualifications.

from nexustrade import report

excerpts = report.source_excerpts(
    "fetch:annual", prepared_document["visible_text"],
    passages=["Year 2025. Revenue was 17 million.",
              "Year 2026. Revenue was 29 million."],
)
report.write_inputs({"sourceExcerpts": excerpts, "statistics": calculations})

The optional helper preserves the supplied passages, normalizes whitespace, and reports missing or ambiguous matches. It does not choose facts or certify that a claim follows from a quote. The host handles model request capacity separately; research should not discard supported claims to satisfy a prompt-size allowance.

Release files for nexustrade 1.10.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.10.0
File Size Uploaded
nexustrade-1.10.0.tar.gz 347.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for nexustrade 1.10.0
File Interpreter ABI Platform
nexustrade-1.10.0-py3-none-any.whl Python 3 none any Details

Total release size:552.4 kB

Release files / nexustrade-1.10.0.tar.gz

Download URL nexustrade-1.10.0.tar.gz
Size 347.4 kB
Tags Source
SHA-256 checksum
How to use checksums
b643fd4112c0b0f69e13342d46bea4d1c7b9e4fd43267785f8c403d731109cc8
BLAKE2b-256 checksum
How to use checksums
a36045c083437bd53944fdafd5cc7360124113c080d9ee111687fae146fb7158
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / nexustrade-1.10.0-py3-none-any.whl

Download URL nexustrade-1.10.0-py3-none-any.whl
Size 205.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1ba9e5ce69be17cb1634ee39b396a1e14ea559fecb13bb860334fbe2d9d77e25
BLAKE2b-256 checksum
How to use checksums
e6d27194ed53d09e3f02bf3dc82cbc08076fa0e072e9e1e21961c1bd8bf47bb8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

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

This release

1.10.0 This release

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

1.0.0

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