NexusTrade TypeScript SDK

MCP serverCommerce & finance

NexusTrade 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

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.

NexusTrade TypeScript SDKStart free

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/mcp

    Add 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=eyJ1cmwiOiJodHRwczovL25leHVzdHJhZGUuaW8vYXBpL21jcCJ9

    Open the link and Cursor adds the server at that address.

  • ChatGPT

    https://nexustrade.io/api/mcp

    In 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_creators discovers public creators and their marketplace portfolios.
  • subscribe_portfolio validates a monetized listing and returns a safe checkout preview; the user completes payment in NexusTrade, never through the MCP tool.
  • fork_shared_portfolio creates a one-time editable copy of a marketplace strategy in a new or existing portfolio.
  • copy_trade_shared continuously 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:

FieldMeaning
peakReservedCollateralLargest collateral locked at any tick, in account currency
medianReservedCollateralMedian 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.

GroupExamples
Price & volumePrice OpeningPrice HighOfDay MinuteBarHigh MinuteBarLow HeikinAshi VWAP Volume GapPercentage
TechnicalsSMA EMA RSI BollingerBand AverageTrueRange CrossAbove
Position statePositionValue PositionPercentChange PositionMaxDrawdown
Portfolio statePortfolioValue BuyingPower MaxDrawdown InitialValue
FundamentalsFundamental Economic DaysUntilEarnings IsIndexMember IsIndustry
OptionsOptionDaysToExpiration OptionCollateral OptionSpreadEntryPrice OptionUnrealizedPnL openOption closeOption
Actionsbuy sell alert dynamicRebalance rebalanceOption
Selectionfilter selectTop selectPercentile universe
Logicalways atLeast atMost exactly fewerThan multi and or sequence
Anchored levelsIndicatorAtEntry 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);
OptionDefaultMeaning
timeoutSeconds900Give up waiting (the job keeps running)
pollIntervalSeconds2First interval; backs off 1.5×
maxPollIntervalSeconds15Interval ceiling
throwOnFailuretrueThrow 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.

CallPurpose
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