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 分