@mfxhub/sdk 0.2.2 → 0.3.0

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/dist/index.d.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  * const client = new MfxClient("https://api.dev.mfxhub.com/v1", keyId, privateKey);
7
7
  * ```
8
8
  */
9
- export { MfxClient, DEFAULT_READ_RETRIES, DEFAULT_TIMEOUT_MS } from "./client.js";
10
- export type { Answer, Budget, ClientOptions, RequestSpec } from "./client.js";
9
+ export { MfxClient, CALL_LOG_ENV, DEFAULT_READ_RETRIES, DEFAULT_TIMEOUT_MS } from "./client.js";
10
+ export type { Answer, Budget, CallTiming, ClientOptions, RequestSpec } from "./client.js";
11
11
  export * from "./errors.js";
12
12
  export { Page } from "./pages.js";
13
13
  export { Catalog } from "./catalog.js";
@@ -21,7 +21,9 @@ export type { Connector, StreamConnection, StreamEvent, StreamOptions, StreamSta
21
21
  export type { ClosePosition, ClosePositionBy, ClosePositionPartial, Keyed, ModifyOrder, ModifyPosition, OrderType, PlaceOrder, SetTrailingStop, Side, WaitFor, } from "./execution.js";
22
22
  export type { CommandState, ExecutionResult, Reason, Tickets } from "./results.js";
23
23
  export { Terminals, isUsable } from "./terminals.js";
24
- export type { CreateTerminal, JournalLine, JournalOptions, PageOptions, Redundancy, Terminal, TerminalFilter, TerminalJournal, TerminalState, } from "./terminals.js";
24
+ export type { CreateTerminal, BulkAction, BulkAsk, JournalLine, JournalOptions, PageOptions, Redundancy, Terminal, TerminalFilter, TerminalJournal, TerminalQueue, TerminalState, WaitOptions, } from "./terminals.js";
25
+ export { TradingAccounts } from "./trading-accounts.js";
26
+ export type { AccountEnvironment, AccountState, TradingAccount, TradingAccountFilter, TradingAccountPageOptions, Venue, } from "./trading-accounts.js";
25
27
  export { RequestSigner, SigningKey, signingMessage } from "./signing.js";
26
28
  export { KEY_HEADER, NONCE_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER } from "./signing.js";
27
29
  export type { components, operations, paths } from "./generated/api.js";
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * const client = new MfxClient("https://api.dev.mfxhub.com/v1", keyId, privateKey);
7
7
  * ```
8
8
  */
9
- export { MfxClient, DEFAULT_READ_RETRIES, DEFAULT_TIMEOUT_MS } from "./client.js";
9
+ export { MfxClient, CALL_LOG_ENV, DEFAULT_READ_RETRIES, DEFAULT_TIMEOUT_MS } from "./client.js";
10
10
  export * from "./errors.js";
11
11
  export { Page } from "./pages.js";
12
12
  export { Catalog } from "./catalog.js";
@@ -14,6 +14,7 @@ export { Accounts } from "./accounts.js";
14
14
  export { Execution, POLL_CAP_MS, POLL_FROM_MS } from "./execution.js";
15
15
  export { CLOSE_LIFETIME, CLOSE_REAUTHENTICATE, CLOSE_SLOW_CONSUMER, Channel, ChannelRefused, MfxStream, Resync, StreamClosed, StreamMessage, wsConnector, } from "./stream.js";
16
16
  export { Terminals, isUsable } from "./terminals.js";
17
+ export { TradingAccounts } from "./trading-accounts.js";
17
18
  export { RequestSigner, SigningKey, signingMessage } from "./signing.js";
18
19
  export { KEY_HEADER, NONCE_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER } from "./signing.js";
19
20
  export { VERSION } from "./version.js";
