NexusTrade Financial MCP

io.github.austin-starks由 GitHub 账号 austin-starks 发布v1.0.1更新于 Oct 26, 2026

Quant research, backtesting, marketplace subscriptions, editable forks, copy trading, and execution.

已验证Streamable HTTP可网页运行金融数据与分析AI 与机器学习

概览

AI 生成的概览

托管式 NexusTrade MCP 服务器,用于量化研究:编写并回测交易策略、运行优化,并管理模拟或实盘投资组合。

可以做什么
  • 研究市场,发现公开策略创作者与市场策略组合
  • 编写、分叉和编辑投资组合与策略,并保存或部署
  • 运行回测、优化与滚动前推研究并轮询结果
  • 以只读 SQL 查询行情数据湖
  • 订阅、分叉或跟单市场策略
  • 提交经纪商订单,管理模拟或实盘部署
适用场景
适合希望让助手起草并回测交易策略、运行优化研究,或在对话中查看和管理 NexusTrade 投资组合与市场策略的场景。
运行要求
远程 Streamable HTTP 端点位于 nexustrade.io,需要 OAuth 登录;仅支持 stdio 的客户端需借助 mcp-remote 桥接。完整工具集(含优化)需要已注册的 NexusTrade 账号与 API 密钥,匿名工作区功能受限。实盘交易需通过浏览器 OAuth 跳转关联经纪商账户。
安装前请注意
相关工具可更改投资组合、计划与经纪商订单,且 deploy 可能让已有的实盘组合恢复实盘交易,部署前应确认部署类型。实盘订单只会暂存等待人工批准,不会直接提交。订阅仅返回结账预览,付款在 NexusTrade 内完成,不通过该工具。回测结果不构成投资建议。

安装

在 SourceWeft 中

  1. 打开 控制台中的 NexusTrade Financial MCP,将其添加到工作区。
  2. 连接 所需的 凭据。它们会被私密存储,绝不会在公开页面上展示。
  3. 为需要使用其工具的对话启用该服务。

Web executable,通过 Streamable HTTP。 远程服务在工作区中配置后即可从网页运行时运行。

交给智能体安装

把这段提示词粘贴到 Claude Code、Codex、Cursor 等智能体,或 SourceWeft 对话中。智能体会读取该服务器的安装指南,先向你展示发布者、端点和登录方式,经你同意后再把它添加到你的客户端。密钥不会写进任何命令。

阅读 https://sourceweft.com/mcp/io.github.austin-starks%2Fnexustrade-mcp/install.md,并按其中说明把这个 MCP 服务器接入我的客户端。修改任何配置之前,先告诉我它的发布者、端点和登录方式,等我确认。不要把 API 密钥写进命令或我会提交的文件;问我密钥应从哪里获取。

查看智能体所依据的安装指南

其他 MCP 客户端

把它添加到你客户端的 mcpServers 配置中。 客户端会在首次连接时完成 OAuth 登录。

{
  "mcpServers": {
    "nexustrade-financial": {
      "type": "http",
      "url": "https://nexustrade.io/api/mcp"
    }
  }
}

README

[NexusTrade]

NexusTrade TypeScript SDK

Author trading strategies in typed TypeScript. Backtest them on the engine that runs them live.

[npm] [Node] [License] [Deps]

Quickstart · Authoring · Polling · Agents · Lake SQL · Auth · Errors


bash
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:

bash
claude mcp add --transport http nexustrade https://nexustrade.io/api/mcp

Cursor and other remote-capable clients use:

