@mfxhub/sdk 0.0.0 → 0.1.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/client.js ADDED
@@ -0,0 +1,200 @@
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
+ import { MfxApiError, MfxTimeout, MfxTransportError, errorFor, retryAfterSeconds, } from "./errors.js";
9
+ import { RequestSigner } from "./signing.js";
10
+ import { Catalog } from "./catalog.js";
11
+ import { Execution } from "./execution.js";
12
+ import { MfxStream } from "./stream.js";
13
+ import { Terminals } from "./terminals.js";
14
+ /** Default time a single request may take. */
15
+ export const DEFAULT_TIMEOUT_MS = 30_000;
16
+ /** How many times a rate-limited read is tried again. */
17
+ export const DEFAULT_READ_RETRIES = 3;
18
+ /** The longest the client waits between attempts when the platform names no delay. */
19
+ const BACKOFF_CAP_MS = 30_000;
20
+ /** Connection-phase failures: the request was never sent. */
21
+ const NOT_SENT = new Set([
22
+ "ECONNREFUSED",
23
+ "ENOTFOUND",
24
+ "EAI_AGAIN",
25
+ "ENETUNREACH",
26
+ "EHOSTUNREACH",
27
+ "UND_ERR_CONNECT_TIMEOUT",
28
+ ]);
29
+ const wait = (ms) => new Promise((resolve) => {
30
+ setTimeout(resolve, ms);
31
+ });
32
+ /** A tenant's connection to MFX Hub. */
33
+ export class MfxClient {
34
+ #signer;
35
+ #base;
36
+ #timeoutMs;
37
+ #readRetries;
38
+ #sleep;
39
+ #fetch;
40
+ #random;
41
+ #budget = null;
42
+ /** The terminals this key's tenant owns. */
43
+ terminals;
44
+ /** Order entry. Every call resolves to a result; none rejects. */
45
+ execution;
46
+ /** Brokers, symbols and quotes. */
47
+ catalog;
48
+ /**
49
+ * @param baseUrl the API's base, including the version: `https://api.mfxhub.com/v1`, or
50
+ * `https://api.dev.mfxhub.com/v1` for development
51
+ * @param keyId the API key's id
52
+ * @param privateKey its private key: PEM, the base64 of its 32 bytes, or a SigningKey
53
+ */
54
+ constructor(baseUrl, keyId, privateKey, options = {}) {
55
+ this.#base = new URL(baseUrl.replace(/\/+$/, "") + "/");
56
+ this.#signer = new RequestSigner(keyId, privateKey, options.clock);
57
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
58
+ this.#readRetries = options.readRetries ?? DEFAULT_READ_RETRIES;
59
+ this.#sleep = options.sleep ?? wait;
60
+ this.#fetch = options.fetch ?? fetch;
61
+ this.#random = options.random ?? Math.random;
62
+ this.terminals = new Terminals(this);
63
+ this.execution = new Execution(this);
64
+ this.catalog = new Catalog(this);
65
+ }
66
+ /** The id of the key this client signs with. Safe to log, unlike the private key. */
67
+ get keyId() {
68
+ return this.#signer.keyId;
69
+ }
70
+ /** The REST base this client was pointed at. The stream's address is derived from it. */
71
+ get baseUrl() {
72
+ return this.#base.href.replace(/\/$/, "");
73
+ }
74
+ /** What the key had left when MFX Hub last answered; null before the first answer. */
75
+ get budget() {
76
+ return this.#budget;
77
+ }
78
+ /** The signer, for the stream's upgrade. Not part of the public surface. */
79
+ get signer() {
80
+ return this.#signer;
81
+ }
82
+ /**
83
+ * Jittered exponential backoff for an attempt, when the platform gave no delay: up to 1 s, 2 s,
84
+ * 4 s ... capped at 30 s. The reads and the stream's reconnects share it, so they lean on this
85
+ * tenant's quota at the same rate.
86
+ */
87
+ backoffMs(attempt) {
88
+ return Math.round(this.#random() * Math.min(1000 * 2 ** attempt, BACKOFF_CAP_MS));
89
+ }
90
+ /**
91
+ * The multiplexed event stream, opened: subscribe, then iterate. Close it when done.
92
+ * Reconnects underneath, resuming what replays; see {@link MfxStream}.
93
+ */
94
+ stream(options = {}) {
95
+ return new MfxStream(this, options).open();
96
+ }
97
+ /** How the client waits; the stream uses it between reconnects. */
98
+ pause(ms) {
99
+ return this.#sleep(ms);
100
+ }
101
+ /** A read: retried after a 429 with the platform's own delay; a failure throws its typed error. */
102
+ async read(spec) {
103
+ for (let attempt = 0;; attempt++) {
104
+ const answer = await this.send(spec);
105
+ if (answer.status === 429 && attempt < this.#readRetries) {
106
+ const told = retryAfterSeconds(answer.headers.get("Retry-After"));
107
+ await this.#sleep(told === null ? this.backoffMs(attempt) : 1000 * told);
108
+ continue;
109
+ }
110
+ throwFor(answer, spec);
111
+ return answer.body;
112
+ }
113
+ }
114
+ /** A write: sent once, never retried; a failure throws its typed error. */
115
+ async write(spec) {
116
+ const answer = await this.send(spec);
117
+ throwFor(answer, spec);
118
+ return answer.body;
119
+ }
120
+ /** One attempt, signed afresh. Transport faults become MfxTransportError or MfxTimeout. */
121
+ async send(spec) {
122
+ const url = new URL(spec.path.replace(/^\//, ""), this.#base);
123
+ for (const [name, value] of Object.entries(spec.query ?? {})) {
124
+ if (value !== null && value !== undefined)
125
+ url.searchParams.set(name, String(value));
126
+ }
127
+ const body = spec.body === undefined ? new Uint8Array() : Buffer.from(JSON.stringify(spec.body));
128
+ const headers = {
129
+ Accept: "application/json",
130
+ ...this.#signer.headersFor(spec.method, url.pathname + url.search, body),
131
+ };
132
+ if (spec.body !== undefined)
133
+ headers["Content-Type"] = "application/json";
134
+ if (spec.idempotencyKey !== undefined)
135
+ headers["Idempotency-Key"] = spec.idempotencyKey;
136
+ let response;
137
+ try {
138
+ response = await this.#fetch(url, {
139
+ method: spec.method,
140
+ headers,
141
+ ...(spec.body === undefined ? {} : { body }),
142
+ signal: AbortSignal.timeout(this.#timeoutMs),
143
+ });
144
+ }
145
+ catch (error) {
146
+ throw transportError(error, spec);
147
+ }
148
+ const text = await response.text();
149
+ this.#noteBudget(response.headers);
150
+ let parsed = null;
151
+ if (text !== "") {
152
+ try {
153
+ parsed = JSON.parse(text);
154
+ }
155
+ catch {
156
+ parsed = text;
157
+ }
158
+ }
159
+ return { status: response.status, headers: response.headers, body: parsed };
160
+ }
161
+ #noteBudget(headers) {
162
+ const remaining = headers.get("X-MFX-Budget-Remaining");
163
+ if (remaining !== null && Number.isFinite(Number(remaining))) {
164
+ this.#budget = {
165
+ remaining: Number(remaining),
166
+ source: headers.get("X-MFX-Budget-Source") ?? "",
167
+ };
168
+ }
169
+ }
170
+ toString() {
171
+ return `MfxClient(${this.baseUrl}, ${this.keyId})`;
172
+ }
173
+ }
174
+ /** The typed error a failed answer means, or nothing for a success. */
175
+ export function throwFor(answer, spec) {
176
+ if (answer.status < 400)
177
+ return;
178
+ const body = answer.body;
179
+ const error = typeof body === "object" && body !== null
180
+ ? body.error
181
+ : undefined;
182
+ const code = typeof error?.code === "string" ? error.code : "";
183
+ const message = typeof error?.message === "string" ? error.message : `${spec.method} ${spec.path} failed`;
184
+ const details = typeof error?.details === "object" && error.details !== null
185
+ ? error.details
186
+ : {};
187
+ throw errorFor(code, message, answer.status, details, retryAfterSeconds(answer.headers.get("Retry-After")));
188
+ }
189
+ function transportError(error, spec) {
190
+ const what = `${spec.method} ${spec.path}`;
191
+ if (error instanceof DOMException &&
192
+ (error.name === "TimeoutError" || error.name === "AbortError")) {
193
+ return new MfxTimeout(`${what}: no answer in time`);
194
+ }
195
+ const cause = error?.cause;
196
+ const code = typeof cause?.code === "string" ? cause.code : "";
197
+ const text = error instanceof Error ? error.message : String(error);
198
+ return new MfxTransportError(`${what}: ${text}${code ? ` (${code})` : ""}`, !NOT_SENT.has(code));
199
+ }
200
+ export { MfxApiError };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * One non-negative decimal as the contract writes it: plain digits, no sign, no exponent, no
3
+ * leading zeros (`1E+3` is `"1000"`, `1.5e-3` is `"0.0015"`). Digits are never rounded or dropped.
4
+ *
5
+ * @param name the argument's name, for the message
6
+ * @param value a decimal string, such as `"0.01"`
7
+ */
8
+ export declare function decimal(name: string, value: unknown): string;
9
+ /** A decimal the API sent, kept as its string; anything else reads as absent. */
10
+ export declare function decimalOut(value: unknown): string | null;
@@ -0,0 +1,51 @@
1
+ // Prices, volumes and money are decimal strings, in and out.
2
+ //
3
+ // A JavaScript `number` is a binary float: 0.07 * 3 is 0.21000000000000002, and position sizing is
4
+ // arithmetic, so a computed volume is the ordinary case. Taking a number would put that error on
5
+ // the wire. So a number is refused before anything is sent, with a message that says what to pass.
6
+ const DECIMAL = /^\+?(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/;
7
+ /** No price or volume needs more; a larger exponent would only build a huge string. */
8
+ const MAX_EXPONENT = 100;
9
+ /**
10
+ * One non-negative decimal as the contract writes it: plain digits, no sign, no exponent, no
11
+ * leading zeros (`1E+3` is `"1000"`, `1.5e-3` is `"0.0015"`). Digits are never rounded or dropped.
12
+ *
13
+ * @param name the argument's name, for the message
14
+ * @param value a decimal string, such as `"0.01"`
15
+ */
16
+ export function decimal(name, value) {
17
+ if (typeof value === "number") {
18
+ throw new TypeError(`${name} is a decimal string, not a number: a computed number carries binary error ` +
19
+ `(0.07 * 3 is 0.21000000000000002), and MFX Hub takes decimals as strings so nothing is ` +
20
+ `lost. Pass "${String(value)}" only if that is exactly the value you mean.`);
21
+ }
22
+ if (typeof value !== "string") {
23
+ throw new TypeError(`${name} is a decimal string; got ${typeof value}`);
24
+ }
25
+ const match = DECIMAL.exec(value.trim());
26
+ const whole = match?.[1] ?? "";
27
+ const fraction = match?.[2] ?? "";
28
+ if (match === null || whole + fraction === "") {
29
+ throw new RangeError(`${name} is not a non-negative decimal: ${JSON.stringify(value)}`);
30
+ }
31
+ const exponent = Number(match[3] ?? "0");
32
+ if (Math.abs(exponent) > MAX_EXPONENT) {
33
+ throw new RangeError(`${name} has an exponent beyond ±${MAX_EXPONENT}: ${value}`);
34
+ }
35
+ const digits = whole + fraction;
36
+ // Where the decimal point falls in `digits` once the exponent is applied.
37
+ const point = whole.length + exponent;
38
+ const padded = point <= 0
39
+ ? "0".repeat(1 - point) + digits
40
+ : point > digits.length
41
+ ? digits + "0".repeat(point - digits.length)
42
+ : digits;
43
+ const at = point <= 0 ? 1 : point;
44
+ const intPart = padded.slice(0, at).replace(/^0+(?=\d)/, "");
45
+ const fracPart = padded.slice(at);
46
+ return fracPart === "" ? intPart : `${intPart}.${fracPart}`;
47
+ }
48
+ /** A decimal the API sent, kept as its string; anything else reads as absent. */
49
+ export function decimalOut(value) {
50
+ return typeof value === "string" && value !== "" ? value : null;
51
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Every failure a read can raise, typed by the platform's `error.code`.
3
+ *
4
+ * The code decides; the HTTP status only when the code is one this version does not know, so a
5
+ * code the platform adds later still arrives as a typed error rather than failing the caller.
6
+ * Execution methods never raise: they return an `ExecutionResult` instead.
7
+ */
8
+ /** Every failure this SDK raises. */
9
+ export declare class MfxError extends Error {
10
+ name: string;
11
+ }
12
+ /**
13
+ * The request never got an answer: DNS, TCP, TLS, a dropped connection.
14
+ *
15
+ * `mayHaveReached` is false only when the connection never opened, so nothing was sent. A
16
+ * connection that dropped after the request was written may have delivered it.
17
+ */
18
+ export declare class MfxTransportError extends MfxError {
19
+ readonly mayHaveReached: boolean;
20
+ name: string;
21
+ constructor(message: string, mayHaveReached?: boolean);
22
+ }
23
+ /** No answer in time. Never retried: the request may have arrived. */
24
+ export declare class MfxTimeout extends MfxTransportError {
25
+ name: string;
26
+ }
27
+ /** The platform answered with an error. */
28
+ export declare class MfxApiError extends MfxError {
29
+ /** The HTTP status; 0 for an error sent on the stream. */
30
+ readonly status: number;
31
+ /** The platform's error code, as it arrived. */
32
+ readonly code: string;
33
+ /** What the platform said about it, such as the field that was wrong. */
34
+ readonly details: Readonly<Record<string, unknown>>;
35
+ /** How long the platform asked the caller to wait, from Retry-After. */
36
+ readonly retryAfterS: number | null;
37
+ name: string;
38
+ constructor(message: string,
39
+ /** The HTTP status; 0 for an error sent on the stream. */
40
+ status: number,
41
+ /** The platform's error code, as it arrived. */
42
+ code: string,
43
+ /** What the platform said about it, such as the field that was wrong. */
44
+ details?: Readonly<Record<string, unknown>>,
45
+ /** How long the platform asked the caller to wait, from Retry-After. */
46
+ retryAfterS?: number | null);
47
+ }
48
+ /** The request cannot be taken as it was sent; `details` says which part. */
49
+ export declare class ValidationError extends MfxApiError {
50
+ name: string;
51
+ }
52
+ /** The request is not signed by a key the platform knows. */
53
+ export declare class AuthError extends MfxApiError {
54
+ name: string;
55
+ }
56
+ /** The key may not do this. */
57
+ export declare class ForbiddenError extends MfxApiError {
58
+ name: string;
59
+ }
60
+ /** The operator has disabled the tenant. */
61
+ export declare class TenantDisabledError extends ForbiddenError {
62
+ name: string;
63
+ }
64
+ /** The operator has suspended the tenant. */
65
+ export declare class TenantSuspendedError extends ForbiddenError {
66
+ name: string;
67
+ }
68
+ /** A planned maintenance window; `details.ends_at` says when it ends. */
69
+ export declare class MaintenanceError extends ForbiddenError {
70
+ name: string;
71
+ }
72
+ /** No such thing; also the answer for another tenant's id. */
73
+ export declare class NotFoundError extends MfxApiError {
74
+ name: string;
75
+ }
76
+ /** The request conflicts with what already exists. */
77
+ export declare class ConflictError extends MfxApiError {
78
+ name: string;
79
+ }
80
+ /** The thing exists and is not in a state this can be done in. */
81
+ export declare class StateError extends ConflictError {
82
+ name: string;
83
+ }
84
+ /** The terminal cannot be placed. */
85
+ export declare class PlacementError extends ConflictError {
86
+ name: string;
87
+ }
88
+ /** MFX Hub does not support this broker or server now; the message says why. */
89
+ export declare class UnsupportedBrokerError extends ConflictError {
90
+ name: string;
91
+ }
92
+ /** Too many requests; `retryAfterS` says how long to wait. */
93
+ export declare class RateLimitError extends MfxApiError {
94
+ name: string;
95
+ }
96
+ /** The tenant's terminal or broker did not answer. */
97
+ export declare class UpstreamError extends MfxApiError {
98
+ name: string;
99
+ }
100
+ /** A store of the platform's own did not answer; `retryAfterS` says when to try again. */
101
+ export declare class DependencyUnavailableError extends UpstreamError {
102
+ name: string;
103
+ }
104
+ /** An answer from the network's edge, not from MFX Hub (a 502 or 504). */
105
+ export declare class EdgeError extends UpstreamError {
106
+ name: string;
107
+ }
108
+ /** MFX Hub failed, and it is not the caller's request that is wrong. */
109
+ export declare class InternalError extends MfxApiError {
110
+ name: string;
111
+ }
112
+ /** The typed error for a failure the platform described, on a call's answer or the stream. */
113
+ export declare function errorFor(code: string, message: string, status: number, details?: Readonly<Record<string, unknown>>, retryAfterS?: number | null): MfxApiError;
114
+ /** Seconds from a Retry-After header, or null when there is none or it cannot be read. */
115
+ export declare function retryAfterSeconds(header: string | null): number | null;
package/dist/errors.js ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Every failure a read can raise, typed by the platform's `error.code`.
3
+ *
4
+ * The code decides; the HTTP status only when the code is one this version does not know, so a
5
+ * code the platform adds later still arrives as a typed error rather than failing the caller.
6
+ * Execution methods never raise: they return an `ExecutionResult` instead.
7
+ */
8
+ /** Every failure this SDK raises. */
9
+ export class MfxError extends Error {
10
+ name = "MfxError";
11
+ }
12
+ /**
13
+ * The request never got an answer: DNS, TCP, TLS, a dropped connection.
14
+ *
15
+ * `mayHaveReached` is false only when the connection never opened, so nothing was sent. A
16
+ * connection that dropped after the request was written may have delivered it.
17
+ */
18
+ export class MfxTransportError extends MfxError {
19
+ mayHaveReached;
20
+ name = "MfxTransportError";
21
+ constructor(message, mayHaveReached = true) {
22
+ super(message);
23
+ this.mayHaveReached = mayHaveReached;
24
+ }
25
+ }
26
+ /** No answer in time. Never retried: the request may have arrived. */
27
+ export class MfxTimeout extends MfxTransportError {
28
+ name = "MfxTimeout";
29
+ }
30
+ /** The platform answered with an error. */
31
+ export class MfxApiError extends MfxError {
32
+ status;
33
+ code;
34
+ details;
35
+ retryAfterS;
36
+ name = "MfxApiError";
37
+ constructor(message,
38
+ /** The HTTP status; 0 for an error sent on the stream. */
39
+ status,
40
+ /** The platform's error code, as it arrived. */
41
+ code,
42
+ /** What the platform said about it, such as the field that was wrong. */
43
+ details = {},
44
+ /** How long the platform asked the caller to wait, from Retry-After. */
45
+ retryAfterS = null) {
46
+ super(message);
47
+ this.status = status;
48
+ this.code = code;
49
+ this.details = details;
50
+ this.retryAfterS = retryAfterS;
51
+ }
52
+ }
53
+ /** The request cannot be taken as it was sent; `details` says which part. */
54
+ export class ValidationError extends MfxApiError {
55
+ name = "ValidationError";
56
+ }
57
+ /** The request is not signed by a key the platform knows. */
58
+ export class AuthError extends MfxApiError {
59
+ name = "AuthError";
60
+ }
61
+ /** The key may not do this. */
62
+ export class ForbiddenError extends MfxApiError {
63
+ name = "ForbiddenError";
64
+ }
65
+ /** The operator has disabled the tenant. */
66
+ export class TenantDisabledError extends ForbiddenError {
67
+ name = "TenantDisabledError";
68
+ }
69
+ /** The operator has suspended the tenant. */
70
+ export class TenantSuspendedError extends ForbiddenError {
71
+ name = "TenantSuspendedError";
72
+ }
73
+ /** A planned maintenance window; `details.ends_at` says when it ends. */
74
+ export class MaintenanceError extends ForbiddenError {
75
+ name = "MaintenanceError";
76
+ }
77
+ /** No such thing; also the answer for another tenant's id. */
78
+ export class NotFoundError extends MfxApiError {
79
+ name = "NotFoundError";
80
+ }
81
+ /** The request conflicts with what already exists. */
82
+ export class ConflictError extends MfxApiError {
83
+ name = "ConflictError";
84
+ }
85
+ /** The thing exists and is not in a state this can be done in. */
86
+ export class StateError extends ConflictError {
87
+ name = "StateError";
88
+ }
89
+ /** The terminal cannot be placed. */
90
+ export class PlacementError extends ConflictError {
91
+ name = "PlacementError";
92
+ }
93
+ /** MFX Hub does not support this broker or server now; the message says why. */
94
+ export class UnsupportedBrokerError extends ConflictError {
95
+ name = "UnsupportedBrokerError";
96
+ }
97
+ /** Too many requests; `retryAfterS` says how long to wait. */
98
+ export class RateLimitError extends MfxApiError {
99
+ name = "RateLimitError";
100
+ }
101
+ /** The tenant's terminal or broker did not answer. */
102
+ export class UpstreamError extends MfxApiError {
103
+ name = "UpstreamError";
104
+ }
105
+ /** A store of the platform's own did not answer; `retryAfterS` says when to try again. */
106
+ export class DependencyUnavailableError extends UpstreamError {
107
+ name = "DependencyUnavailableError";
108
+ }
109
+ /** An answer from the network's edge, not from MFX Hub (a 502 or 504). */
110
+ export class EdgeError extends UpstreamError {
111
+ name = "EdgeError";
112
+ }
113
+ /** MFX Hub failed, and it is not the caller's request that is wrong. */
114
+ export class InternalError extends MfxApiError {
115
+ name = "InternalError";
116
+ }
117
+ const BY_CODE = {
118
+ validation_error: ValidationError,
119
+ auth_error: AuthError,
120
+ forbidden: ForbiddenError,
121
+ tenant_disabled: TenantDisabledError,
122
+ tenant_suspended: TenantSuspendedError,
123
+ maintenance: MaintenanceError,
124
+ not_found: NotFoundError,
125
+ conflict: ConflictError,
126
+ state_error: StateError,
127
+ placement_error: PlacementError,
128
+ broker_not_supported: UnsupportedBrokerError,
129
+ rate_limited: RateLimitError,
130
+ upstream_error: UpstreamError,
131
+ dependency_unavailable: DependencyUnavailableError,
132
+ internal_error: InternalError,
133
+ };
134
+ const BY_STATUS = {
135
+ 400: ValidationError,
136
+ 401: AuthError,
137
+ 403: ForbiddenError,
138
+ 404: NotFoundError,
139
+ 409: ConflictError,
140
+ 413: ValidationError,
141
+ 422: ValidationError,
142
+ 429: RateLimitError,
143
+ 500: InternalError,
144
+ 502: EdgeError,
145
+ 503: UpstreamError,
146
+ 504: EdgeError,
147
+ };
148
+ /** The typed error for a failure the platform described, on a call's answer or the stream. */
149
+ export function errorFor(code, message, status, details = {}, retryAfterS = null) {
150
+ const Kind = BY_CODE[code] ?? BY_STATUS[status] ?? MfxApiError;
151
+ return new Kind(message, status, code, details, retryAfterS);
152
+ }
153
+ /** Seconds from a Retry-After header, or null when there is none or it cannot be read. */
154
+ export function retryAfterSeconds(header) {
155
+ if (header === null || header.trim() === "")
156
+ return null;
157
+ const seconds = Number(header);
158
+ if (Number.isFinite(seconds))
159
+ return Math.max(0, seconds);
160
+ const at = Date.parse(header);
161
+ return Number.isNaN(at) ? null : Math.max(0, (at - Date.now()) / 1000);
162
+ }
@@ -0,0 +1,48 @@
1
+ import { type MfxClient } from "./client.js";
2
+ import type { components } from "./generated/api.js";
3
+ import { type ExecutionResult } from "./results.js";
4
+ type OrderPlace = components["schemas"]["OrderPlace"];
5
+ export type Side = OrderPlace["side"];
6
+ export type OrderType = OrderPlace["order_type"];
7
+ /** What {@link Execution.placeOrder} takes. */
8
+ export interface PlaceOrder {
9
+ /** The broker's own symbol name (`EURUSD.sd`); MFX never renames an instrument. */
10
+ readonly symbol: string;
11
+ readonly side: Side;
12
+ /** In lots, as a decimal string: `"0.01"`. A number is refused. */
13
+ readonly volume: string;
14
+ /** Required: the same key sent again returns the original command, not a second order. */
15
+ readonly idempotencyKey: string;
16
+ /** `market` unless given. */
17
+ readonly orderType?: OrderType;
18
+ /** A pending order's price; for `stop_limit`, the stop price where the order wakes up. */
19
+ readonly price?: string;
20
+ /** For `stop_limit` only: the limit order placed when the stop price is reached. */
21
+ readonly stopLimitPrice?: string;
22
+ readonly sl?: string;
23
+ readonly tp?: string;
24
+ /** How far from the requested price a market order may fill, in points. */
25
+ readonly deviationPoints?: number;
26
+ /** When a pending order expires. */
27
+ readonly expiresAt?: Date | string;
28
+ /** How long the platform may hold the command before it gives up, 1,000 to 60,000 ms. */
29
+ readonly expiresInMs?: number;
30
+ /** The expert advisor number the order is tagged with. */
31
+ readonly magic?: number;
32
+ /** Up to 31 characters; longer is refused, because MT5 would cut it without saying so. */
33
+ readonly comment?: string;
34
+ }
35
+ /** Order entry for one tenant. Every method resolves to a result; none rejects. */
36
+ export declare class Execution {
37
+ #private;
38
+ constructor(client: MfxClient);
39
+ /**
40
+ * Place a market or pending order. Resolves once the platform has persisted the command
41
+ * (state ACCEPTED, with its `commandId`); the outcome follows on the stream.
42
+ *
43
+ * Throws only for a wrong argument, before anything is sent: a number where a decimal string
44
+ * belongs, a missing idempotency key, a comment over 31 characters.
45
+ */
46
+ placeOrder(terminalId: string, order: PlaceOrder): Promise<ExecutionResult>;
47
+ }
48
+ export {};