@@ -52,9 +52,31 @@ export interface PageOptions extends TerminalFilter {
52
52
  /**
53
53
  * Can this terminal be used right now: market data, execution, account reads? True only in ACTIVE.
54
54
  * OFFLINE means its cell is not reporting; the platform brings it back by itself, so retry rather
55
- * than create another.
55
+ * than create another. QUEUED means it is placed and waiting for its turn to start: its cell
56
+ * starts terminals a few at a time, and `queue.expected_start_at` says when to expect it.
56
57
  */
57
58
  export declare function isUsable(terminal: Pick<Terminal, "state">): boolean;
59
+ /** Where a QUEUED terminal waits: its place, its expected start and why it waits. */
60
+ export type TerminalQueue = components["schemas"]["TerminalQueue"];
61
+ /** A bulk restart or retire: what was taken, what was refused, and its expected finish. */
62
+ export type BulkAction = components["schemas"]["BulkAction"];
63
+ /** What {@link Terminals.waitUntilActive} takes. */
64
+ export interface WaitOptions {
65
+ /** How long to wait in all, in milliseconds. 15 minutes unless given. */
66
+ readonly timeoutMs?: number;
67
+ }
68
+ /** Which of your terminals a bulk action takes, and why. At least one filter. */
69
+ export interface BulkAsk {
70
+ /** Why; written to the audit log and to each item. */
71
+ readonly reason: string;
72
+ /** Required: the same key sent again returns the same action. */
73
+ readonly idempotencyKey: string;
74
+ readonly farmId?: string;
75
+ readonly brokerId?: string;
76
+ readonly terminalIds?: readonly string[];
77
+ /** Without it, a terminal with open positions is refused and the rest are taken. */
78
+ readonly confirmOpenPositions?: boolean;
79
+ }
58
80
  /** Provision and inspect the terminals this key's tenant owns. */
59
81
  export declare class Terminals {
60
82
  #private;
@@ -79,5 +101,30 @@ export declare class Terminals {
79
101
  * Sent once and never retried: if the answer is lost, send the same idempotency key again.
80
102
  */
81
103
  create(request: CreateTerminal): Promise<Terminal>;
104
+ /**
105
+ * Read the terminal until it is no longer on its way to ACTIVE (REQUESTED, QUEUED, PROVISIONING,
106
+ * LOGGING_IN), or until `timeoutMs` has been waited. Returns the last read either way: ACTIVE,
107
+ * a state it settled in instead (FAILED, for one), or still on its way when the time ran out.
108
+ *
109
+ * A QUEUED terminal is read again at its `queue.expected_start_at` (between 2 s and a minute
110
+ * away), not on a fixed beat: its cell starts terminals a few at a time, so asking every two
111
+ * seconds tells nothing new. Any other state is read again after 2 s, doubling to 15 s.
112
+ */
113
+ waitUntilActive(terminalId: string, options?: WaitOptions): Promise<Terminal>;
114
+ /**
115
+ * Restart many of your terminals at once. Each is judged by the rules of a single restart; a
116
+ * refused one is listed in `refused` with its rule's code and the rest are taken. Each cell
117
+ * carries its share at its own pace, so the terminals already running on it are not slowed.
118
+ * Answered at once with the action and its expected finish; its items never expire.
119
+ */
120
+ bulkRestart(ask: BulkAsk): Promise<BulkAction>;
121
+ /** Retire many of your terminals at once, as {@link bulkRestart} restarts them. */
122
+ bulkRetire(ask: BulkAsk): Promise<BulkAction>;
123
+ /** A bulk action's progress: counts per state, what was refused, and its expected finish. */
124
+ getBulkAction(bulkActionId: string): Promise<BulkAction>;
125
+ /** Cancel a bulk action: its waiting items are cancelled, running ones finish. */
126
+ cancelBulkAction(bulkActionId: string, options: {
127
+ readonly idempotencyKey: string;
128
+ }): Promise<BulkAction>;
82
129
  }
83
130
  export {};
package/dist/terminals.js CHANGED
@@ -39,11 +39,53 @@ function stateQuery(state) {
39
39
  /**
40
40
  * Can this terminal be used right now: market data, execution, account reads? True only in ACTIVE.
41
41
  * OFFLINE means its cell is not reporting; the platform brings it back by itself, so retry rather
42
- * than create another.
42
+ * than create another. QUEUED means it is placed and waiting for its turn to start: its cell
43
+ * starts terminals a few at a time, and `queue.expected_start_at` says when to expect it.
43
44
  */
44
45
  export function isUsable(terminal) {
45
46
  return terminal.state === "ACTIVE";
46
47
  }
48
+ /** The states a terminal passes through on its way to ACTIVE; every other state is settled. */
49
+ const ON_ITS_WAY = new Set([
50
+ "REQUESTED",
51
+ "QUEUED",
52
+ "PROVISIONING",
53
+ "LOGGING_IN",
54
+ ]);
55
+ const POLL_FROM_MS = 2_000;
56
+ const POLL_CAP_MS = 15_000;
57
+ /** A QUEUED terminal is read again at its expected start, but never less often than this. */
58
+ const QUEUED_CAP_MS = 60_000;
59
+ function bulkBody(ask) {
60
+ if (typeof ask.reason !== "string" || ask.reason.trim() === "" || ask.reason.length > 500) {
61
+ throw new RangeError("reason is 1 to 500 characters");
62
+ }
63
+ if (ask.farmId === undefined && ask.brokerId === undefined && ask.terminalIds === undefined) {
64
+ throw new RangeError("a bulk action names farmId, brokerId or terminalIds");
65
+ }
66
+ if (ask.terminalIds !== undefined && ask.terminalIds.length > 1000) {
67
+ throw new RangeError("terminalIds is at most 1000");
68
+ }
69
+ return {
70
+ reason: ask.reason,
71
+ confirm_open_positions: ask.confirmOpenPositions ?? false,
72
+ ...(ask.farmId === undefined ? {} : { farm_id: uuid("farmId", ask.farmId) }),
73
+ ...(ask.brokerId === undefined ? {} : { broker_id: uuid("brokerId", ask.brokerId) }),
74
+ ...(ask.terminalIds === undefined
75
+ ? {}
76
+ : { terminal_ids: ask.terminalIds.map((id) => uuid("terminalIds", id)) }),
77
+ };
78
+ }
79
+ /** How long to wait before the next read of a terminal still on its way. */
80
+ function nextWaitMs(terminal, backoffMs, nowMs) {
81
+ const expected = terminal.state === "QUEUED" ? terminal.queue?.expected_start_at : undefined;
82
+ if (typeof expected === "string") {
83
+ const until = Date.parse(expected) - nowMs;
84
+ if (Number.isFinite(until))
85
+ return Math.min(Math.max(until, POLL_FROM_MS), QUEUED_CAP_MS);
86
+ }
87
+ return Math.min(backoffMs, POLL_CAP_MS);
88
+ }
47
89
  /** Provision and inspect the terminals this key's tenant owns. */
48
90
  export class Terminals {
49
91
  #client;
@@ -129,4 +171,67 @@ export class Terminals {
129
171
  idempotencyKey: key,
130
172
  }));
131
173
  }
174
+ /**
175
+ * Read the terminal until it is no longer on its way to ACTIVE (REQUESTED, QUEUED, PROVISIONING,
176
+ * LOGGING_IN), or until `timeoutMs` has been waited. Returns the last read either way: ACTIVE,
177
+ * a state it settled in instead (FAILED, for one), or still on its way when the time ran out.
178
+ *
179
+ * A QUEUED terminal is read again at its `queue.expected_start_at` (between 2 s and a minute
180
+ * away), not on a fixed beat: its cell starts terminals a few at a time, so asking every two
181
+ * seconds tells nothing new. Any other state is read again after 2 s, doubling to 15 s.
182
+ */
183
+ async waitUntilActive(terminalId, options = {}) {
184
+ const timeoutMs = options.timeoutMs ?? 15 * 60_000;
185
+ if (typeof timeoutMs !== "number" || !(timeoutMs >= 0)) {
186
+ throw new RangeError("timeoutMs is 0 or more");
187
+ }
188
+ let waited = 0;
189
+ let backoff = POLL_FROM_MS;
190
+ let terminal = await this.get(terminalId);
191
+ while (ON_ITS_WAY.has(terminal.state) && waited < timeoutMs) {
192
+ const ms = Math.min(nextWaitMs(terminal, backoff, Date.now()), timeoutMs - waited);
193
+ await this.#client.pause(ms);
194
+ waited += ms;
195
+ backoff *= 2;
196
+ terminal = await this.get(terminalId);
197
+ }
198
+ return terminal;
199
+ }
200
+ /**
201
+ * Restart many of your terminals at once. Each is judged by the rules of a single restart; a
202
+ * refused one is listed in `refused` with its rule's code and the rest are taken. Each cell
203
+ * carries its share at its own pace, so the terminals already running on it are not slowed.
204
+ * Answered at once with the action and its expected finish; its items never expire.
205
+ */
206
+ async bulkRestart(ask) {
207
+ return this.#bulk("/terminals:restart", ask);
208
+ }
209
+ /** Retire many of your terminals at once, as {@link bulkRestart} restarts them. */
210
+ async bulkRetire(ask) {
211
+ return this.#bulk("/terminals:retire", ask);
212
+ }
213
+ /** A bulk action's progress: counts per state, what was refused, and its expected finish. */
214
+ async getBulkAction(bulkActionId) {
215
+ const id = uuid("bulkActionId", bulkActionId);
216
+ return (await this.#client.read({ method: "GET", path: `/bulk-actions/${id}` }));
217
+ }
218
+ /** Cancel a bulk action: its waiting items are cancelled, running ones finish. */
219
+ async cancelBulkAction(bulkActionId, options) {
220
+ const id = uuid("bulkActionId", bulkActionId);
221
+ return (await this.#client.write({
222
+ method: "POST",
223
+ path: `/bulk-actions/${id}/cancel`,
224
+ idempotencyKey: idempotencyKey(options.idempotencyKey),
225
+ }));
226
+ }
227
+ async #bulk(path, ask) {
228
+ const key = idempotencyKey(ask.idempotencyKey);
229
+ const body = bulkBody(ask);
230
+ return (await this.#client.write({
231
+ method: "POST",
232
+ path,
233
+ body,
234
+ idempotencyKey: key,
235
+ }));
236
+ }
132
237
  }
