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
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)
Order execution belongs to the strategy. Omit it for the backward-compatible Market default, use a fixed unit price for Buy/Sell, or set an option strategy's maximum net debit / minimum net credit:
nt.strategy(
"Buy SPY at my price",
nt.always(),
nt.buy(nt.stock_asset("SPY"), 10),
order_execution=nt.limit_order(
price=nt.unit_price_limit(500),
working_time=nt.good_for_day(),
),
)
nt.strategy(
"Sell the spread for $1.50 or better",
nt.always(),
option_action,
order_execution=nt.limit_order(
price=nt.minimum_net_credit(1.5),
working_time=nt.good_for_minutes(30),
),
)
An option net limit can also follow a position the strategy holds. Pass an indicator instead of a number and the engine evaluates it when the strategy fires. If it has no positive value at that moment, no order is placed:
nt.strategy(
"Sell the credit spread for the debit fill plus $0.50",
when_debit_spread_held,
credit_spread_action,
order_execution=nt.limit_order(
price=nt.minimum_net_credit(
nt.Plus(nt.OptionSpreadEntryPrice("SPX", "call", "long", "vertical"), nt.Value(0.5))
),
working_time=nt.good_for_day(),
),
)
current_limit() creates a quote-relative Limit for dynamic rebalance
strategies. It keeps the no-worse-than-current-quote protection, but it is not a
resting price target. Live option strategies must choose an explicit Limit
policy.
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 OptionSpreadEntryPrice 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)
Editing a saved portfolio
update_portfolio applies deterministic edits with no LLM in the path. The
operations sequence is a typed union of TypedDicts, so a type checker knows
which keys each edit needs.
from nexustrade import PortfolioEditOperation
operations: list[PortfolioEditOperation] = [
{"type": "rename", "name": "AAPL Income"},
{
"type": "replaceStrategy",
"targetStrategyId": strategy_id, # or targetStrategyName
"strategyObject": nt.strategy(
"Buy AAPL",
nt.always(),
nt.buy(nt.stock_asset("AAPL"), 25, "percent of portfolio"),
order_execution=nt.limit_order(
price=nt.unit_price_limit(150),
working_time=nt.good_for_day(),
),
),
},
]
client.update_portfolio(portfolio_id, operations, idempotency_key="aapl-limit-v1")
The five edits are rename, addStrategies, removeStrategies,
replaceStrategy, and replaceStrategies. Deploy, undeploy, delete,
scheduling, and trading-policy operations are not reachable on this route.
replaceStrategies replaces the whole set, so a strategy left out of the
list is deleted. Carry unchanged strategies through verbatim, including the
orderExecution each already has, or a working Limit silently reverts to
Market. removeStrategies takes strategy ids from a fetched portfolio; removal
by name is rejected.
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 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 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 orperpetual_growth_rate, never both.- Dated DCF/IRR:
present_value_cash_flows,enterprise_value_from_fcff,fcff_valuation_caseandinternal_rate_of_returnacceptvaluation_dateandcash_flow_datesas 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(...)andremaining_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 returnsvalue=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 asperiod_flow(..., basis=...), records what an amount IS beneath its label.definitionis 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_flowreturnsbasis_reconciliationwith anagreementfromnt.finance.BASIS_AGREEMENTS—single-basis,differing-bases,no-elapsed-flow,undeclaredorundeclared-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 abridgedstatus by declaring one adjustment worth zero.nt.finance.elapsed_period_fraction(period_start=..., period_end=..., as_of=...)counts INCLUSIVE elapsed days, the same conventionremaining_period_flowuses:as_ofis elapsed and the remainder begins the next day. A hand-written(as_of - start).daysis 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 beforeas_of, and returns the exactobserved_atinstant beside the value. A daily bar stamped2026-09-04 20:00:00is an observation for 2026-09-04 but is after the instant2026-09-04, so aWHERE 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 withdefinitionsnaming each denominator, andfcff_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 whenreport.write(inputs=inputs, model=model)runs.model=is reference resolution, not automatic model export. Include full required sections in the payload, for examplestatistics={"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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nexustrade-1.14.0.tar.gz.
File metadata
- Download URL: nexustrade-1.14.0.tar.gz
- Upload date:
- Size: 356.0 kB
- Tags: Source
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9a02093555b565500dfffd108b15a2aa380cf7da307f95654f2e198da9b2c8ce
|
|
| MD5 |
2e83f33e2dffdf4bab04a5ce2487e245
|
|
| BLAKE2b-256 |
bdd56e6d1900ebf3a16a7ab34731c576998a996d832fe7917bbbb0b179ac7f30
|
File details
Details for the file nexustrade-1.14.0-py3-none-any.whl.
File metadata
- Download URL: nexustrade-1.14.0-py3-none-any.whl
- Upload date:
- Size: 208.6 kB
- Tags: Python 3
- Uploaded using 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}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee9e1b2c78325f4c5cc0805777cf257d530e3f7f0f53b5ded6fc569275160052
|
|
| MD5 |
2ef9f44084069ef3ed8b5381719bd1ac
|
|
| BLAKE2b-256 |
034bfeb03914e86fb760ce43ed4db749200857696923fb915bc2ebb464c96e26
|