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:
| Capability | MT5 | TradeLocker | Tradovate | DXtrade | cTrader | Match-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):
| Broker | Test files | Tests |
|---|---|---|
| MT5 | 1 | 31 |
| TradeLocker | 6 | 69 |
| Tradovate | 6 | 87 |
| DXtrade | 5 | 57 |
| cTrader | 6 | 60 |
| Match-Trader | 5 | 54 |
Rithmic (architecture, FakeRithmicTransport) | 8 | 116 |
| Total | 37 | 474 |
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
| Platform | Asset class | Connector module | Status |
|---|---|---|---|
| MetaTrader 5 (MT5) | Forex | src/lib/connectors/mt5 | Implemented |
| TradeLocker | Forex | src/lib/connectors/tradelocker | Implemented |
| Tradovate | Futures | src/lib/connectors/tradovate | Implemented |
| Rithmic | Futures | src/lib/connectors/rithmic | Implemented — architecture & business logic complete; wire transport not yet connected to a live gateway (see docs/RITHMIC_TRANSPORT.md) |
| DXtrade | Forex | src/lib/connectors/dxtrade | Implemented — 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 |
| cTrader | Forex | src/lib/connectors/ctrader | Implemented — 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-Trader | Forex | src/lib/connectors/match-trader | Implemented — 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)
| Feature | MT5 | TradeLocker | Tradovate | Rithmic | DXtrade | cTrader | Match-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/modifyorderand `/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.
ProtoOASpotEventis 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.
ProtoOATradercarries 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.
- **
clientMsgIdecho-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/closeandpending-order/cancelrequest 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-apiheader 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'sPlaceOrderRequestgained 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
| Platform | Fields | Notes | |
|---|---|---|---|
| MT5 | login, 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. | |
| TradeLocker | email, password, server, environment (demo\ | live) | No server-side secret required beyond the trader's own login. |
| Tradovate | name, password, cid, sec, environment (demo\ | live), optionally appId/appVersion/deviceId | cid/sec identify an approved Tradovate API application — a separate credential from the trader's own login, obtained from Tradovate directly. |
| Rithmic | username, password, systemName, environment (demo\ | live), optionally appName/appVersion | systemName 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. |
| DXtrade | username, password, domain, server | server 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. | |
| cTrader | clientId, 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-Trader | email, password, brokerId, server, optionally systemUuid | server 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).