@@ -0,0 +1,44 @@
1
+ import type { MfxClient } from "./client.js";
2
+ import type { components } from "./generated/api.js";
3
+ import { type Page } from "./pages.js";
4
+ /** One trading account at one venue, as the API describes it. */
5
+ export type TradingAccount = components["schemas"]["TradingAccount"];
6
+ /** The trading technology an account is on. */
7
+ export type Venue = TradingAccount["venue"];
8
+ /** An account's state, in words that hold on every venue. */
9
+ export type AccountState = TradingAccount["state"];
10
+ /** Live or demo. */
11
+ export type AccountEnvironment = NonNullable<TradingAccount["environment"]>;
12
+ /** Narrow the list on the platform, before the page is cut. */
13
+ export interface TradingAccountFilter {
14
+ /** Only accounts on this venue, or on one of these. */
15
+ readonly venue?: Venue | readonly Venue[];
16
+ /** Only live, or only demo, accounts. */
17
+ readonly environment?: AccountEnvironment;
18
+ /** Only accounts in this state, or in one of these. */
19
+ readonly state?: AccountState | readonly AccountState[];
20
+ }
21
+ /** What {@link TradingAccounts.page} takes. */
22
+ export interface TradingAccountPageOptions extends TradingAccountFilter {
23
+ /** The `nextCursor` of the page before; leave it out for the first page. */
24
+ readonly cursor?: string;
25
+ /** How many to a page, 1 to 200. */
26
+ readonly limit?: number;
27
+ }
28
+ /** Read the trading accounts this key's tenant holds. Needs `acct:read`. */
29
+ export declare class TradingAccounts {
30
+ #private;
31
+ constructor(client: MfxClient);
32
+ /**
33
+ * One trading account, by its own id or by one of its terminals' ids. Another tenant's, or an id
34
+ * that is not an account of yours, is 404.
35
+ */
36
+ get(accountId: string): Promise<TradingAccount>;
37
+ /** One page of this tenant's trading accounts, newest first. One request. */
38
+ page(options?: TradingAccountPageOptions): Promise<Page<TradingAccount>>;
39
+ /**
40
+ * Every trading account, following the cursor, one request a page. There is no total. The
41
+ * filters narrow the list on the platform, so a filtered page is never short for being filtered.
42
+ */
43
+ list(options?: Omit<TradingAccountPageOptions, "cursor">): AsyncGenerator<TradingAccount, void, undefined>;
44
+ }
@@ -0,0 +1,76 @@
1
+ // `client.tradingAccounts`: the tenant's trading accounts over every venue.
2
+ //
3
+ // One trading account is one account at one venue: an MT5 account (run by one terminal, or two with
4
+ // HA), and from the cTrader release a cTrader account. An MT5 account's id is its terminal's id, so
5
+ // an id you already hold reads its account, and `get` also takes either HA terminal's id. Its state
6
+ // is the venue-neutral word (CONNECTED, DEGRADED, REAUTH_NEEDED, ...); `client.terminals.get` has
7
+ // the MT5 detail.
8
+ //
9
+ // DECISION: a namespace of its own beside `client.accounts`, which stays what it was: the balance,
10
+ // positions and deals of a terminal's account. Folding the two together would give `accounts.get`
11
+ // two meanings.
12
+ import { limit as pageLimit, uuid } from "./arguments.js";
13
+ import { everyItem, pageOf } from "./pages.js";
14
+ function joined(name, value) {
15
+ if (value === undefined)
16
+ return undefined;
17
+ const words = typeof value === "string" ? [value] : value;
18
+ if (words.length === 0)
19
+ throw new RangeError(`${name} is one or more values; got none`);
20
+ for (const word of words) {
21
+ if (typeof word !== "string" || word === "")
22
+ throw new TypeError(`${name} is a list of words`);
23
+ }
24
+ return words.join(",");
25
+ }
26
+ /** Read the trading accounts this key's tenant holds. Needs `acct:read`. */
27
+ export class TradingAccounts {
28
+ #client;
29
+ constructor(client) {
30
+ this.#client = client;
31
+ }
32
+ /**
33
+ * One trading account, by its own id or by one of its terminals' ids. Another tenant's, or an id
34
+ * that is not an account of yours, is 404.
35
+ */
36
+ async get(accountId) {
37
+ const id = uuid("accountId", accountId);
38
+ return (await this.#client.read({ method: "GET", path: `/accounts/${id}` }));
39
+ }
40
+ /** One page of this tenant's trading accounts, newest first. One request. */
41
+ async page(options = {}) {
42
+ const limit = pageLimit(options.limit);
43
+ if (options.cursor !== undefined && typeof options.cursor !== "string") {
44
+ throw new TypeError("cursor is the nextCursor of the page before");
45
+ }
46
+ if (options.environment !== undefined && typeof options.environment !== "string") {
47
+ throw new TypeError("environment is live or demo");
48
+ }
49
+ const body = await this.#client.read({
50
+ method: "GET",
51
+ path: "/accounts",
52
+ query: {
53
+ cursor: options.cursor,
54
+ limit,
55
+ venue: joined("venue", options.venue),
56
+ environment: options.environment,
57
+ state: joined("state", options.state),
58
+ },
59
+ });
60
+ return pageOf(body);
61
+ }
62
+ /**
63
+ * Every trading account, following the cursor, one request a page. There is no total. The
64
+ * filters narrow the list on the platform, so a filtered page is never short for being filtered.
65
+ */
66
+ list(options = {}) {
67
+ const limit = pageLimit(options.limit);
68
+ const rest = {
69
+ ...(limit === undefined ? {} : { limit }),
70
+ ...(options.venue === undefined ? {} : { venue: options.venue }),
71
+ ...(options.environment === undefined ? {} : { environment: options.environment }),
72
+ ...(options.state === undefined ? {} : { state: options.state }),
73
+ };
74
+ return everyItem((cursor) => this.page({ ...rest, ...(cursor === undefined ? {} : { cursor }) }));
75
+ }
76
+ }
package/dist/version.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  /** This package's version. */
2
- export declare const VERSION = "0.2.2";
2
+ export declare const VERSION = "0.3.0";
3
3
  /** What every request says it is, as a product token: the SDK's language and its version. */
4
- export declare const USER_AGENT = "mfx-sdk-typescript/0.2.2";
4
+ export declare const USER_AGENT = "mfx-sdk-typescript/0.3.0";
package/dist/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // This package's version, in one place: exported by the package, and named by every request.
2
2
  /** This package's version. */
3
- export const VERSION = "0.2.2";
3
+ export const VERSION = "0.3.0";
4
4
  /** What every request says it is, as a product token: the SDK's language and its version. */
5
5
  export const USER_AGENT = `mfx-sdk-typescript/${VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mfxhub/sdk",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "Typed TypeScript client for the MFX Hub API: hosted trading terminals, market data, execution and a live event stream",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",