@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/CHANGELOG.md +53 -0
- package/dist/client.d.ts +19 -7
- package/dist/client.js +59 -10
- package/dist/generated/api.d.ts +7914 -3234
- package/dist/index.d.ts +5 -3
- package/dist/index.js +2 -1
- package/dist/terminals.d.ts +48 -1
- package/dist/terminals.js +106 -1
- package/dist/trading-accounts.d.ts +44 -0
- package/dist/trading-accounts.js +76 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,59 @@ Every release of `@mfxhub/sdk`, newest first. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.3.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- A terminal can be `QUEUED`: placed on a cell and waiting for its turn to start. A cell starts
|
|
12
|
+
terminals a few at a time so the ones already running are not slowed. `terminal.queue` says its
|
|
13
|
+
place, when it is expected to start (`expected_start_at`) and why it waits (a coarse `reason`).
|
|
14
|
+
`isUsable` is false for it, as for every state but ACTIVE.
|
|
15
|
+
- `terminals.waitUntilActive(id, { timeoutMs })`: reads the terminal until it is no longer on its
|
|
16
|
+
way to ACTIVE. A QUEUED terminal is read again at its expected start (2 s to 1 min away), not on
|
|
17
|
+
a fixed beat.
|
|
18
|
+
- `terminals.bulkRestart` and `terminals.bulkRetire` (`POST /v1/terminals:restart`,
|
|
19
|
+
`POST /v1/terminals:retire`): many terminals by farm, broker or ids, answered at once with the
|
|
20
|
+
action, what was refused and the expected finish. `getBulkAction` and `cancelBulkAction` follow
|
|
21
|
+
and cancel it.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- The vendored API description is the platform's current public contract, with the queued state,
|
|
26
|
+
the queue fields and the bulk routes; the generated types follow it.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- A request path whose first segment holds a colon (`/terminals:restart`) is sent to the API. The
|
|
31
|
+
URL parser read `terminals:` as a scheme.
|
|
32
|
+
|
|
33
|
+
## 0.2.4
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- `client.tradingAccounts`: the trading accounts over every venue (`GET /v1/accounts`,
|
|
38
|
+
`GET /v1/accounts/{id}`, scope `acct:read`). `page()` and `list()` narrow by `venue`,
|
|
39
|
+
`environment` and `state` on the platform; `get(id)` takes the account's id or one of its
|
|
40
|
+
terminals' ids. An MT5 account's id is its terminal's id. The state is the venue-neutral word
|
|
41
|
+
(`CONNECTED`, `DEGRADED`, `REAUTH_NEEDED`, ...). `client.accounts` is unchanged: it is still the
|
|
42
|
+
balance, positions and deals of a terminal's account.
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- The vendored API description is the platform's current public contract, and the generated types
|
|
47
|
+
follow it.
|
|
48
|
+
|
|
49
|
+
## 0.2.3
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- Every HTTP attempt is timed. `new MfxClient(..., { onCall })` is told each one as a `CallTiming`
|
|
54
|
+
(`what`, `status`, `ms`; `status` is `null` when no answer came), and when the environment
|
|
55
|
+
variable `MFX_CALL_LOG` names a file, one line `{"status": 200, "ms": 12.5}` is appended to it
|
|
56
|
+
for each. A read retried after a 429 is two attempts. Nothing else is written: no path, id,
|
|
57
|
+
header or key. A listener that throws, or a file that cannot be written, never fails a call.
|
|
58
|
+
The platform's checks use it to count and time a sample's calls, as they do for the Python SDK.
|
|
59
|
+
|
|
7
60
|
## 0.2.2
|
|
8
61
|
|
|
9
62
|
### Fixed
|
package/dist/client.d.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `MfxClient`: one signed connection to the MFX Hub API.
|
|
3
|
-
*
|
|
4
|
-
* Every request is signed with the tenant's Ed25519 key. Reads that are rate limited are retried
|
|
5
|
-
* after the platform's own `Retry-After`; writes are never retried, because a retried write is a
|
|
6
|
-
* second order. Failures become typed errors from `errors.ts`.
|
|
7
|
-
*/
|
|
8
1
|
import { MfxApiError } from "./errors.js";
|
|
9
2
|
import { RequestSigner, type SigningKey } from "./signing.js";
|
|
10
3
|
import { Accounts } from "./accounts.js";
|
|
@@ -12,10 +5,23 @@ import { Catalog } from "./catalog.js";
|
|
|
12
5
|
import { Execution } from "./execution.js";
|
|
13
6
|
import { MfxStream, type StreamOptions } from "./stream.js";
|
|
14
7
|
import { Terminals } from "./terminals.js";
|
|
8
|
+
import { TradingAccounts } from "./trading-accounts.js";
|
|
15
9
|
/** Default time a single request may take. */
|
|
16
10
|
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
17
11
|
/** How many times a rate-limited read is tried again. */
|
|
18
12
|
export declare const DEFAULT_READ_RETRIES = 3;
|
|
13
|
+
/** The environment variable naming a file to append one JSON line to for every HTTP attempt. */
|
|
14
|
+
export declare const CALL_LOG_ENV = "MFX_CALL_LOG";
|
|
15
|
+
/**
|
|
16
|
+
* One HTTP attempt the client made: what was asked, the status, and how long it took.
|
|
17
|
+
* Each attempt is a call: a read retried after a 429 is two. `status` is null when no answer came
|
|
18
|
+
* (a connection that failed, a timeout). `ms` is measured on the client's own clock.
|
|
19
|
+
*/
|
|
20
|
+
export interface CallTiming {
|
|
21
|
+
readonly what: string;
|
|
22
|
+
readonly status: number | null;
|
|
23
|
+
readonly ms: number;
|
|
24
|
+
}
|
|
19
25
|
/** What the key had left when MFX Hub last answered. */
|
|
20
26
|
export interface Budget {
|
|
21
27
|
readonly remaining: number;
|
|
@@ -35,6 +41,10 @@ export interface ClientOptions {
|
|
|
35
41
|
readonly clock?: () => number;
|
|
36
42
|
/** A number in [0, 1) for the backoff's jitter; replaceable in tests. */
|
|
37
43
|
readonly random?: () => number;
|
|
44
|
+
/** Told each call as it ends ({@link CallTiming}); `MFX_CALL_LOG` is the same, as a file. */
|
|
45
|
+
readonly onCall?: (timing: CallTiming) => void;
|
|
46
|
+
/** A monotonic clock in milliseconds; replaceable in tests. */
|
|
47
|
+
readonly now?: () => number;
|
|
38
48
|
}
|
|
39
49
|
/** One request, as the resource modules describe it. */
|
|
40
50
|
export interface RequestSpec {
|
|
@@ -64,6 +74,8 @@ export declare class MfxClient {
|
|
|
64
74
|
readonly catalog: Catalog;
|
|
65
75
|
/** The trading account behind a terminal: figures, positions, pending orders and deals. */
|
|
66
76
|
readonly accounts: Accounts;
|
|
77
|
+
/** The trading accounts over every venue: an MT5 account, and later a cTrader one. */
|
|
78
|
+
readonly tradingAccounts: TradingAccounts;
|
|
67
79
|
/**
|
|
68
80
|
* @param baseUrl the API's base, including the version: `https://api.mfxhub.com/v1`, or
|
|
69
81
|
* `https://api.dev.mfxhub.com/v1` for development
|
package/dist/client.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* after the platform's own `Retry-After`; writes are never retried, because a retried write is a
|
|
6
6
|
* second order. Failures become typed errors from `errors.ts`.
|
|
7
7
|
*/
|
|
8
|
+
import { appendFileSync } from "node:fs";
|
|
8
9
|
import { MfxApiError, MfxTimeout, MfxTransportError, errorFor, retryAfterSeconds, } from "./errors.js";
|
|
9
10
|
import { RequestSigner } from "./signing.js";
|
|
10
11
|
import { Accounts } from "./accounts.js";
|
|
@@ -12,6 +13,7 @@ import { Catalog } from "./catalog.js";
|
|
|
12
13
|
import { Execution } from "./execution.js";
|
|
13
14
|
import { MfxStream } from "./stream.js";
|
|
14
15
|
import { Terminals } from "./terminals.js";
|
|
16
|
+
import { TradingAccounts } from "./trading-accounts.js";
|
|
15
17
|
import { USER_AGENT } from "./version.js";
|
|
16
18
|
/** Default time a single request may take. */
|
|
17
19
|
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -28,6 +30,8 @@ const NOT_SENT = new Set([
|
|
|
28
30
|
"EHOSTUNREACH",
|
|
29
31
|
"UND_ERR_CONNECT_TIMEOUT",
|
|
30
32
|
]);
|
|
33
|
+
/** The environment variable naming a file to append one JSON line to for every HTTP attempt. */
|
|
34
|
+
export const CALL_LOG_ENV = "MFX_CALL_LOG";
|
|
31
35
|
const wait = (ms) => new Promise((resolve) => {
|
|
32
36
|
setTimeout(resolve, ms);
|
|
33
37
|
});
|
|
@@ -40,6 +44,9 @@ export class MfxClient {
|
|
|
40
44
|
#sleep;
|
|
41
45
|
#fetch;
|
|
42
46
|
#random;
|
|
47
|
+
#onCall;
|
|
48
|
+
#now;
|
|
49
|
+
#callLog;
|
|
43
50
|
#budget = null;
|
|
44
51
|
/** The terminals this key's tenant owns. */
|
|
45
52
|
terminals;
|
|
@@ -49,6 +56,8 @@ export class MfxClient {
|
|
|
49
56
|
catalog;
|
|
50
57
|
/** The trading account behind a terminal: figures, positions, pending orders and deals. */
|
|
51
58
|
accounts;
|
|
59
|
+
/** The trading accounts over every venue: an MT5 account, and later a cTrader one. */
|
|
60
|
+
tradingAccounts;
|
|
52
61
|
/**
|
|
53
62
|
* @param baseUrl the API's base, including the version: `https://api.mfxhub.com/v1`, or
|
|
54
63
|
* `https://api.dev.mfxhub.com/v1` for development
|
|
@@ -63,10 +72,14 @@ export class MfxClient {
|
|
|
63
72
|
this.#sleep = options.sleep ?? wait;
|
|
64
73
|
this.#fetch = options.fetch ?? fetch;
|
|
65
74
|
this.#random = options.random ?? Math.random;
|
|
75
|
+
this.#onCall = options.onCall;
|
|
76
|
+
this.#now = options.now ?? (() => performance.now());
|
|
77
|
+
this.#callLog = process.env[CALL_LOG_ENV] || undefined;
|
|
66
78
|
this.terminals = new Terminals(this);
|
|
67
79
|
this.execution = new Execution(this);
|
|
68
80
|
this.catalog = new Catalog(this);
|
|
69
81
|
this.accounts = new Accounts(this);
|
|
82
|
+
this.tradingAccounts = new TradingAccounts(this);
|
|
70
83
|
}
|
|
71
84
|
/** The id of the key this client signs with. Safe to log, unlike the private key. */
|
|
72
85
|
get keyId() {
|
|
@@ -124,7 +137,9 @@ export class MfxClient {
|
|
|
124
137
|
}
|
|
125
138
|
/** One attempt, signed afresh. Transport faults become MfxTransportError or MfxTimeout. */
|
|
126
139
|
async send(spec) {
|
|
127
|
-
|
|
140
|
+
// "./" keeps a path with a colon in its first segment (`/terminals:restart`) relative: without
|
|
141
|
+
// it the URL parser reads `terminals:` as a scheme and the request leaves the API.
|
|
142
|
+
const url = new URL("./" + spec.path.replace(/^\/+/, ""), this.#base);
|
|
128
143
|
for (const [name, value] of Object.entries(spec.query ?? {})) {
|
|
129
144
|
if (value === null || value === undefined)
|
|
130
145
|
continue;
|
|
@@ -146,18 +161,29 @@ export class MfxClient {
|
|
|
146
161
|
headers["Content-Type"] = "application/json";
|
|
147
162
|
if (spec.idempotencyKey !== undefined)
|
|
148
163
|
headers["Idempotency-Key"] = spec.idempotencyKey;
|
|
149
|
-
|
|
164
|
+
const started = this.#now();
|
|
165
|
+
let status = null;
|
|
150
166
|
try {
|
|
151
|
-
response
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
167
|
+
let response;
|
|
168
|
+
try {
|
|
169
|
+
response = await this.#fetch(url, {
|
|
170
|
+
method: spec.method,
|
|
171
|
+
headers,
|
|
172
|
+
...(spec.body === undefined ? {} : { body }),
|
|
173
|
+
signal: AbortSignal.timeout(this.#timeoutMs),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
throw transportError(error, spec);
|
|
178
|
+
}
|
|
179
|
+
status = response.status;
|
|
180
|
+
return await this.#answer(response);
|
|
157
181
|
}
|
|
158
|
-
|
|
159
|
-
|
|
182
|
+
finally {
|
|
183
|
+
this.#timed(`${spec.method} ${spec.path}`, status, started);
|
|
160
184
|
}
|
|
185
|
+
}
|
|
186
|
+
async #answer(response) {
|
|
161
187
|
const text = await response.text();
|
|
162
188
|
this.#noteBudget(response.headers);
|
|
163
189
|
let parsed = null;
|
|
@@ -171,6 +197,29 @@ export class MfxClient {
|
|
|
171
197
|
}
|
|
172
198
|
return { status: response.status, headers: response.headers, body: parsed };
|
|
173
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Hand one call's timing on. Never throws: timing is an observation, not a reason for a call
|
|
202
|
+
* that succeeded to fail. Nothing but the status and the time is written: no path, id, header
|
|
203
|
+
* or key.
|
|
204
|
+
*/
|
|
205
|
+
#timed(what, status, started) {
|
|
206
|
+
const timing = { what, status, ms: this.#now() - started };
|
|
207
|
+
try {
|
|
208
|
+
this.#onCall?.(timing);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// A listener's bug must not fail the call.
|
|
212
|
+
}
|
|
213
|
+
if (this.#callLog !== undefined) {
|
|
214
|
+
try {
|
|
215
|
+
const ms = Math.round(timing.ms * 1000) / 1000;
|
|
216
|
+
appendFileSync(this.#callLog, JSON.stringify({ status, ms }) + "\n", "utf8");
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// An unwritable log is no reason to fail a call.
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
174
223
|
#noteBudget(headers) {
|
|
175
224
|
const remaining = headers.get("X-MFX-Budget-Remaining");
|
|
176
225
|
if (remaining !== null && Number.isFinite(Number(remaining))) {
|