ctrader-x 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -176,7 +176,7 @@ npm run start:trendbars # fetch the last 24h of H1 bars for a symbol
|
|
|
176
176
|
|
|
177
177
|
## API Reference
|
|
178
178
|
|
|
179
|
-
Everything below is exported from the package root (`import { ... } from 'ctrader-x'`). Classes with events expose `once()` and `off()` with the same signatures as `on()`. The generated Protobuf message and enum types (`ProtoOA...`, `Proto...` — roughly 300 of them) aren't listed individually; see [Types](#types) at the end.
|
|
179
|
+
Everything below is exported from the package root (`import { ... } from 'ctrader-x'`). For a signatures-only cheat sheet, skip to [Quick reference](#quick-reference). Classes with events expose `once()` and `off()` with the same signatures as `on()`. The generated Protobuf message and enum types (`ProtoOA...`, `Proto...` — roughly 300 of them) aren't listed individually; see [Types](#types) at the end.
|
|
180
180
|
|
|
181
181
|
### Transport
|
|
182
182
|
|
|
@@ -446,6 +446,119 @@ Order mutations have no dedicated response message — the outcome arrives as a
|
|
|
446
446
|
|
|
447
447
|
Every Protobuf message and enum from Spotware's Open API — `ProtoOANewOrderReq`, `ProtoOATradeSide`, `ProtoOAExecutionEvent`, and roughly 300 more — is generated directly from the official `.proto` files (see [Regenerating protocol types](#regenerating-protocol-types)) and exported from the package root. They aren't listed individually here; each one carries its own field-level doc comments, visible in your editor.
|
|
448
448
|
|
|
449
|
+
## Quick reference
|
|
450
|
+
|
|
451
|
+
Everything the package exports, in one place, for when you already know the concepts and just need the signature. The [API Reference](#api-reference) above has the same material with the reasoning behind it.
|
|
452
|
+
|
|
453
|
+
### Methods
|
|
454
|
+
|
|
455
|
+
| Class | Member | Description |
|
|
456
|
+
| --- | --- | --- |
|
|
457
|
+
| `SpotwareTransport` | `connect(): Promise<void>` | Opens the connection. Rejects if the *first* attempt fails; later reconnects keep retrying. |
|
|
458
|
+
| | `disconnect(): Promise<void>` | Intentional close — no auto-reconnect follows. |
|
|
459
|
+
| | `send(message: ProtoMessage): Promise<void>` | Sends a raw framed message, rate-limited per Spotware's documented limits. |
|
|
460
|
+
| `SpotwareOAuthClient` | `buildAuthorizationUrl({ redirectUri, scope }): string` | The URL to send the user to for browser authorization. |
|
|
461
|
+
| | `exchangeAuthorizationCode({ code, redirectUri }): Promise<ISpotwareOAuthToken>` | Trades the redirect code for tokens. The code expires in ~1 minute. |
|
|
462
|
+
| | `refreshAccessToken({ refreshToken }): Promise<ISpotwareOAuthToken>` | Refresh tokens are single-use — persist what comes back. |
|
|
463
|
+
| `SpotwareSocketAuthenticator` | `authenticateApplication(clientId, clientSecret): Promise<void>` | `ApplicationAuthReq`. Once per connection. |
|
|
464
|
+
| | `listAccounts(accessToken): Promise<ProtoOACtidTraderAccount[]>` | The only way to discover a `ctidTraderAccountId` from a bare access token. |
|
|
465
|
+
| | `authenticateAccount(ctidTraderAccountId, accessToken): Promise<void>` | `AccountAuthReq`. Once per account. |
|
|
466
|
+
| `SpotwareClient` | `readonly ctidTraderAccountId: number` | The account this client is bound to. |
|
|
467
|
+
| | `connect(): Promise<void>` | Connects and completes the auth handshake. |
|
|
468
|
+
| | `disconnect(): Promise<void>` | Closes the underlying transport. |
|
|
469
|
+
| | `send(payloadType, payload): Promise<ProtoMessage>` | Correlated request. Rejects with `SpotwareRequestError` on error, timeout, or a drop mid-flight. |
|
|
470
|
+
| `SpotwareMarketData` | `readonly symbols: SpotwareSymbolCatalog` | The catalog used to resolve names to ids. |
|
|
471
|
+
| | `subscribe(symbol: number \| string): Promise<void>` | By `symbolId` or by name. Re-applied automatically after a reconnect. |
|
|
472
|
+
| | `unsubscribe(symbol: number \| string): Promise<void>` | |
|
|
473
|
+
| | `getTrendbars(params: IGetTrendbarsParams): Promise<ITrendbar[]>` | One-off historical fetch, not a subscription. Prices already converted. |
|
|
474
|
+
| `SpotwareSymbolCatalog` | `getAll(): Promise<ProtoOALightSymbol[]>` | Cached after the first call. |
|
|
475
|
+
| | `findByName(symbolName): Promise<ProtoOALightSymbol \| undefined>` | Case-insensitive. |
|
|
476
|
+
| | `findById(symbolId): Promise<ProtoOALightSymbol \| undefined>` | |
|
|
477
|
+
| | `refresh(): Promise<ProtoOALightSymbol[]>` | Forces a re-fetch of the list. |
|
|
478
|
+
| | `getFullSymbol(symbolId): Promise<ProtoOASymbol \| undefined>` | Full spec — `lotSize`, volume bounds, `digits`, `pipPosition`. Cached per id. |
|
|
479
|
+
| `SpotwareTrading` | `placeMarketOrder(params): Promise<ProtoOAExecutionEvent>` | |
|
|
480
|
+
| | `placeLimitOrder(params): Promise<ProtoOAExecutionEvent>` | |
|
|
481
|
+
| | `amendOrder(params): Promise<ProtoOAExecutionEvent>` | |
|
|
482
|
+
| | `cancelOrder(orderId): Promise<ProtoOAExecutionEvent>` | |
|
|
483
|
+
| | `closePosition(params): Promise<ProtoOAExecutionEvent>` | |
|
|
484
|
+
| | `getOpenPositionsAndOrders(): Promise<IOpenPositionsAndOrders>` | |
|
|
485
|
+
|
|
486
|
+
Constructors: `new SpotwareTransport(options)`, `new SpotwareOAuthClient({ clientId, clientSecret })`, `new SpotwareSocketAuthenticator(transport, options?)`, `new SpotwareClient(options)`, `new SpotwareMarketData(client, symbolCatalog?)`, `new SpotwareSymbolCatalog(client)`, `new SpotwareTrading(client)`.
|
|
487
|
+
|
|
488
|
+
### Events
|
|
489
|
+
|
|
490
|
+
Every emitter also exposes `once()` and `off()` with the same signatures as `on()`.
|
|
491
|
+
|
|
492
|
+
| Class | Event | Listener arguments |
|
|
493
|
+
| --- | --- | --- |
|
|
494
|
+
| `SpotwareTransport` | `connected` | — |
|
|
495
|
+
| | `disconnected` | `(reason: SpotwareDisconnectReason)` |
|
|
496
|
+
| | `reconnecting` | `(attempt: number, delayMs: number)` |
|
|
497
|
+
| | `message` | `(message: ProtoMessage)` |
|
|
498
|
+
| | `error` | `(error: Error)` |
|
|
499
|
+
| `SpotwareClient` | `authenticated` | — (fires again after every reconnect) |
|
|
500
|
+
| | `tokenRefreshed` | `(token: ISpotwareOAuthToken)` — persist this |
|
|
501
|
+
| | `message` | `(message: ProtoMessage)` — every message, correlated or not |
|
|
502
|
+
| | `error` | `(error: Error)` — includes forwarded transport errors |
|
|
503
|
+
| `SpotwareMarketData` | `price` | `(update: ISpotwarePriceUpdate)` |
|
|
504
|
+
| | `error` | `(error: Error)` |
|
|
505
|
+
|
|
506
|
+
Always attach an `'error'` listener: per Node's `EventEmitter` convention, an unlistened `'error'` event throws and crashes the process.
|
|
507
|
+
|
|
508
|
+
### Interfaces
|
|
509
|
+
|
|
510
|
+
| Interface | Shape |
|
|
511
|
+
| --- | --- |
|
|
512
|
+
| `ISpotwareTransportOptions` | `{ host, port?, reconnectBackoff?, socketFactory?, staleConnectionTimeoutMs? }` |
|
|
513
|
+
| `ISpotwareClientOptions` | `{ transport, oauthClient, clientId, clientSecret, ctidTraderAccountId, token, requestTimeoutMs?, tokenRefreshBufferMs? }` |
|
|
514
|
+
| `ISpotwareOAuthToken` | `{ accessToken, refreshToken, tokenType, expiresIn }` |
|
|
515
|
+
| `IReconnectBackoffOptions` | `{ baseDelayMs, maxDelayMs, factor }` |
|
|
516
|
+
| `ISpotwarePriceUpdate` | `{ symbolId, bid?, ask?, timestamp? }` — decimal prices, already unscaled |
|
|
517
|
+
| `IGetTrendbarsParams` | `{ symbolId, period, fromTimestamp?, toTimestamp?, count? }` — timestamps in Unix ms |
|
|
518
|
+
| `ITrendbar` | `{ period, timestamp?, open, high, low, close, volume }` |
|
|
519
|
+
| `IPlaceMarketOrderParams` | `{ symbolId, tradeSide, volume, stopLoss?, takeProfit?, comment?, label? }` — volume in units |
|
|
520
|
+
| `IPlaceLimitOrderParams` | `IPlaceMarketOrderParams` + `{ limitPrice, timeInForce?, expirationTimestamp? }` |
|
|
521
|
+
| `IAmendOrderParams` | `{ orderId, volume?, limitPrice?, stopPrice?, stopLoss?, takeProfit?, expirationTimestamp? }` |
|
|
522
|
+
| `IClosePositionParams` | `{ positionId, volume }` — volume in units |
|
|
523
|
+
| `IOpenPositionsAndOrders` | `{ positions: ProtoOAPosition[], orders: ProtoOAOrder[] }` |
|
|
524
|
+
|
|
525
|
+
### Constants, enums and types
|
|
526
|
+
|
|
527
|
+
| Export | Value or shape |
|
|
528
|
+
| --- | --- |
|
|
529
|
+
| `SpotwareHost` | `enum { DEMO = 'demo.ctraderapi.com', LIVE = 'live.ctraderapi.com' }` |
|
|
530
|
+
| `SpotwareOAuthScope` | `enum { ACCOUNTS = 'accounts', TRADING = 'trading' }` |
|
|
531
|
+
| `SPOTWARE_PORT` | `5035` |
|
|
532
|
+
| `SPOTWARE_PRICE_SCALE` | `100_000` |
|
|
533
|
+
| `SPOTWARE_VOLUME_SCALE` | `100` |
|
|
534
|
+
| `DEFAULT_RECONNECT_BACKOFF_OPTIONS` | `{ baseDelayMs: 500, maxDelayMs: 30_000, factor: 2 }` |
|
|
535
|
+
| `calculateReconnectDelayMs(attempt, options?)` | Pure function — next backoff delay, with jitter. |
|
|
536
|
+
| `SpotwareDisconnectReason` | `'manual' \| 'dropped'` |
|
|
537
|
+
| `SpotwareSocketFactory` | `(port: number, host: string) => Promise<Duplex>` |
|
|
538
|
+
|
|
539
|
+
### Errors
|
|
540
|
+
|
|
541
|
+
All extend `Error`, so `instanceof` works.
|
|
542
|
+
|
|
543
|
+
| Error | Extra fields | Thrown by |
|
|
544
|
+
| --- | --- | --- |
|
|
545
|
+
| `SpotwareOAuthError` | `errorCode?: string`, `httpStatus?: number` | `SpotwareOAuthClient` |
|
|
546
|
+
| `SpotwareSocketAuthError` | `errorCode?: string` | `SpotwareSocketAuthenticator` |
|
|
547
|
+
| `SpotwareRequestError` | `errorCode?: string` | `SpotwareClient.send()`, and everything built on it |
|
|
548
|
+
|
|
549
|
+
### Defaults
|
|
550
|
+
|
|
551
|
+
| Setting | Default | Set via |
|
|
552
|
+
| --- | --- | --- |
|
|
553
|
+
| Port | `5035` | `ISpotwareTransportOptions.port` |
|
|
554
|
+
| Stale connection timeout | `30_000` ms | `ISpotwareTransportOptions.staleConnectionTimeoutMs` |
|
|
555
|
+
| Reconnect backoff | `500` ms base, `30_000` ms cap, factor `2` | `ISpotwareTransportOptions.reconnectBackoff` |
|
|
556
|
+
| Auth handshake timeout | `10_000` ms | `ISpotwareSocketAuthenticatorOptions.responseTimeoutMs` |
|
|
557
|
+
| Request timeout | `10_000` ms | `ISpotwareClientOptions.requestTimeoutMs` |
|
|
558
|
+
| Token refresh buffer | `300_000` ms (5 min before expiry) | `ISpotwareClientOptions.tokenRefreshBufferMs` |
|
|
559
|
+
| Heartbeat interval | `10_000` ms | not configurable — Spotware requires at least one every 10s |
|
|
560
|
+
| Rate limits | 5 req/s historical, 50 req/s everything else | not configurable — enforced automatically, per connection |
|
|
561
|
+
|
|
449
562
|
## Development
|
|
450
563
|
|
|
451
564
|
### Running tests
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spotware-socket-authenticator.d.ts","sourceRoot":"","sources":["../../src/auth/spotware-socket-authenticator.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"spotware-socket-authenticator.d.ts","sourceRoot":"","sources":["../../src/auth/spotware-socket-authenticator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAIH,wBAAwB,EAO3B,MAAM,UAAU,CAAC;AAKlB,MAAM,WAAW,mCAAmC;IAChD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,qBAAa,2BAA2B;IAIhC,OAAO,CAAC,QAAQ,CAAC,SAAS;IAH9B,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;gBAGtB,SAAS,EAAE,iBAAiB,EAC7C,OAAO,GAAE,mCAAwC;IAK/C,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY9E,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,wBAAwB,EAAE,CAAC;IActE,mBAAmB,CAAC,mBAAmB,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1F,OAAO,CAAC,YAAY;CAoDvB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SpotwareSocketAuthenticator = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
5
|
const types_1 = require("../types");
|
|
5
6
|
const spotware_socket_auth_error_1 = require("./spotware-socket-auth-error");
|
|
6
7
|
const DEFAULT_RESPONSE_TIMEOUT_MS = 10_000;
|
|
@@ -40,6 +41,7 @@ class SpotwareSocketAuthenticator {
|
|
|
40
41
|
}), types_1.ProtoOAPayloadType.PROTO_OA_ACCOUNT_AUTH_RES);
|
|
41
42
|
}
|
|
42
43
|
sendAndAwait(message, expectedPayloadType) {
|
|
44
|
+
const clientMsgId = (0, node_crypto_1.randomUUID)();
|
|
43
45
|
return new Promise((resolve, reject) => {
|
|
44
46
|
let timeout;
|
|
45
47
|
const cleanup = () => {
|
|
@@ -47,6 +49,13 @@ class SpotwareSocketAuthenticator {
|
|
|
47
49
|
this.transport.off('message', onMessage);
|
|
48
50
|
};
|
|
49
51
|
const onMessage = (received) => {
|
|
52
|
+
// Two handshakes can overlap on one socket (e.g. a reconnect firing while the
|
|
53
|
+
// previous attempt is still in flight), and both would be waiting on the same
|
|
54
|
+
// payloadType. Without this, one response would settle both — leaving an
|
|
55
|
+
// account that never got a reply believing it is authenticated.
|
|
56
|
+
if (received.clientMsgId && received.clientMsgId !== clientMsgId) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
50
59
|
if (received.payloadType === expectedPayloadType) {
|
|
51
60
|
cleanup();
|
|
52
61
|
resolve(received);
|
|
@@ -69,7 +78,7 @@ class SpotwareSocketAuthenticator {
|
|
|
69
78
|
reject(new spotware_socket_auth_error_1.SpotwareSocketAuthError(`Timed out waiting for payloadType ${expectedPayloadType}`));
|
|
70
79
|
}, this.responseTimeoutMs);
|
|
71
80
|
this.transport.on('message', onMessage);
|
|
72
|
-
this.transport.send(message).catch((error) => {
|
|
81
|
+
this.transport.send(types_1.ProtoMessage.fromPartial({ ...message, clientMsgId })).catch((error) => {
|
|
73
82
|
cleanup();
|
|
74
83
|
reject(error);
|
|
75
84
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"spotware-socket-authenticator.js","sourceRoot":"","sources":["../../src/auth/spotware-socket-authenticator.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"spotware-socket-authenticator.js","sourceRoot":"","sources":["../../src/auth/spotware-socket-authenticator.ts"],"names":[],"mappings":"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"}
|