Repository navigation
feat(uta): add a cTrader Open API broker pack - #1784
Draft
mehdijamshidian90 wants to merge 22 commits into
Draft
mehdijamshidian90 wants to merge 22 commits into
mehdijamshidian90 wants to merge 22 commits into
Conversation
…ers, quotes, bars)
…tead of hedging on hedged accounts
…moves cTrader stores a stop-limit's limit as a distance from the stop, so an amend that sent only the new stop moved the limit with it. Recompute the distance from the unchanged limit, refuse a stop that would cross it, and re-send the current distance on every stop-limit amend.
…nects Trading mode, schedule and volume limits were cached for the life of the connection. Drop a spec when the venue reports the symbol changed, and drop all of them after a reconnect, since changes may have been missed.
The venue rejects an amend of a stop or stop-limit order that omits the
stop price, even when the stop is unchanged ("Stop price should be
defined", Spotware demo), so volume-only and limit-only amends failed.
Re-send the current stop price, and a limit order's limit price, unless
the amend changes it. The fake venue now reproduces the rejection.
|
@mehdijamshidian90 is attempting to deploy a commit to the luokerenx4's Team Team on Vercel. A member of the Team first needs to authorize it. |
cTrader has no venue-wide clock and the market clock reports only the first listed symbol's schedule, while contract details left `tradingHours` empty. An agent therefore had no way to learn when a given symbol trades (index CFDs, FX, metals and crypto all differ). Contract details now carry the symbol's own schedule for the coming week in IBKR's `tradingHours` format, wall-clock in the symbol's `timeZoneId`, with days without a session marked CLOSED. Session starts round up and ends round down to the minute so the hours never claim the symbol trades when it does not, and the wall-clock arithmetic keeps the hours stable across daylight-saving changes. A symbol without a schedule still reports no hours. Read back on the Spotware demo for XAUUSD, EURUSD, US 30, US 500, US TECH 100, XTIUSD, BTCUSD and ETHUSD.
When a position's stop-loss or take-profit fired on the venue, the adapter reported the leg as cancelled and never gave an exit price, so UTA could not list the fill and journals had to guess the exit. The venue serves both legs with one STOP_LOSS_TAKE_PROFIT order that carries the stop-loss as `stopPrice` and the take-profit as `limitPrice`; the settle logic read `stopLoss`/`takeProfit`, which that order does not have. The order list by position also leaves the protection order out. Settled legs now come from the position's deals: the closing deal points to the order that closed it, a protection order fires the leg whose level is nearer the fill, and the deals give the volume-weighted exit price and volume. A position closed by another order cancels both legs. Orders read from the venue now also report `executedVolume` as their filled quantity, so a pending order that fills later is listed as a fill too. Verified on the Spotware demo with a fired take-profit (minimum EURUSD, account flat afterwards).
On the demo venue, the deal list for a just-closed position answered INCORRECT_BOUNDARIES for one to two hours and then accepted the same window. The error escaped getOrder, so UTA's order sync failed for every order and pushes failed too: a break-even stop move stayed pending until it went stale. An unreadable closed leg is now reported as unknown, so UTA keeps syncing the other orders and retries the leg on a later poll.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
ctraderBroker Pack: a UTA broker for any broker that offers cTrader (FX, metals, indices, commodities, share and crypto CFDs), demo and live, through Spotware's cloud Open API. Nothing has to run on the user's machine and there is no third-party SDK: the adapter speaks the documented JSON protocol over the runtime's globalWebSocket(Node 22+, Bun). It is broker- and symbol-neutral. Every instrument comes from the account's own symbol catalog, and nothing is hard-coded per broker, region or symbol.Why cTrader: many retail brokers offer it, including in regions the current broker set does not serve. The Open API is free, cloud-hosted and has a demo environment, so it fits the Broker Pack boundary without native code (MT5, by comparison, needs a running Windows terminal).
This is a draft for your review. I have not opened a separate proposal issue. If it is easier to review in parts (for example transport and session first), I am happy to split it.
Design
Code lives in
services/uta/src/domain/trading/brokers/ctrader/, wrapped bypackages/uta-broker-ctraderlike the other packs.Transport (
ctrader-transport.ts): request/reply correlation byclientMsgId, timeouts, heartbeat, and FIFO rate limiting below the documented per-connection limits (50 req/s, 5 req/s historical). Payloads are never logged; auth requests carry secrets.Session (
ctrader-session.ts): app auth, then account discovery by access token, then account auth. The account is selected by the login shown in cTrader, and a demo login on a live connection (or the reverse) is refused. The session never reconnects on its own. It reports dead/alive, and UTA's recovery loop calls an idempotentensureConnected(). Concurrent callers share one in-flight attempt, so recovery can never run overlapping handshakes (the failure class behind [UTA] Recovery loop: a nudge during an in-flight attempt starts a parallel chain, and close() does not stop in-flight recovery #1762).Identity: nativeKey = the broker's symbol name, conId = symbolId, secType
CFDfor every instrument, currency = quote asset.assetClassForis derived from the broker's own category and asset-class names.Units: quantities are base units, never lots, because lot size differs per broker and symbol. Volume errors name the lot size so an agent can convert.
Orders: MKT, LMT, STP and STP LMT.
clientOrderIdbefore reporting an unknown outcome.Position protection: cTrader keeps SL/TP on the position, not as orders. They are exposed as synthetic orders
sl-<positionId>/tp-<positionId>, which are:PlaceOrderResult.legsgetOpenOrdersmodifyOrderand removed bycancelOrderAn amend always re-sends the other level, because the venue drops an omitted one.
Hedged accounts: an order opposite an open position would open a second, opposite position. UTA's model is netting, so such an order is linked to the position it reduces (
positionId). Orders that cannot be linked are refused with an explanation: a flip, an order spanning several positions, or one with both sides open. The venue does not cancel a linked pending order when its position closes, soclosePositiondoes.Reads: hedged positions are merged per (symbol, side) with a volume-weighted average cost.
closePositioncloses the oldest first and refuses when both sides are open.Integration
CTRADER_PRESETin Recommended, with thesecuritiesguard set and demo/live modes. Client secret and tokens are write-only, and the fingerprint is mode + account number.ui/src/demohandlers.pnpm-lock.yamlgains only the new importer.docs/broker-packs.mddocs/uta-live-testing.mdplans/ctrader-broker-pack.mdDecision needed: rotating refresh token
cTrader access tokens last about 30 days. Refreshing works, but it rotates the refresh token and voids the stored pair. UTA cannot write Alice's sealed
accounts.json. So today the rotated pair lives only in memory (the default hook logs a warning), and a UTA restart after a refresh needs a new authorization. The preset hint tells the user.The plan lists three options:
I did not want to pick a cross-owner design without you. The session isolates the mechanics behind a single
onTokensRefreshedhook, so any of the three plugs in there.Verification
services/uta,@traderalice/uta-protocol, the pack, andui.pnpm test(andpnpm test:changedafter the last fix) on Windows x64: 7,755 passed. The 14 failures in 11 files fail identically on cleandev5da8dfc on this host (symlink EPERM without Developer Mode, and three CLI environment specs).pnpm test:integration:uta: 291 passed.pnpm broker-packs:buildandverify-broker-packs.ts --compiledwith the pinned Bun 1.4.2 on win32-x64: six archives verified; the cTrader pack is 2.4 MiB and imports and constructs in the compiled probe.pnpm -F open-alice-ui dev:demo: the Connect-broker card, install step and form (six fields, secrets masked), with no console errors.Known limits
alice-utahas not been run yet; acceptance so far is adapter-level.getMarketClockreports the first listed symbol's schedule, because cTrader has no venue-wide clock.