json
{  "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:

json
{  "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

ts
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:

ts
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:

ts
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:

ts
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:

ts
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.

Option selection and execution limits

strategy(..., { pendingOrderPolicy: "CancelConflictingBuys" }) enables backtest-only exit priority over conflicting pending buys. Omission preserves waiting. Paper and live deployment reject this opt-in because broker cancellation acknowledgements are not implemented. The setting also accepts explicit "Wait".

Builders describe drafts. Typechecking does not prove that the engine can execute every requested rule; the API validates complete portfolios and their conditions.

  • Expiration selectors support a DTE window and nearest, middle, or furthest. middle selects the middle eligible expiration by index, not the expiration nearest a target such as 180 days. There is no monthly-only or quarterly-only expiration filter.
  • rebalanceOption accepts renewBeforeDays (integer 1–90) and revisitDisclosedTargets. These maintain modeled political-disclosure option targets. The strategy must contain a NewPoliticalDisclosure condition for instrument Option with exactly one member ID. These fields do not implement a general close-and-replace roll for a trend strategy.
  • For backtests, the job's feeConfig.OptionSlippageFraction sets quote-side fills: 0 uses the midpoint, 1 uses ask for buys and bid for sells, and the default is 0.5. This job setting does not add a last-trade fallback or define live order execution.
  • Evaluation timing and order execution are separate. OpenClose schedules evaluations at session open and close; it does not itself carry a signal observed at close into an order at the next open. Explicit zero timing offsets and a gt(MinutesAfterOpen(), Value(0)) condition can restrict evaluations to the calendar session close. MinutesUntilClose counts down to 16:00 Eastern, so it cannot identify an early session close.

When authoring an approximation, describe the actual expiration policy, signal timing, fills, and replacement behavior. Backtest results describe that implementation, not the exact method it approximates.

What you can build — 170+ generated builders
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.

mermaid
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:

ts
{  id: "op_...",  kind: "backtest",          // backtest | optimization | walk_forward  status: "queued",          // queued | running | completed | failed | cancelled  result: {...},             // present only once terminal  error: { code, message, retryable },}
ts
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.

Transient network and HTTP 408/429/5xx polling failures retry the same operation at most twice consecutively, within the wait deadline. Persistent failures keep their original error code and operation ID; no new job is submitted.

Durable lake exports use a 900-second server budget by default (registered API keys may request up to 1800 seconds). waitForLakeQuery defaults to 1200 seconds to include queue time. Compute token budgets remain 90 seconds by default and 180 seconds maximum. A client wait timeout does not extend the server budget.

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:

ts
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.

ts
const book = nt.portfolio("Momentum", [  /* … */]);
await book.save({ idempotencyKey: "momentum-v1" }); // persists; sets book.idconst deployment = await book.deploy(); // starts paper tradingawait 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.

ts
deployment.portfolioId; // the running portfoliodeployment.deploymentType; // paper, unless you deployed an existing live onedeployment.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.

ts
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.

ts
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",});

Prefer one updatePortfolio operation with a finished portfolio patch. It can combine strategies, deployment (frequency and timing), name, policy.stockEligibility, dividendPolicy, and alerts. Omitted fields retain their saved values. Draft starting capital and capabilities can change only before deployment. The existing strategy and rename operations remain supported. Every authored operation for the target in one call commits together; validation failure saves none of them. Reusing the same idempotency key replays the saved result. Deploy, undeploy, delete and automated approval remain separate account operations.

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:

ts
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.

ts
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:

ts
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

ts
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.

ts
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.

ts
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.

ts
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:

ts
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

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:

mermaid
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).
ts
const run = await client.createAgent("Find momentum names in the S&P 500", {  idempotencyKey: "momentum-scan-v1",  costCeilingUsd: 20,});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");}

Lake SQL

Read-only SQL over the NexusTrade market-data lake, against the server-resolved lake.* catalog. Results are durable Parquet parts rather than an implicitly materialized in-memory array.

mermaid
flowchart LR    A[createLakeQuery] --> B[waitForLakeQuery]    B --> C[getLakeQueryManifest]    C --> D[downloadLakeQueryPart]    D --> E[Stream Parquet within your memory budget]
ts
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);

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.

ts
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

returnQuery 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 result.outcome, not on status alone:

outcomeMeaning
ROWSMatches found
EMPTYEvery filter ran and nothing cleared them all — an answer
CLARIFICATIONThe question was ambiguous; result.clarification asks
GENERATION_FAILEDThe retry budget was spent — the only case worth retrying

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

Use the manifest plus downloadLakeQueryPart to 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 ships nt.lake.sql(...), a DuckDB/pandas convenience layer over these same endpoints.

Managed watchlist universes

nt.updateWatchlist({watchlistKey, memberId, instrument, amountBasis, chamber, output}) maintains a visible managed list from PoliticalRemainingPurchases. Its strategy has no condition and places no orders. nt.watchlistUniverse(watchlistKey) selects that producer's committed membership for a separate DynamicRebalance in the same portfolio. Each key has one producer and may have several consumers. Other action types cannot consume this universe.

ts
const producer = nt.updateWatchlist({  watchlistKey: "pelosi-holdings", memberId: "P000197", instrument: "Equity",  amountBasis: "Midpoint", chamber: "All",  output: {name: "Pelosi disclosed holdings", purpose: "Follow public holdings",    alertSettings: {onChange: true, inApp: true, email: true, briefCadence: "weekly"}},});const consumerUniverse = nt.watchlistUniverse("pelosi-holdings");

output supplies initial name, purpose and alerts. Subsequent user renames and settings remain authoritative. Live and paper consumers wait for durable list publication; backtests keep membership local to the run and never edit the user's visible list. Discovery and data loads remain full, so this does not establish a performance saving. Omitted amount basis uses LowerBound.

updateInstitutionalWatchlist follows an exact manager's latest publicly available complete equity book. updateInsiderWatchlist selects qualifying positive purchases by first public availability, with optional owner, issuer, and role scope. Both use the same publication and consumer contract:

ts
const manager = nt.updateInstitutionalWatchlist({  watchlistKey: "berkshire-equities", managerCik: "1067983",});const insider = nt.updateInsiderWatchlist({  watchlistKey: "insider-purchases", windowDays: 90, role: "OfficerOrDirector",});const screen = nt.updateScreenerWatchlist({  watchlistKey: "berkshire-top-20", refreshMinutes: 1440,  columns: [nt.screenColumn("price", nt.screenPrice()),    nt.screenColumn("holding", nt.screenDisclosure(manager.source))],  filter: nt.screenAll(nt.screenCompare("price", "Gte", 5),    nt.screenCompare("holding", "Gt", 0)),  selection: nt.screenTop("holding", 20),});const screenUniverse = nt.watchlistUniverse("berkshire-top-20");

The screen evaluates on its scheduled cadence and reuses accepted membership between refreshes. Missing facts remain unknown. screenAny composes OR rules; screenPercentile ranks the filtered candidates with known rank values. screenBarVolume and screenBarDollarVolume require a completed historical bar; live quotes do not supply those metrics in this release. Price and disclosure metrics support live screens. Failed coverage or publication blocks new allocation while consumers retain their exit behavior. Screen execution uses in-memory DuckDB; no local database volume or LLM call is required for refreshes.

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

MethodPurpose
listBrokerages()Every connectable brokerage and whether it is linked
getBrokerage(brokerage)Whether one brokerage is linked
connectBrokerage(brokerage, { wait })Log the connect URL and wait for the link
createOrders(portfolioId, orders, { idempotencyKey })Stage orders; live ones need approval
listOrders(options)List owned orders by portfolio/status, with pagination
cancelOrders(orderIds, { idempotencyKey, portfolioId })Cancel explicit IDs with a result for each

Portfolios

MethodPurpose
createPortfolio(book, { idempotencyKey })Persist a portfolio definition
listPortfolios(options)List portfolios, with filters and pagination
getPortfolio(portfolioId)Read one portfolio
updatePortfolio(id, operations, { idempotencyKey })Atomically save a finished authored portfolio patch
forkPublicPortfolio(sharedId, { idempotencyKey })Fork a public portfolio into the workspace
deploy(portfolioId, { frequency })Start paper trading it
undeploy(portfolioId)Stop it

Backtests

MethodPurpose
createBacktest(handle, { idempotencyKey })Submit one backtest
createBacktests(handles, { idempotencyKey })Submit many in one request
getBacktest(backtestId)Read the operation
waitForBacktest(backtestId, options)Block until terminal
waitForBacktests(operations, options)Block on a whole batch

Optimization and walk-forward

MethodPurpose
createOptimization(handle, { idempotencyKey })Submit an optimization
getOptimization(optimizationId)Read the operation
waitForOptimization(optimizationId, options)Block until terminal
createSystematicSweep(handle, { idempotencyKey })Submit an explicit-gene sweep
getSystematicSweep(optimizationId)Read the sweep operation
waitForSystematicSweep(optimizationId, options)Block until terminal
createWalkForward(handle, { idempotencyKey })Submit a walk-forward study
getWalkForward(studyId)Read the operation
waitForWalkForward(studyId, options)Block until terminal

Custom data sources

MethodPurpose
createCustomIndicator(spec, { idempotencyKey })Create a series, optionally seeded
listCustomIndicators(options)List owned series
getCustomIndicator(id)Read one, with its point count and range
appendCustomIndicatorPoints(id, points, { idempotencyKey })Add points
replaceCustomIndicatorPoints(id, points, { idempotencyKey, allowShrink })Replace the complete series while retaining its id
archiveCustomIndicator(id, { confirm })Soft-archive a series
restoreCustomIndicator(id)Restore an archived series
createCustomIndicatorUpload(id, options)Open an upload slot (CSV/JSON/JSONL)
completeCustomIndicatorUpload(id, jobId)Start validating uploaded bytes
getCustomIndicatorUpload(id, jobId)Read the upload operation
waitForCustomIndicatorUpload(id, jobId, options)Block until validated

Agent runs

MethodPurpose
createAgent(prompt, { idempotencyKey })Start a run
getAgent(agentId)Read its status
attachAgent(agentId, { cursor })Reattach to a run already in flight

Lake SQL

MethodPurpose
createLakeQuery(request, { idempotencyKey })Submit read-only SQL
getLakeQuery(queryId)Read the operation
waitForLakeQuery(queryId, options)Block until terminal
cancelLakeQuery(queryId)Cancel an owned query
createLakeAsk(question)Ask the lake in plain language
getLakeAsk(askId)Read the operation
waitForLakeAsk(askId, options)Block until terminal
cancelLakeAsk(askId)Cancel an owned ask
getLakeQueryManifest(queryId)Schema, checksums, and part metadata
downloadLakeQueryPart(queryId, part, options)Download one Parquet part
getLakeCatalog()List queryable tables
describeLakeTable(table)Columns and types for one table

Natural language

MethodPurpose
createNlScreen(question, { returnQuery })Screen stocks from a plain-language question
getNlScreen(screenId)Read the operation
waitForNlScreen(screenId, options)Block until terminal
cancelNlScreen(screenId)Cancel an owned screen

Client construction

MethodPurpose
new NexusTradeClient({ apiKey, baseUrl })Explicit credentials
new NexusTradeClient()Lazy anonymous workspace with strict limits
NexusTradeClient.fromEnvironment()Read them from the environment or .env
exportWorkspaceSession()Export an anonymous workspace for later use
importWorkspaceSession(token)Resume an existing anonymous workspace

PortfolioHandle — returned by the portfolio(...) builder and by getPortfolio / listPortfolios.

MethodPurpose
save({ idempotencyKey })Persist it as a draft, setting .id
backtest({ startDate, endDate, idempotencyKey })Backtest it, preferring the saved id
deploy({ frequency })Mint the real paper portfolio (new id)
undeploy()Deactivate its deployment
setStockEligibility(eligibility)Set the stock eligibility it will send

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:

ts
const guest = new NexusTradeClient();await guest.listPortfolios();const token = guest.exportWorkspaceSession();
const resumed = new NexusTradeClient({ workspaceSession: 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.

ts
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 .env file at or above the current directory, so a local project works with no exports, no dotenv dependency, and no --env-file flag:

bash
# .envNEXUSTRADE_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 process.env. Opt out with NEXUSTRADE_DISABLE_DOTENV=1.

ScopeGrants
readPortfolio, backtest, genetic, sweep, and walk-forward reads
writePortfolio create/edit/fork, backtests, genetic/sweep, and walk-forward launches
lakeLake catalog, query lifecycle, manifests, result parts

A key missing the scope gets 403 insufficient_scope.

OAuth is not accepted here. NexusTrade's OAuth flow serves the MCP server. These endpoints take sk- API keys only; a bearer JWT is rejected with 401 invalid_token.

Transport hardening. HTTPS is required (except loopback). The client refuses cross-origin redirects, so the credential cannot be replayed to another host, and refuses to follow a redirect on any non-GET request, so a redirect can never re-submit a paid job. The key is held in a #private field and never appears in a stringified client.

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.

ts
await client.createBacktest(handle, { idempotencyKey: "momentum-2024-v1" });

Errors

ts
import { NexusTradeApiError } from "nexustrade";
try {  await client.createBacktest(handle, { idempotencyKey: "run-1" });} catch (error) {  if (    error instanceof NexusTradeApiError &&    error.code === "rate_limit_exceeded"  ) {    // back off  }  throw error;}
StatusCodeMeaning
401invalid_tokenMissing, malformed, or expired key (or an OAuth JWT)
403insufficient_scopeKey lacks read, write, or lake
400invalid_request, invalid_portfolioMalformed input
400invalid_idempotency_keyMust match [A-Za-z0-9._:-]{1,160}
409idempotency_conflictKey reused with a different payload
409idempotency_in_progressSame key, first call still running. Re-poll, do not resubmit
404not_found, operation_not_foundUnknown or not yours
429rate_limit_exceededBack 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

new HttpTransport({ timeoutSeconds }) (default 30) is a total wall-clock deadline for one request. 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.

Requirements

Node 18+ (uses the global fetch). Contributing: the test suite runs TypeScript directly via node --test, which needs Node 22.6+ for type stripping. The published dist/ is plain JavaScript and has no such requirement.

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.

License

MIT

Compositional trading controls (2.1)

Account rules compose the existing PortfolioValue, arithmetic, IndicatorWindowAgo, IndicatorAtCondition and ordinary conditions. A conditional snapshot can retain any numeric operand; it has no built-in gain thresholds or release policy. Its selection is First, Latest, Maximum or Minimum. An initial value is only a fallback before a capture (or after expiry), not part of the extrema. For a second crossing within an hour, capture with First and use andThen(60, "Minute", crossing(), crossing()) as the condition, where crossing() builds gt(CrossAbove(Price(asset), Value(level)), Value(0)). Staying above the level is not another crossing. The first capture remains fixed on later crossings unless an expiry was configured. PositionEntryPrice, PositionPeakPrice and PositionMinutesSinceHigh are narrow position measurements. Reuse MinutesSinceOrder for purchase cadence. Settlement accounting remains on the portfolio.

short (Python short) sells one stock to open a short of exactly the sized shares; it defaults to "number of assets" and cannot be sized as a percent of current positions. cover buys the short back; its default is 100 percent of the open short, and every size is capped at the short, so it never goes long. A Short is refused while the book is long that stock, is capped by Reg-T initial margin, and is always whole shares. Backtests and paper books always support them; a live book needs a brokerage that can short stock (Alpaca, Tradier, TradeStation or Public, on a margin account).

sellHoldings (Python sell_holdings) is a Sell action with a candidate-bound holding condition. Its strategy condition supplies the account/market gate; only actual qualifying stock holdings are sold. Ordinary fixed-asset Sell actions retain their existing behavior.

A Buy action can configure funding: candidate condition, rankBy indicator, direction (Highest/Lowest), and source sale amount expressed as a percentage of current positions. The strategy owns its pending workflow and every order. It waits for actual fills and settlement and rechecks its current condition; proposed proceeds never become cash. Symbol order breaks equal ranking scores.

Buy/Sell executionPriority is an integer: lower numbers execute first. Funding sales may supply their own numeric priority, otherwise they inherit the purchase priority. Untagged trading events keep FIFO order. Broker submission ordering is scoped per portfolio; one account cannot delay another account's orders through its priority settings. Already submitted broker orders cannot be reordered. pendingOrderPolicy: "WaitForTarget" checks orders for the actual target asset without cancellation; the default waits for portfolio orders. Priority never changes a safety gate or chooses which holdings are weak.

The portfolio retains cashSettlement: "T1" and engine-owned actual-fill settlement accounting. Configure it before deployment; edits to this setting are restricted to undeployed drafts. Current integration supports stock/ETF Buy/Sell actions, alerts and watchlist producers. Other asset/executor paths remain explicitly rejected with T1 until integrated.

Public cash-only requests use useMargin: false. Live admission additionally uses Public's cash-only withdrawal balance as a conservative cap, which can be stricter than settled trading cash. Missing cash/calendar evidence blocks buys. Other brokers need a live T1 evidence adapter. Existing owner approval and trade limits apply. Native strategy/indicator runtime uses the existing checkpoint path; its verification does not establish live broker execution.

ConsecutiveTrue measures elapsed time while its ordinary condition stays true. CountTrue counts matching intervals inside a time lookback. For example, a 20 TradingDay lookback with countInterval: {length: 5, interval: "Minute"} counts at most one match per five-minute interval across twenty exchange sessions. Omitting countInterval means one unit of the lookback interval. Comparisons and visit/crossing events belong in the ordinary condition, including Then. There are no sampled-comparison, distinct, cadence-count or clock modes.

PortfolioValue keeps its existing cash-plus-positions semantics during warmup and trading. Backtesting and paper trading do not depend on the selected pricing broker. Live capability checks reject T1 when the adapter lacks verified settled cash or cash-only submission support. Broker facts remain in the existing account reconciliation path. Porting a T1 strategy preserves its settlement policy.

For conditional capture, expiry runs from the retained observation's capture time. Minimum/Maximum with expiry is not a sliding-window minimum/maximum. IndicatorMinimum and IndicatorMaximum provide rolling extrema of one numeric operand. CurrentValue(operand) explicitly binds today's reference inside a historical CountTrue condition; historical operands retain their observed values. Outside that query it returns its operand. Bind the complete stateful operand, rather than inserting a reference inside its recurrence.

TradingDay windows use observed exchange sessions, including holidays and early closes. Elapsed Day/Hour/Minute windows keep their calendar meaning. IndicatorWindowAgo with TradingDay reads the confirmed value at the end of the reference session. Missing session evidence returns no value. CurrentTimestamp returns the evaluation time in Unix seconds, for ordinary candle-age comparisons.

来源:README.md,提交 54c040a

工具

0
该服务器需要登录,因此 SourceWeft 无法在没有账号的情况下读取其工具,也未收录。已连接账号所能看到的内容可能因账号及其权限而异。

版本历史

1
  1. v1.0.1最新Sep 16, 2026

评分

已满足适用检查项的 85%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通必需25 分
  • 已声明认证方式必需10 分
  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分