NexusTrade Financial MCP
About
Quantitative research, backtesting, creator-marketplace subscriptions, editable strategy forks, continuous copy trading, and controlled brokerage workflows through more than 120 MCP tools.
Details
- Author
- austin-starks
- Categories
- Finance, Other
Jump to
Setup
Install NexusTrade Financial MCP in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/austin-starks/nexustrade-ts
Follow the installation instructions in the repository README, then restart your MCP client.
Author trading strategies in typed TypeScript. Backtest them on the engine that runs them live.
Quickstart·Authoring·Polling·Agents·Lake SQL·Auth·Errors
Zero runtime dependencies.ESM and CommonJS builds ship together, with types.
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 establishedmcp-remotebridge—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:
See thedeveloper guide, theutility tool reference, and theAurora 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.
import { NexusTradeClient, always, backtest, buy, portfolio, stockAsset, strategy, } from "nexustrade"; const client = new NexusTradeClient({ apiKey: "sk-...", baseUrl: "https://nexustrade.io/api/v1", }); 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 includewarnings: string[]immediately after submission and again in the terminalresult. Treat them as material caveats; they do not change a successful operation into a failure.
Every builder is generated from the same indicator specification the NexusTrade engine runs, so a book isvalid by constructionrather than by convention.
TypeScript cannot overload comparison operators, so indicators compose throughgt/gte/lt/lte/eq/neqandand/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: 80, }) ), ], { initialValue: 100_000 } );
Every builder is fully typed — your editor completes the whole surface.
createenqueues work and returns immediately. It doesnotresolve 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);
A timeout throwsoperation_timeoutand doesnotcancel the job — call the waiter again with the same id rather than resubmitting.
Batches.createBacktestssubmits 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-forwardfollow 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);
Authoring and backtesting a book does not persist it.savewrites it to your account;deploystarts 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
saveanddeployproduce different ids, and the distinction matters.savepersists adraftand setsbook.idto it.deploymints the real paper portfolio and returns its ownportfolioId— deploying creates a portfolio rather than converting the draft into one, so the two ids coexist. Hold on todeployment.portfolioIdfor anything that reads live state;book.idaddresses 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 optionaltransport; 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);
listPortfoliosfilters withincludePaper,includeLive,includeInactive,includeChatPortfolios,search,limit, andpage.includePositionsdefaults off whensearchis set.
A portfolio you create here is always paper, and minting aliveone still happens in the web app. Orders and brokerage status are reachable from here; seeLive trading.
Butdeploycan start live trading.Given the id of a portfolio that is already deployed, it reactivates that portfolio as whatever it already is — soclient.deploy(id)on a paused live portfolio resumes live trading against the connected brokerage, andincludeLive: trueabove will hand you such an id. Checkdeployment.deploymentTypebefore treating a deploy as simulated.
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
connectBrokeragewaits by defaultonly when stdout is a TTY. In CI, cron, orrun_computeit rejects withbrokerage_not_connectedimmediately, 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
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.
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 withCustomIndicator.
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)), ]);
scopeis"global"(one series) or"asset"(one series per ticker, so every point needs aticker). It cannot be changed after creation.
DeclarepointKindwhenever the time semantics are known:observationfor point-in-time samples,period_aggregateplusaggregatePeriod(1d,1w,1mo, or1q) for closed-period values, anddisclosedfor 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.pointsis 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.
Points accepttimestamp,value,ticker,assetType, andavailableAt— camelCase or snake_case, withDateobjects allowed. SetavailableAtwhen 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/waitForCustomIndicatorUploadexpose the three steps directly. CSV, JSON, and JSONL up to 100 MB.
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: createAgent(prompt) Run->>Engine: POST /agents Engine-->>Run: run id loop for await (const event of run) Run->>Engine: GET events (cursor) Engine-->>Run: new events alt event.needsApproval Run-->>You: plan or action awaiting approval You->>Run: approve() or reject() Run->>Engine: POST approval else event.needsInput 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 attachAgent(run.id).
const run = await client.createAgent("Find momentum names in the S&P 500", { idempotencyKey: "momentum-scan-v1", }); for await (const event of run) { console.log(event.text); if (event.needsApproval) await run.approve(); if (event.needsInput) await run.say("Focus on tech"); }
Read-only SQL over the NexusTrade market-data lake, against the server-resolvedlake.catalog. Results are durable Parquet parts rather than an implicitly materialized in-memory array.
flowchart LR A[createLakeQuery] --> B[waitForLakeQuery] B --> C[getLakeQueryManifest] C --> D[downloadLakeQueryPart] D --> E[Stream Parquet within your memory budget]
const query = await client.createLakeQuery( { query: "SELECT ticker, date, closingPrice FROM lake.daily_ohlc WHERE ticker = ?", params: ["AAPL"], limits: { maxRows: 10_000 }, }, { idempotencyKey: "aapl-daily-v1" } ); const finished = await client.waitForLakeQuery(query.id as string); const manifest = await client.getLakeQueryManifest(finished.id as string);
Describe the screen instead of writing the SQL. The server generates it, validates it against the samelake.catalog the engine reads, executes it, and hands back both the rows and the statement.
const screen = await client.createNlScreen( "technology stocks with a market cap over 100 billion and a PE under 30" ); const done = await client.waitForNlScreen(screen.id as string); const result = done.result as Record<string, unknown>; console.log(result.rows); console.log(result.sql); // always check the SQL — it is model-generated
returnQuerydefaults totruebecause 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 onresult.outcome, not on status alone:
This method spends LLM credits. The structuredlakeAPI below does not.
Use the manifest plusdownloadLakeQueryPartto stream results within your own memory budget. NexusTrade picks a compatible backing engine for the referenced tables; your SQL does not change when it does.
The Python SDK additionally shipsnt.lake.sql(...), a DuckDB/pandas convenience layer over these same endpoints.
Every public method onNexusTradeClient. A test in this package fails if one is missing here, so this list cannot drift from the code.
PortfolioHandle— returned by theportfolio(...)builder and bygetPortfolio/listPortfolios.
Create a key atnexustrade.io/developers(Profile → API Keys). Keys start withsk-and are shown once.
const client = new NexusTradeClient({ apiKey: "sk-...", baseUrl: "https://nexustrade.io/api/v1", }); // or set NEXUSTRADE_API_KEY / NEXUSTRADE_API_BASE_URL and: const fromEnv = new NexusTradeClient();
Both variables are also read from a.envfileat or above the current directory, so a local project works with no exports, nodotenvdependency, and no--env-fileflag:
# .env NEXUSTRADE_API_KEY=sk-... NEXUSTRADE_API_BASE_URL=https://nexustrade.io/api/v1
The real environment always wins — a.envvalue is used only when the variable is absent, so a stale file can never override what you exported. Nothing is written back toprocess.env. Opt out withNEXUSTRADE_DISABLE_DOTENV=1.
A key missing the scope gets403 insufficient_scope.
OAuth is not accepted here.NexusTrade's OAuth flow serves the MCP server. These endpoints takesk-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. The key is held in a#privatefield and never appears in a stringified client.
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.
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.



