All documentation

Broker Integration Report

Status as of this writing. Covers every broker connector implemented against the shared TradingConnector architecture (see docs/ARCHITECTURE.md for how the abstraction itself works).

Beta Launch verification (Phase 2 of the production readiness pass)

The Beta Launch checklist asked to "verify every broker integration" — login, account retrieval, balance, equity, positions, pending orders, market orders, close order, modify SL/TP, reconnection, error recovery, copy latency — for all 6 supported brokers (MT5, TradeLocker, Tradovate, DXtrade, cTrader, Match-Trader; Rithmic is architecturally complete but excluded here since its wire transport isn't connected to a live gateway yet — see docs/RITHMIC_TRANSPORT.md).

How this was actually verified, stated plainly: this environment has no broker demo/live credentials and no network path to a real broker server — the "Live-tested vs. simulated functionality" section below already establishes that as true for every connector in this codebase, before this pass and independent of it. What follows is code-level and automated-test verification, run fresh for this pass, not a claim of live testing. Run docs/LIVE_TEST_CHECKLIST.md's peer, this report's own "Validation checklist" section below, against a real demo account for each broker before routing real money through it — that step cannot be substituted by anything done in this environment.

Capability coverage matrix — ✅ real, dedicated test coverage exists (unit + connector-level, against each broker's fake HTTP/session layer); every cell below was re-verified by re-running that broker's full test file set immediately before this table was written, not carried over from memory:

CapabilityMT5TradeLockerTradovateDXtradecTraderMatch-Trader
Login / authentication✅✅✅✅✅✅
Account retrieval✅✅✅⚠️ best-effort (see Known Limitations)✅⚠️ best-effort SYSTEM_UUID (see Known Limitations)
Balance✅✅✅✅✅✅
Equity✅✅✅✅⚠️ equals balance (see Known Limitations — no equity field in the trimmed schema)✅
Positions✅✅✅✅✅✅
Pending orders✅✅✅✅✅✅
Market orders✅✅✅✅✅✅
Close order✅✅✅✅✅✅
Modify SL/TP✅✅✅⚠️ price only (see Known Limitations)✅✅ positions directly; pending orders via cancel+recreate
Reconnection✅✅✅✅✅✅
Error recovery✅✅✅✅✅✅
Copy latency (instrumented)✅✅✅✅✅✅

"Copy latency" here means: every connector records connector_order_latency_ms/connector_position_sync_latency_ms (and connector_market_data_latency_ms where price streaming exists) via the shared withLatency() helper, visible at GET /api/health and GET /api/health/market-data — the instrumentation is real and verified present in each connector's source. The actual *numbers* it would report against a live broker cannot be produced in this environment (no live connection to time); synthetic, local, in-process Copy Engine throughput numbers are covered separately in docs/STRESS_TEST_REPORT.md, clearly labeled as not the same measurement.

Test evidence (re-run immediately before writing this table, one combined vitest run invocation, all passing):

BrokerTest filesTests
MT5131
TradeLocker669
Tradovate687
DXtrade557
cTrader660
Match-Trader554
Rithmic (architecture, FakeRithmicTransport)8116
Total37474

Every ⚠️ above is a pre-existing, already-documented limitation (see "Known limitations" below for each broker) surfaced honestly rather than silently — none of them are new findings from this pass, and none block the corresponding capability from working, only from being fully schema-confirmed pending a live test.

Supported brokers

PlatformAsset classConnector moduleStatus
MetaTrader 5 (MT5)Forexsrc/lib/connectors/mt5Implemented
TradeLockerForexsrc/lib/connectors/tradelockerImplemented
TradovateFuturessrc/lib/connectors/tradovateImplemented
RithmicFuturessrc/lib/connectors/rithmicImplemented — architecture & business logic complete; wire transport not yet connected to a live gateway (see docs/RITHMIC_TRANSPORT.md)
DXtradeForexsrc/lib/connectors/dxtradeImplemented — see the DXtrade-specific note under "Live-tested vs. simulated functionality" below; confidence is a step below TradeLocker/Tradovate's because DXtrade's public docs don't publish full response schemas
cTraderForexsrc/lib/connectors/ctraderImplemented — a real Protocol Buffers/TLS implementation, not a stub or an unimplemented transport port (see the cTrader-specific note below); the only connector with genuine server-push execution events
Match-TraderForexsrc/lib/connectors/match-traderImplemented — REST-polling, the same architecture as TradeLocker/DXtrade; confidence is comparable to DXtrade's (endpoints/methods and most response fields confirmed against Match-Trade Technologies' own docs, a few request bodies inferred by symmetry — see the Match-Trader-specific note below)

Every other platform in src/lib/connectors/registry/platforms.ts (MT4, CQG, NinjaTrader, Interactive Brokers, Binance, Bybit, Coinbase, Kraken, OKX, Hyperliquid, Bitget, MEXC, KuCoin) is known to the registry and selectable in principle, but has no connector implementation yet — platformRegistry.create() returns a StubConnector for it, whose every action-taking method throws ConnectorNotImplementedError.

Supported features per broker

Legend: ✅ implemented · ➖ not applicable to this platform · ⚠️ implemented with a stated limitation (see Known Limitations)

FeatureMT5TradeLockerTradovateRithmicDXtradecTraderMatch-Trader
Authentication✅✅✅✅ (against FakeRithmicTransport)✅✅ (app auth + account auth, OAuth accessToken — see Known Limitations)✅
Account discovery✅✅✅✅⚠️ best-effort (see Known Limitations)✅ (confirmed real endpoint)⚠️ best-effort SYSTEM_UUID resolution (see Known Limitations)
Live positions (streaming)✅ push✅ polled✅ polled✅ push (+ reconciliation poll safety net)✅ polled✅ push (+ reconciliation poll safety net)✅ polled
Live price streaming➖ (not required by MT5's milestone)✅ polled quotes✅ real WebSocket✅ push (Ticker Plant)➖ (no confirmed schema — see Known Limitations)➖ (no confirmed per-symbol price scale — see Known Limitations)➖ (WebSocket market-watch exists per the docs but its schema isn't confirmed — see Known Limitations)
Market orders✅✅✅✅✅✅✅
Limit orders✅✅✅✅✅✅✅
Stop orders✅✅✅✅✅✅✅
Stop-limit orders➖ (rejected — not modeled for this platform)➖ (rejected)➖ (rejected)✅ (the only connector supporting this order type — see sdk/types.ts's PlaceOrderRequest)➖ (rejected)➖ (rejected)➖ (rejected)
Modify orders (price/SL/TP)✅✅✅✅✅ price only (see Known Limitations)✅✅ positions directly; pending orders via cancel+recreate (see Known Limitations)
Cancel pending orders✅✅✅✅✅✅✅
Close positions✅✅✅✅✅ (via an opposite CLOSE-effect order — dxSCA has no dedicated close endpoint)✅ (dedicated ProtoOAClosePositionReq)✅ (dedicated positions/close endpoint)
Copy pending orders (open/modify/close lifecycle)✅ push✅ polled⚠️ open/close only, own orders only✅ push✅ polled✅ push✅ polled
Partial close synchronization✅✅✅✅✅✅✅
Break-even / trailing-stop sync✅ (generic SL/TP modify path)✅ (generic SL/TP modify path)➖ (Tradovate positions carry no SL/TP field at all)➖ (Rithmic positions carry no SL/TP field either — same reasoning as Tradovate)✅ (generic SL/TP modify path)✅ (generic SL/TP modify path)✅ (generic SL/TP modify path)
Automatic reconnect✅✅✅✅✅✅✅
Retry logic (network-level)✅ (MetaApi SDK's own)✅ (fetchJson backoff)✅ (fetchJson backoff)✅ (ReconnectScheduler, same shared primitive)✅ (fetchJson backoff)✅ (ReconnectScheduler + a fresh TLS socket)✅ (fetchJson backoff)
Health check✅✅✅✅✅✅✅

"Break-even / trailing-stop sync" is not a distinct code path on any connector — see docs/ARCHITECTURE.md's Copy Engine section: any SL/TP change on the master, however it was triggered (manually, by a break-even rule, by a trailing stop), arrives as an ordinary modify event and is mirrored generically. Tradovate and Rithmic are marked ➖ here specifically because their positions have no SL/TP field to change in the first place (protective orders are separate order objects neither platform links back to a position the way MT5/TradeLocker do).

Live-tested vs. simulated functionality

No connector in this codebase has been exercised against a live or demo broker account during development. MT5/TradeLocker/Tradovate were built and verified by one of the following methods, and this distinction matters for how much to trust each claim above:

  • MT5 — built against metaapi.cloud-sdk's own installed TypeScript

type definitions (node_modules/metaapi.cloud-sdk/lib/**), a real, versioned third-party SDK. Every field/method name used is confirmed against that SDK's own source, not guessed.

  • TradeLocker — every endpoint, request body, and response envelope

was verified against TradeLocker's own official Python client (github.com/TradeLocker/tradelocker-python) source code, which makes the exact same HTTP calls a Node/browser client would.

  • Tradovate — every REST endpoint and the WebSocket wire protocol were

verified against Tradovate's own official reference client (github.com/tradovate/example-api-js) and the Tradovate Partner API documentation (partner.tradovate.com).

  • Rithmic — a materially different, more explicit case, stated plainly:

Rithmic's real API (R|Protocol — Protocol Buffers over WebSocket) has no installable SDK and no publicly-verifiable .proto schema this project has access to (see docs/RITHMIC_TRANSPORT.md for the full reasoning). Per explicit instruction, this codebase does not reverse-engineer or guess that wire format. Every business-level concept (account/balance/ position/order/fill shapes, all four order types, reconnect/heartbeat/ retry logic, Copy Engine integration) is real, complete, and tested — but tested exclusively against FakeRithmicTransport (tests/helpers/rithmic-fakes.ts), never a live or even a simulated- -at-the-wire-level Rithmic gateway. The transport that would actually reach Rithmic, RithmicUnimplementedTransport, throws a clear, documented error on every call by design (see that file). This is a different, more conservative claim than MT5/TradeLocker/Tradovate's "verified against real source, not live-tested" — for Rithmic, the wire protocol itself is entirely unimplemented, not just unverified live. Run docs/LIVE_TEST_CHECKLIST.md only once a real RithmicTransport exists per docs/RITHMIC_TRANSPORT.md.

What this means concretely for MT5/TradeLocker/Tradovate: connection, authentication, and read operations (accounts/balance/positions) are the highest-confidence paths — they mirror real, working example code closely. Write operations (place/modify/cancel/close) and the exact response envelope shapes for those calls are verified against documented request formats but a handful of response-field assumptions (flagged individually in each connector's own source comments — search for "best-effort" or "not fully confirmed" in src/lib/connectors/*/sdk-adapter.ts and normalize.ts) have not been confirmed against an actual server response. Before routing real money through any of these connectors, run the Validation Checklist below against a live demo account for that broker (for Rithmic, docs/LIVE_TEST_CHECKLIST.md instead, and only after docs/RITHMIC_TRANSPORT.md's transport swap-in).

Rithmic — live-tested items: none. Zero calls have been made against a real Rithmic gateway of any kind.

DXtrade — a third distinct case, between TradeLocker/Tradovate's and Rithmic's: DXtrade (Devexperts dxSCA REST API) is single-tenant per broker (every deployment runs its own domain — there is no one canonical demo/live host or one canonical "the docs" the way MT5/ TradeLocker/Tradovate each have). What's confirmed here comes from TWO independent real brokers' own public dxSCA API reference sites (Sway Charts and Velotrade — both running the same Devexperts-built docs, which is what makes them trustworthy as "the real dxSCA schema" rather than one broker's customization): every endpoint path, HTTP method, the authentication flow (POST /login → Authorization: DXAPI <token> on every other call), and the full Order Response shape (including its legs[] array) are confirmed directly, quoted, not guessed. What is not confirmed: neither site publishes a field-level JSON schema for the Position or Account Metrics response bodies, or for the exact value If-Match expects, or for account-discovery's response shape — see src/lib/connectors/dxtrade/raw-types.ts's file doc for the complete, itemized breakdown and sdk-adapter.ts's defensive multi-candidate-key parsing this drives. Tested exclusively against FakeDXtradeApi (tests/helpers/dxtrade-fakes.ts), never a live DXtrade gateway.

cTrader — the highest-confidence case after MT5/TradeLocker/Tradovate, for a different reason: cTrader's Open API is Spotware's own public, canonical Protocol Buffers schema — not a broker's own docs summary or a third-party reference client, but the literal source of truth every real cTrader client (including Spotware's own) is generated from: github.com/spotware/openapi-proto-messages. Every field name AND numeric wire value used in src/lib/connectors/ctrader/proto-schema.ts and raw-types.ts's PAYLOAD_TYPE map was copied verbatim from that repository's OpenApiMessages.proto, OpenApiModelMessages.proto, OpenApiCommonMessages.proto, and OpenApiCommonModelMessages.proto — including all 89 ProtoOAPayloadType enum values, cross-checked to confirm order placement/amend/cancel/close have NO dedicated response message (their outcome arrives exclusively via ProtoOAExecutionEvent, which is why every order operation in this connector is fire-and- forget). The TLS host/port and length-prefix framing were confirmed via cTrader's own Help Centre and cross-checked against two independent brokers' real integration guides. One real, protobufjs-level bug was caught and fixed during development via direct encode/decode smoke testing before any connector code was built on top of it — see tests/ctrader-proto-schema.test.ts and wire-transport.ts's encodeMessage() doc comment. What is NOT live-tested: the actual TLS connection to demo.ctraderapi.com/live.ctraderapi.com, since no cTrader Open API application (clientId/clientSecret) or OAuth access token is available in this environment — tested exclusively against FakeCTraderApi/FakeCTraderSession (tests/helpers/ctrader-fakes.ts).

Match-Trader — comparable confidence to DXtrade, for the same reason: every endpoint path, HTTP method, and the fields called out as CONFIRMED in src/lib/connectors/match-trader/raw-types.ts's file doc were verified against Match-Trade Technologies' own official documentation portal (docs.match-trade.com, the Theneo-hosted API reference) — including the full confirmed field lists for the login response, balance, opened-positions, active-orders, and the open/edit-position and create-pending-order request bodies. What's best-effort instead (flagged individually, same standard as DXtrade's own best-effort pieces): the exact positions/close and pending-order/cancel request bodies (inferred by symmetry with the confirmed position/edit shape), whether the Auth-trading-api header value is literally the same token as the co-auth cookie, and which field in the login response actually holds SYSTEM_UUID. Tested exclusively against FakeMatchTraderApi/FakeMatchTraderSession (tests/helpers/match-trader-fakes.ts), never a live Match-Trader gateway.

Rithmic — mock-tested items (all 98 connector/service tests + 18 Copy Engine validation tests, against FakeRithmicTransport): login/ logout/heartbeat, account/balance/position/order queries, all 4 order types' place/modify/cancel/close, push-driven position/order event diffing, the reconciliation safety-net poll, live quote subscription, automatic reconnect (including backoff-step retry and heartbeat- triggered recovery), health checks, and the full Copy Engine pipeline (one-master-many-followers, many-masters-many-followers, all 4 lot-sizing modes, reverse copy, symbol mapping, pending orders, partial close, SL/TP modify, emergency stop, pause/resume, duplicate prevention, queue processing, reconnect-recovery reconciliation).

Known limitations

MT5

  • Requires MetaApi.cloud as an intermediary (a paid third-party service);

MT5 has no first-party REST/WebSocket API of its own to connect to directly.

  • DEAL_ENTRY_INOUT (a close-and-reverse in a single MT5 deal) is not

modeled distinctly — dropped rather than misreported as a plain open or close (see mt5/normalize.ts).

TradeLocker

  • Has no push/streaming capability at all — "live" positions, orders, and

balance are all periodic polling (default 5s), and "live" prices are polling too (default 1s). Effective real-time-ness is bounded by these intervals, not sub-second.

  • Resolving a position's stop-loss/take-profit requires cross-referencing

the orders snapshot from the same poll cycle by stopLossId/ takeProfitId; which order field holds the actual trigger price (price vs stopPrice) is a documented best-effort guess, not a confirmed fact.

  • The exact response envelope for /order/modifyorder and `/order/

cancelorder was inferred by symmetry with /order/placeorder's documented {orderId, failureReason, failureText}`, not directly confirmed.

Tradovate

  • Same "no push for positions/orders/balance" situation as TradeLocker —

polling-based, default 5s.

  • /order/list (Tradovate's own REST schema) does not expose an order's

quantity, price, or type — only its status. Pending-order tracking is therefore only fully detailed for orders THIS connector itself placed (cached locally at placeOrder() time); an order placed by some other means still produces open/close events, just without volume/price until Tradovate's fuller order-detail endpoints are integrated.

  • Live price streaming requires a runtime global WebSocket (available in

browsers and Node 22+); the connector throws a clear, typed error rather than a cryptic ReferenceError if it's unavailable in the deployment runtime.

  • The WebSocket quote entry key names (entries.Bid, entries.Offer)

are a best-effort reading of Tradovate's market-data protocol docs, not directly confirmed against a live frame.

  • Requires a Tradovate-issued API application (cid/sec), obtained

separately from a trader's own login — not every Tradovate account has one by default.

Rithmic

  • The wire transport is not connected to a live gateway. See

docs/RITHMIC_TRANSPORT.md for the full explanation, what's required to change that, and the exact (single-file) swap-in steps. Everything above the transport — the connector, all four services, reconnect/ heartbeat/retry, Copy Engine integration, UI — is complete and tested against FakeRithmicTransport regardless.

  • Exchange routing is hardcoded to CME (RithmicOrderService's

DEFAULT_EXCHANGE) — this codebase has no symbol→exchange mapping data. Trading products on other exchanges (ICE, CBOT as a distinct routing target, etc.) needs that resolved properly once a real transport exists.

  • Like Tradovate, Rithmic positions carry no stop-loss/take-profit field

— protective orders are separate order objects, not linked back to a position. SL/TP is always reported null on a position snapshot, a stated simplification, not a guess.

  • Pending-order reconciliation (the periodic safety-net poll, as opposed

to the primary push path) only adds newly-discovered working orders — it deliberately does not synthesize a close event for an order that vanishes from a snapshot without ever arriving via the push channel (onOrderUpdate), which is treated as the authoritative source for order closes. A documented asymmetry with position reconciliation (which IS fully reconciled), not an oversight — see RithmicPositionService's own doc comment.

  • Credential field names (systemName, appName/appVersion, fcmId/

ibId) are drawn from Rithmic's own public developer materials, not from a live login flow — confirm they match exactly once real developer documentation is available (see docs/RITHMIC_TRANSPORT.md).

DXtrade

  • No confirmed push/WebSocket wire format. Devexperts document that

a DXtrade Push API exists in principle, but neither public reference source this connector was verified against (see the "Live-tested vs. simulated" section above) publishes its message schema — so, like TradeLocker, this connector is REST-polling only (deps.pollIntervalMs, default 5s). Unlike TradeLocker, live price streaming (streamPrice()) isn't implemented at all here — the market-data REST endpoint's request/response fields aren't confirmed either, and this codebase doesn't guess wire formats it can't verify (the same standard Rithmic's transport is held to).

  • Position and Account Metrics field names are best-effort, not

confirmed — see raw-types.ts's file doc for exactly which fields. sdk-adapter.ts parses both defensively (several plausible key names tried in order) rather than assuming one, but a real DXtrade server using different field names than guessed here would still misreport a value silently rather than crash. Confirm field names for real before trusting balance/equity/position figures with live money.

  • Account discovery (`GET /users/{username}`) is best-effort. The

endpoint's existence is confirmed; its response including a nested account list is not. credentials.accountId bypasses this endpoint entirely and is the reliable path — the Add Broker wizard doesn't currently expose that field, so today every DXtrade connection uses discovery and depends on it working.

  • `If-Match`'s exact expected value isn't documented beyond "an

ETag from a previous response" — this connector uses the Order Response's own version field (a confirmed real field) as that value, a reasonable but unconfirmed interpretation. If a real DXtrade server rejects this, modify/cancel calls will surface a 409/412 translated to ConnectorConnectionError rather than silently corrupting state.

  • `modifyOrder` only changes price — dxSCA's Order Response has no

confirmed SL/TP fields at the order level (protective levels appear to live on the position, matching getPositions()'s SL/TP fields), so ModifyOrderRequest.stopLoss/.takeProfit are currently no-ops for DXtrade specifically; every other connector honors them.

  • No dedicated "close position" endpoint exists in dxSCA — closing

is implemented as placing a new, opposite-side order with positionEffect: 'CLOSE' against the position's own code, the documented dxSCA convention (confirmed: positionEffect/ positionCode are real fields on the Order Response's legs[]).

cTrader

  • No OAuth authorization flow implemented. cTrader's Open API does

not accept a trader's raw username/password at all (unlike every other connector in this codebase) — it requires an access token obtained via cTrader's own browser-based OAuth2 flow. This connector accepts a pre-obtained accessToken as a credential (entered directly in the Add Broker wizard) rather than performing that redirect flow itself, a materially larger, separate feature outside this milestone's scope — see raw-types.ts's file doc.

  • No live price streaming. ProtoOASpotEvent is confirmed to

exist, but correctly de-scaling its bid/ask requires each symbol's digits field from the FULL ProtoOASymbol (PROTO_OA_SYMBOL_BY_ID_REQ), not the lighter ProtoOALightSymbol this connector fetches for id↔name lookup — rather than guess a scale factor and silently misreport every price, this is a stated limitation, not implemented.

  • Equity always equals balance. ProtoOATrader carries no equity

field in the trimmed schema; computing real equity (balance + unrealized P&L across every open position) would need an extra aggregation this milestone didn't build.

  • **clientMsgId echo-back is a documented assumption, not a confirmed

fact.** Request/response correlation (application auth, account auth, account list, trader info, reconcile) relies on the server echoing back the clientMsgId this connector sets — the field's evident purpose per ProtoMessage's own doc ("the request message ID assigned by the client"), but not literally confirmed by example. If wrong, affected calls fail safe: a clear timeout error after 15s, not a hang or silent corruption.

  • `If-Match`-equivalent concurrency does not apply here — unlike

DXtrade, cTrader's order operations take no such header; this note is intentionally absent for cTrader.

  • Volume's exact real-world unit conversion is partially confirmed.

Spotware's own docs state volume is "in 0.01 of a unit" (confirmed, applied in normalize.ts), but the further "1.0 lot = how many base units" mapping is per-symbol (a lotSize/contract-size field on the full ProtoOASymbol, not fetched here) — flagged, not guessed.

  • Credentials required: clientId/clientSecret (a registered

Open API application from openapi.ctrader.com — separately obtained, not the trader's own login, the same situation Tradovate's cid/sec are already in) plus a pre-obtained accessToken (see above).

Match-Trader

  • No confirmed push/streaming capability. Match-Trader's own docs

mention a WebSocket market-watch channel (STOMP protocol) for live prices, but no equivalent push channel for position/order/balance changes is confirmed — REST-polling only (deps.pollIntervalMs, default 5s), same as TradeLocker/DXtrade. No live price streaming bonus method either — the market-watch WebSocket's exact message schema wasn't confirmed from available public sources.

  • SYSTEM_UUID resolution is best-effort. The login response is

confirmed to include tradingAccounts/selectedTradingAccount objects, but the exact field holding the SYSTEM_UUID every other call's URL path needs isn't shown verbatim in the source material — read defensively under several plausible key names, with credentials.systemUuid as a reliable override the Add Broker wizard doesn't currently expose.

  • **positions/close and pending-order/cancel request bodies are

inferred, not confirmed** — by symmetry with position/edit's confirmed shape and every other single-target action, respectively. See raw-types.ts's file doc.

  • **Modifying a pending order is cancel-then-recreate, not a true

edit** — no confirmed pending-order/edit request shape was found in the source material, so a price/SL/TP change on a still-pending order cancels it and creates a fresh one with the new values, rather than guessing an edit endpoint's body. The resulting order gets a new id; callers that track orders by id across a modify should expect this.

  • **Auth-trading-api header value is assumed identical to the

co-auth cookie's token** — both are confirmed as required headers, but whether they're literally the same value or two different tokens from different parts of the login response isn't shown verbatim.

Cross-cutting

  • No connector runs as a persistent background process. Every

connector instance in this codebase is constructed, connected, used, and disconnected within the lifetime of one Next.js request (see src/lib/broker-connections/live.ts and ConnectorManager's own doc comment). Automatic reconnect, heartbeat monitoring, and live streaming all work correctly *within* a connector instance's lifetime, but nothing in the current deployment keeps a connector instance — and therefore its live stream — alive *across* requests. Wiring a real always-on streaming pipeline (master trade → Copy Engine, with no user request in the loop) requires a separate long-running worker process, which is a deployment decision outside this codebase's current scope, not a code change.

Architecture notes

  • Every connector (MT5, TradeLocker, Tradovate, Rithmic, DXtrade, cTrader, Match-Trader) implements the

identical TradingConnector interface (src/lib/connectors/sdk/connector.ts) and shares BaseConnector's connection-state tracking and metrics recording — see docs/ARCHITECTURE.md's Broker Abstraction Layer section. Nothing above the connector layer (registry, manager, Copy Engine) branches on which broker it's talking to.

  • Rithmic is architecturally the closest to MT5 among the three REST/

WebSocket-based connectors (TradeLocker/Tradovate/Rithmic): a single stateful session for its whole lifetime (login once, hold it open), versus TradeLocker's/Tradovate's fully stateless REST calls. Its position/order sync is correspondingly closer to MT5's push model than to TradeLocker's/Tradovate's poll-only model — see RithmicPositionService's own doc comment for how the reconciliation poll plays a safety-net role instead of being the only sync mechanism.

  • Rithmic is the only connector in this codebase built entirely behind

an injected transport PORT with no working default implementation (RithmicTransport/RithmicUnimplementedTransport — src/lib/connectors/rithmic/transport.ts) rather than a concrete sdk-adapter.ts making real network calls. See docs/RITHMIC_TRANSPORT.md for the full rationale and what replacing it involves.

  • sdk/types.ts's PlaceOrderRequest gained a 'stop_limit' order type

and a stopPrice field for Rithmic specifically (a stop-limit order needs a trigger price AND a limit price simultaneously — no existing connector's order model required two distinct prices before). MT5/ TradeLocker/Tradovate's own placeOrder now explicitly reject 'stop_limit' rather than silently mishandling it.

  • Phase 2 validation (tests/rithmic-copy-engine-validation.test.ts)

uncovered and fixed one genuine, pre-existing bug in src/lib/connectors/normalizer/to-trade-signal.ts — the one bridge every connector's output passes through on its way into the Copy Engine — which never actually populated externalTradeId/platformId/ eventId on the signals it built, despite TradeSignal.eventId's own doc comment and the idempotency store's buildIdempotencyKey() already assuming a real connector always supplies one. Fixed in that one shared file, benefiting MT5/TradeLocker/Tradovate's duplicate- prevention robustness equally, not just Rithmic's.

Performance notes

  • New metrics this milestone adds, on top of every existing connector

metric (docs/ARCHITECTURE.md's Observability section): connector_market_data_latency_ms and connector_position_sync_latency_ms, both via the existing withLatency() helper (shared/reliability.ts) — previously built but unused by any connector. All three latency histograms (order/market-data/position-sync) are visible via GET /api/health and GET /api/health/market-data, filterable by platform="rithmic".

  • The new Connection Diagnostics panel

(src/components/broker-connections/connection-diagnostics.tsx, added to the Broker Status page for every platform, not just Rithmic) surfaces reconnect counts, retry counts, error counts, and all three latency histograms' avg/p95 per connection, plus the last 15 structured log lines for that platform — all read from the existing metrics/getRecentLogs() observability module with zero new plumbing.

  • No live performance data exists for Rithmic specifically yet — the

metrics are wired and ready, but docs/RITHMIC_TRANSPORT.md's "Remaining production tasks" section calls out live latency characterization as still outstanding once a real transport exists.

  • RithmicPositionService's reconciliation poll (a safety net alongside

real push updates, not the primary sync path) defaults to a 15s interval — deliberately longer than TradeLocker's/Tradovate's 5s poll, since those platforms have no alternative but polling while Rithmic's poll only exists to catch a missed push.

Required credentials

PlatformFieldsNotes
MT5login, password, server (per connection) + METAAPI_TOKEN (server-wide env var)The MetaApi token is never stored per-connection — only in the env var, re-supplied on every connect.
TradeLockeremail, password, server, environment (demo\live)No server-side secret required beyond the trader's own login.
Tradovatename, password, cid, sec, environment (demo\live), optionally appId/appVersion/deviceIdcid/sec identify an approved Tradovate API application — a separate credential from the trader's own login, obtained from Tradovate directly.
Rithmicusername, password, systemName, environment (demo\live), optionally appName/appVersionsystemName identifies which Rithmic-issued gateway this login connects to — see docs/RITHMIC_TRANSPORT.md for how developer access (including a system name) is actually obtained; not self-serve like Tradovate's.
DXtradeusername, password, domain, serverserver is the broker's own DXtrade REST hostname (e.g. dx.yourbroker.com) — every DXtrade deployment is single-tenant per broker, unlike TradeLocker's shared demo/live hosts, so there's no fixed pair to pick from. domain is dxSCA's own login-body field, distinct from server.
cTraderclientId, clientSecret, accessToken, environment (demo\live)clientId/clientSecret identify a registered Open API application, obtained from openapi.ctrader.com — separate from the trader's own login, same situation as Tradovate's cid/sec. accessToken must be pre-obtained via cTrader's own OAuth2 flow — this connector doesn't perform that redirect flow itself (see Known Limitations).
Match-Traderemail, password, brokerId, server, optionally systemUuidserver is the broker's own Match-Trader hostname (e.g. mtr-live.yourbroker.com) — every deployment is single-tenant per broker, the same situation DXtrade's server field is in. brokerId is Match-Trader's own multi-broker discriminator (confirmed: "brokerId and partnerId refer to the same identifier"), distinct from server. systemUuid is a reliable override for the best-effort auto-discovery (see Known Limitations).

All per-connection credentials are AES-256-GCM encrypted (src/lib/security/credential-crypto.ts) before being stored in broker_connections.encrypted_credentials; the encryption key (CREDENTIAL_ENCRYPTION_KEY) lives only in a server-side env var, never in the database.

Validation checklist

Run this against a real demo account before trusting a connector with live money, and after any change to that connector's sdk-adapter.ts (for Rithmic specifically, use docs/LIVE_TEST_CHECKLIST.md instead — it's scoped around the transport swap-in docs/RITHMIC_TRANSPORT.md describes):

1. Environment — required env vars set (METAAPI_TOKEN for MT5; CREDENTIAL_ENCRYPTION_KEY for all); supabase db push run so the tradelocker/tradovate/rithmic/match-trader platform migrations are applied (ctrader/dxtrade needed no migration — the brokers_platform_check constraint already allowed them from an earlier speculative migration). 2. Add Broker wizard — Test Connection succeeds with real demo credentials for the platform; Save creates a broker_connections row with a non-null encrypted_credentials and a trading_accounts row with the correct balance/equity/currency. 3. Account Details page (/dashboard/accounts/[id]) — Live Snapshot refresh shows live balance/equity and a healthy connection status. 4. Live Positions page — open a position manually on the demo account in the broker's own platform; refresh and confirm it appears with the correct symbol/side/volume/open price. 5. Broker Status page — Check Status reports healthy with a plausible latency (not unreachable, not consistently >2s). 6. Place/modify/close round trip — via the Copy Engine's simulator (/dashboard/copier, Simulate a Trade) with a real master↔follower relationship pointed at demo accounts: open a market order, confirm it fills on the follower; modify SL/TP, confirm it updates; close it, confirm the position disappears and copy_logs records success. Repeat for limit and stop orders. 7. Reconnect — disconnect the demo account's network (or revoke/ re-issue the session token, platform-permitting) mid-poll and confirm the connector's structured logs show Scheduling <platform> reconnect followed by <platform> reconnect succeeded, and that positions resume updating afterward. 8. Metrics sanity — hit GET /api/health/market-data and GET /api/health and confirm connector_order_latency_ms and connector_reconnect_*_total show non-zero, plausible values after steps 6–7. 9. Live price streaming (TradeLocker/Tradovate only) — subscribe to a symbol's price via the connector's streamPrice(), confirm quotes arrive within the platform's documented polling interval (TradeLocker) or promptly after subscribing (Tradovate WebSocket).