NexusTrade TypeScript SDK
MCP serverCommerce & financeNexusTrade is a trading research and automation platform. Once added, your AI can research trading strategies, backtest them against historical data, and place trades on your behalf. It also gives access to a marketplace of strategies you can subscribe to, fork, or copy from other traders.
Use NexusTrade TypeScript SDK in Claude, ChatGPT or Ahel Desktop
Free. Sign in, add NexusTrade TypeScript SDK and connect your AI. About a minute.
Also: Claude Code · Cursor · Codex
Then ask your AI: use NexusTrade TypeScript SDK
Needs your own NexusTrade TypeScript SDK account. You sign in to it and approve access when you connect.
Details
Available today. Use it from your connected AI after setup.
Add the integration and ask your AI to research or backtest a trading strategy. You can then explore the marketplace or set up copy trading before executing real trades.
What your AI can do with it
- Research quantitative trading strategies
- Backtest strategies against historical market data
- Subscribe to strategies from the marketplace
- Fork and edit existing strategies
- Copy trades from other traders
- Execute trades directly
Install NexusTrade TypeScript SDK
The server’s own address, for the clients that take one directly. Or connect Ahel once and every client you use reads it from one address, with the account kept on Ahel rather than in each client’s config.
Claude Code
claude mcp add --transport http --scope user nexustrade-typescript-sdk 'https://nexustrade.io/api/mcp'Run it once in your project, then open /mcp to approve any sign-in the server asks for.
Claude Desktop
https://nexustrade.io/api/mcpAdd a custom connector in Settings, paste this address, and approve the sign-in.
Cursor
cursor://anysphere.cursor-deeplink/mcp/install?name=nexustrade-typescript-sdk&config=eyJ1cmwiOiJodHRwczovL25leHVzdHJhZGUuaW8vYXBpL21jcCJ9Open the link and Cursor adds the server at that address.
ChatGPT
https://nexustrade.io/api/mcpIn Settings, enable Developer mode, create an MCP app, and paste this address. Your plan and workspace must allow custom apps.
Codex
codex mcp add nexustrade-typescript-sdk --url 'https://nexustrade.io/api/mcp'Run it once, then sign in with codex mcp login nexustrade-typescript-sdk if the server asks for an account.
From the project's README
As published by austin-starks/nexustrade-ts in README.md.
Author trading strategies in typed TypeScript. Backtest them on the engine that runs them live.
Quickstart · Authoring · Polling · Agents · Lake SQL · Auth · Errors
npm install nexustrade
Zero runtime dependencies. ESM and CommonJS builds ship together, with types.
MCP server
NexusTrade also exposes the platform as a hosted, remote Model Context Protocol server. Modern MCP clients connect directly to the production Streamable HTTP endpoint and discover NexusTrade OAuth automatically:
claude mcp add --transport http nexustrade https://nexustrade.io/api/mcp
Cursor and other remote-capable clients use:
{
"mcpServers": {
"nexustrade": {
"url": "https://nexustrade.io/api/mcp"
}
}
}
For Claude Desktop and other stdio-only clients, use the established
mcp-remote bridge—no clone or local NexusTrade server is required:
{
"mcpServers": {
"nexustrade": {
"command": "npx",
"args": [
"-y",
"mcp-remote@latest",
"https://nexustrade.io/api/mcp",
"--transport",
"http-only"
]
}
}
}
The live server exposes more than 120 tools across market research, portfolio construction, backtesting, optimization, walk-forward validation, managed compute, Aurora agents, paper trading, and controlled brokerage operations. Its creator-marketplace tools cover the full strategy adoption path:
search_creatorsdiscovers public creators and their marketplace portfolios.subscribe_portfoliovalidates a monetized listing and returns a safe checkout preview; the user completes payment in NexusTrade, never through the MCP tool.fork_shared_portfoliocreates a one-time editable copy of a marketplace strategy in a new or existing portfolio.copy_trade_sharedcontinuously mirrors an accessible strategy into a paper or live portfolio at an explicit allocation.
See the developer guide, the utility tool reference, and the Aurora tool reference.
Research and historical results are not investment advice and do not guarantee future performance. Keep paper and live modes explicit. Tools that can affect portfolios, schedules, or brokerage orders remain subject to the authenticated account's NexusTrade permissions and approval controls.
Quickstart
import {
NexusTradeClient,
always,
backtest,
buy,
portfolio,
stockAsset,
strategy,
} from "nexustrade";
const client = new NexusTradeClient();
const book = portfolio("Example", [
strategy("Buy SPY", always(), buy(stockAsset("SPY"), 100)),
]);
const operation = await client.createBacktest(
backtest(book, { startDate: "2024-01-01", endDate: "2024-12-31" }),
{ idempotencyKey: "example-v1" }
);
const result = await client.waitForBacktest(operation.id as string);
console.log(result.result);
Backtest operations may include warnings: string[] 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 fields carry it, typed as
BacktestCollateralStatistics:
| Field | 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 null, 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.
TypeScript cannot overload comparison operators, so indicators compose through
gt / gte / lt / lte / eq / neq and and / or:
import * as nt from "nexustrade";
const book = nt.portfolio(
"Momentum",
[
nt.strategy(
"Rotate into strength",
nt.always(),
nt.dynamicRebalance({
universe: nt.universe("SP500"),
pipeline: [
nt.filter(nt.gt(nt.Price(nt.CANDIDATE), nt.SMA(nt.CANDIDATE, 200))),
nt.selectTop(nt.RSI(nt.CANDIDATE, 14), 10),
],
weightIndicator: nt.RSI(nt.CANDIDATE, 14),
limit: 10,
deploymentPercent: nt.Value(80),
})
),
],
{ initialValue: 100_000 }
);
Congressional-disclosure signals are native indicators too. House and Senate are combined unless you explicitly choose a chamber; amount metrics default to the disclosed lower bound and confirmed option disclosures are excluded by default:
const pelosiPurchases = nt.PoliticalTrades(
nt.CANDIDATE,
"Nancy Pelosi",
"BuyAmount",
90,
"LowerBound",
"Equity",
);
Use "" as the filer to include all members. BuyCount counts canonical
purchase events, while BuyAmount measures disclosed magnitude. Select
"Option" only when you want option-disclosure activity associated with the
underlying ticker.
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.stockAsset("SPY"), 10), {
orderExecution: nt.limitOrder({
price: nt.unitPriceLimit(500),
workingTime: nt.goodForDay(),
}),
});
nt.strategy("Sell the spread for $1.50 or better", nt.always(), optionAction, {
orderExecution: nt.limitOrder({
price: nt.minimumNetCredit(1.5),
workingTime: nt.goodForMinutes(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", whenDebitSpreadHeld, creditSpreadAction, {
orderExecution: nt.limitOrder({
price: nt.minimumNetCredit(
nt.Plus(nt.OptionSpreadEntryPrice("SPX", "call", "long", "vertical"), nt.Value(0.5)),
),
workingTime: nt.goodForDay(),
}),
});
currentLimit() 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.
| Group | Examples |
|---|---|
| Price & volume | Price OpeningPrice HighOfDay MinuteBarHigh MinuteBarLow HeikinAshi 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 openOption closeOption |
| Actions | buy sell alert dynamicRebalance rebalanceOption |
| Selection | filter selectTop selectPercentile universe |
| Logic | always atLeast atMost exactly fewerThan multi and or sequence |
| Anchored levels | IndicatorAtEntry LastOrderPrice IndicatorAtMinutesAfterOpen IndicatorWindowAgo |
Every builder is fully typed — your editor completes the whole surface.
Jobs run on the engine — you poll
create* enqueues work and returns immediately. It does not resolve when
results exist. There are no webhooks today.
sequenceDiagram
participant You
participant SDK
participant Engine
You->>SDK: createBacktest(book)
SDK->>Engine: POST (enqueue)
Engine-->>SDK: id, status=queued
SDK-->>You: operation (returns immediately)
loop waitForBacktest — backoff 2s→15s
SDK->>Engine: GET /operations/{id}
Engine-->>SDK: status update
end
SDK-->>You: result (when completed)
Note over You,Engine: Poll timeout throws 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 },
}
const finished = await client.waitForBacktest(operation.id as string);
| Option | Default | Meaning |
|---|---|---|
timeoutSeconds | 900 | Give up waiting (the job keeps running) |
pollIntervalSeconds | 2 | First interval; backs off 1.5× |
maxPollIntervalSeconds | 15 | Interval ceiling |
throwOnFailure | true | Throw on failed/cancelled instead of returning |
A timeout throws operation_timeout and does not cancel the job — call the
waiter again with the same id rather than resubmitting.
Batches. createBacktests submits many in one request and returns one
operation each; waitForBacktests(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:
const study = await client.createWalkForward(
nt.walkForward(book, {
globalStartDate: "2022-01-01",
globalEndDate: "2024-12-31",
foldCount: 4,
}),
{ idempotencyKey: "wf-v1" }
);
await client.waitForWalkForward(study.id as string);
Deploying a portfolio
Authoring and backtesting a book does not persist it. save writes it to your
account; deploy starts running it.
const book = nt.portfolio("Momentum", [
/* … */
]);
await book.save({ idempotencyKey: "momentum-v1" }); // persists; sets book.id
const deployment = await book.deploy(); // starts paper trading
await book.undeploy(); // 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
Handle methods accept an optional transport; omitted, they resolve one from
the environment. The same operations exist on the client — client.deploy(id),
client.undeploy(id) — when you have an id rather than a handle.
await client.listPortfolios({ includePaper: true, includePositions: true });
await client.getPortfolio(portfolioId);
Editing a saved portfolio
updatePortfolio applies deterministic edits with no LLM in the path. The
operations array is a typed union, so the compiler knows which fields each
edit needs.
import type { PortfolioEditOperation } from "nexustrade";
const operations: PortfolioEditOperation[] = [
{ type: "rename", name: "AAPL Income" },
{
type: "replaceStrategy",
targetStrategyId: strategyId, // or targetStrategyName
strategyObject: nt.strategy(
"Buy AAPL",
nt.always(),
nt.buy(nt.stockAsset("AAPL"), 25, "percent of portfolio"),
{
orderExecution: nt.limitOrder({
price: nt.unitPriceLimit(150),
workingTime: nt.goodForDay(),
}),
},
),
},
];
await client.updatePortfolio(portfolioId, operations, {
idempotencyKey: "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
array 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.
Stock eligibility is part of what you author. Pass
policy: { stockEligibility } to portfolio(...), or call
setStockEligibility(...) on a handle, to set market-cap bounds, an industry
filter, missingMarketCapBehavior, or shareClassBehavior. Omitted fields take
the defaults. A GOOG/GOOGL pairs book needs ALL_CLASSES, because the default
keeps one share class per company:
const pair = portfolio("GOOG/GOOGL pair", strategies, {
policy: { stockEligibility: { shareClassBehavior: "ALL_CLASSES" } },
});
Automated trading is never authored. Only the owner enables it, in Portfolio
Settings, and a policy that names automatedApproval is refused before it is
sent. Fetched handles include a typed, read-only policy snapshot; saving or
deploying a copy of one carries its stock eligibility and nothing else.
listPortfolios filters with includePaper, includeLive, includeInactive,
includeChatPortfolios, search, limit, and page. includePositions
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 includeLive: 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.
await client.listBrokerages();
// [{ brokerage: "Alpaca", connected: false,
// connectUrl: "https://nexustrade.io/live-trading" }, ...]
await client.connectBrokerage("Alpaca"); // logs the URL, waits until connected
connectBrokerage waits by default only when stdout is a TTY. In CI, cron,
or run_compute it rejects with 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 rejects with the same error rather than an empty array, since an empty array says nothing about why:
await client.listPortfolios({ includeLive: true, includePaper: false });
// NexusTradeApiError: brokerage_not_connected: No live portfolios, and no
// brokerage is connected. Connect one at https://nexustrade.io/live-trading
Orders
const result = await client.createOrders(
portfolioId,
[
{
asset: { name: "SPY", type: "STOCK", symbol: "SPY" },
side: "BUY",
quantity: 10,
orderType: "MARKET",
},
],
{ idempotencyKey: "rebalance-2024-04-01" }
);
// Dollar notional (stock/crypto only — options require contract quantity):
await client.createOrders(
portfolioId,
[
{
asset: { name: "AAPL", type: "STOCK", symbol: "AAPL" },
side: "BUY",
amount: 500,
orderType: "MARKET",
},
],
{ idempotencyKey: "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) {
console.log("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.
Listing requires a read key; cancellation requires a trade key. Both
require a registered account and are unavailable inside compute sandboxes.
const page = await client.listOrders({
portfolioId,
statuses: ["Accepted", "Pending User Approval"],
page: 1,
limit: 50,
});
console.log(page.orders, page.totalPages);
// Cancel only explicit IDs you selected from the listing.
const cancellation = await client.cancelOrders(["ORDER_ID"], {
portfolioId,
idempotencyKey: "cancel-order-v1",
});
console.log(cancellation.canceled, cancellation.rejected);
Listing defaults to Accepted and Pending User Approval. Request Pending,
Partially Filled, Filled, or Canceled explicitly to inspect other states.
Pages are 1-based, with a default limit of 50 and a maximum of 100. The response
includes orders, total, returned, page, limit, totalPages, truncated,
and ignoredStatuses; unknown statuses are reported there when valid statuses
remain, and a wholly invalid filter raises an error. Set
includeRebalanceOrders: true
for an additional page of pending rebalance actions. Their IDs are separate from
order IDs and cannot be passed to cancellation.
Cancellation accepts at most 50 IDs and returns canceled and rejected arrays.
Each success includes orderId, mode (db, broker, or already_canceled),
and canceledOrderIds, including sibling legs canceled together. Each rejection
includes orderId and reason. Inspect both arrays: HTTP success does not mean
every order was canceled. The optional portfolio ID guards against canceling an
order from another portfolio. Reuse the same idempotency key and identical
payload when retrying; an already canceled order returns already_canceled.
The same cancellation rules as MCP/REST apply: Accepted and
Pending User Approval orders can be canceled; Pending, Partially Filled,
and Filled orders are rejected. Rebalance children must be managed through
their parent action. Live orders with a broker ID invoke broker cancellation.
Creation still stages live orders for human approval.
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.
const series = await client.createCustomIndicator(
{
name: "WSB NVDA Mentions",
scope: "asset",
description: "Daily r/wallstreetbets mentions",
pointKind: "observation",
points: [
{ timestamp: "2024-04-01", value: 152, ticker: "NVDA" },
{ timestamp: "2024-04-02", value: 90, ticker: "NVDA" },
],
},
{ idempotencyKey: "wsb-mentions-v1" }
);
const busy = nt.gt(
nt.CustomIndicator(nt.stockAsset("NVDA"), String(series.customIndicatorId)),
100
);
const book = nt.portfolio("Attention", [
nt.strategy("Buy the buzz", busy, nt.buy(nt.stockAsset("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 pointKind whenever the time semantics are known: observation for
point-in-time samples, period_aggregate plus aggregatePeriod (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. A same-day date-only observation becomes
an explicit same-day UTC instant instead of shifting to the next calendar day.
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 resolves. Either way the returned indicator reflects what actually
landed, and an upload that fails validation rejects rather than reporting
success.
Growing a series. Append to the same id every run:
await client.appendCustomIndicatorPoints(
String(series.customIndicatorId),
[{ timestamp: "2024-04-03", value: 118, ticker: "NVDA" }],
{ idempotencyKey: "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 |
|---|---|
createCustomIndicator(spec, { idempotencyKey }) | Create, optionally seeded |
appendCustomIndicatorPoints(id, points, { idempotencyKey }) | Add points |
replaceCustomIndicatorPoints(id, points, { idempotencyKey }) | Replace points, retain id |
archiveCustomIndicator(id) / restoreCustomIndicator(id) | Reversible lifecycle |
listCustomIndicators() / getCustomIndicator(id) | Discover ids and coverage |
Points accept timestamp, value, ticker, assetType, and availableAt
— camelCase or snake_case, with Date objects allowed. Set availableAt when a
value became knowable later than it is dated: an earnings figure stamped to
quarter-end but published weeks after. An unrecognized field throws rather than
being silently dropped.
To hand over a file you already have on disk, createCustomIndicatorUpload /
completeCustomIndicatorUpload / waitForCustomIndicatorUpload expose the
three steps directly. CSV, JSON, and JSONL up to 100 MB.
Agent runs
Shortened here. Read the whole README on GitHub.
Signals
- GitHub stars
- 6
- Last commit
- Oct 2026
Advanced
- Delivery
- nexustrade-mcp MCP server → your Ahel connector (mcp.ahel.ai) → your AI.
- Item type
- mcp-server
- Key
io-github-austin-starks-nexustrade-mcp- Source
- github.com/austin-starks/nexustrade-ts
- Hosted endpoint
https://nexustrade.io/api/mcp
github.com/austin-starks/nexustrade-ts
More in Commerce & finance
MCP server · mikeypetrillo
More in Commerce & financecrypto-quant-signal-mcp
MCP server · algovaultlabs
More in Commerce & financeHive Intelligence MCP
MCP server · hive-intel
More in Commerce & financekeryx
MCP server · tang-vu
More in Commerce & financetruss44-mcp-crypto-price
MCP server · truss44
More in Commerce & financetrading-mcp
MCP server · ai-rook
More in Commerce & finance