@mfxhub/sdk 0.1.2 → 0.2.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 +32 -1
- package/README.md +8 -0
- package/dist/accounts.d.ts +47 -0
- package/dist/accounts.js +79 -0
- package/dist/catalog.d.ts +13 -0
- package/dist/catalog.js +14 -0
- package/dist/client.d.ts +5 -1
- package/dist/client.js +12 -1
- package/dist/events.d.ts +13 -0
- package/dist/events.js +5 -0
- package/dist/execution.d.ts +84 -0
- package/dist/execution.js +129 -1
- package/dist/generated/api.d.ts +1104 -66
- package/dist/index.d.ts +8 -5
- package/dist/index.js +3 -2
- package/dist/terminals.d.ts +32 -4
- package/dist/terminals.js +60 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,38 @@ 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
|
-
##
|
|
7
|
+
## 0.2.0
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `terminals.journal(terminalId, { lines })`: the terminal's MetaTrader journal, newest first, 1 to
|
|
12
|
+
200 lines. While the terminal runs the lines are read from it (`from: "live"`); after it stops
|
|
13
|
+
they are the last lines it kept, for 24 hours (`from: "last_words"`). A terminal with no lines
|
|
14
|
+
held throws `StateError`; a cell that does not answer throws `UpstreamError`.
|
|
15
|
+
- `terminals.page` and `terminals.list` take `labels` (every pair must match) and `state` (one or
|
|
16
|
+
several), applied by the platform before the page is cut.
|
|
17
|
+
- `catalog.catalogues(brokerId)`: one row per server and account type you hold a catalogue for,
|
|
18
|
+
with its `catalogue_hash`, to re-read a symbol list only when it changed.
|
|
19
|
+
- The API description is re-vendored. New on what comes back: `broker_words` and `refusal_kind` on
|
|
20
|
+
a terminal's conditions and on `terminal.condition_changed`, `server_kind` on a terminal, path,
|
|
21
|
+
ISIN, exchange, category, base currency, basis and calculation mode on a catalogue symbol,
|
|
22
|
+
`source` (`live` or `last_known`) on a page of symbols, and the values `added`, `came_back`,
|
|
23
|
+
`description` and `identity` for the `change` of `symbol.spec_changed`.
|
|
24
|
+
- A new event, `maintenance.window_changed`, arrives on the `events` channel like any other. The
|
|
25
|
+
payload types `MaintenanceWindowChanged`, `TerminalConditionChanged`, `SymbolSpecChanged` and
|
|
26
|
+
`SymbolSpecChange` are exported.
|
|
27
|
+
- The rest of order entry on `client.execution`: `modifyOrder`, `cancelOrder`, `modifyPosition`,
|
|
28
|
+
`closePosition`, `closePositionPartial`, `closePositionBy`, `setTrailingStop` and
|
|
29
|
+
`clearTrailingStop`. Like `placeOrder`, each takes a required idempotency key, sends only what
|
|
30
|
+
you give, takes decimals as strings, and resolves to an `ExecutionResult` without ever
|
|
31
|
+
rejecting. A wrong argument (a ticket that is not a positive whole number, a modify that
|
|
32
|
+
changes nothing, a comment over 31 characters) throws before anything is sent.
|
|
33
|
+
- `execution.waitFor(commandId, { timeoutMs })` reads a command until it is final, pausing 200 ms
|
|
34
|
+
and doubling up to 2 s, and returns the last answer when the time runs out. A result that is
|
|
35
|
+
not final is never a reason to send again.
|
|
36
|
+
- `client.accounts`: `account`, `positions` and `pendingOrders` from the terminal's latest
|
|
37
|
+
snapshot, and closed deals inside a required window, a page at a time (`dealsPage`) or every
|
|
38
|
+
page (`deals`).
|
|
8
39
|
|
|
9
40
|
## 0.1.2
|
|
10
41
|
|
package/README.md
CHANGED
|
@@ -105,3 +105,11 @@ must fail until it is removed. To move to a new suite release:
|
|
|
105
105
|
|
|
106
106
|
See [CONTRIBUTING.md](CONTRIBUTING.md) to propose a change, and [SECURITY.md](SECURITY.md) to report a
|
|
107
107
|
security problem.
|
|
108
|
+
|
|
109
|
+
## Licence and trademarks
|
|
110
|
+
|
|
111
|
+
Licensed under the [Apache License 2.0](LICENSE).
|
|
112
|
+
|
|
113
|
+
MFX Hub is an independent service and is not affiliated with, endorsed by or sponsored by
|
|
114
|
+
MetaQuotes Ltd. MetaTrader is a registered trademark of MetaQuotes Ltd., used here only to say which
|
|
115
|
+
trading platform MFX Hub's terminals are compatible with.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { MfxClient } from "./client.js";
|
|
2
|
+
import type { components } from "./generated/api.js";
|
|
3
|
+
import { type Page } from "./pages.js";
|
|
4
|
+
/** The account's figures: balance, equity, margin, as of its latest snapshot. */
|
|
5
|
+
export type Account = components["schemas"]["Account"];
|
|
6
|
+
/** Every open position, as one snapshot with its `as_of`. */
|
|
7
|
+
export type PositionList = components["schemas"]["PositionList"];
|
|
8
|
+
/** One open position. */
|
|
9
|
+
export type Position = components["schemas"]["Position"];
|
|
10
|
+
/** Every pending order, as one snapshot with its `as_of`. */
|
|
11
|
+
export type PendingOrderList = components["schemas"]["PendingOrderList"];
|
|
12
|
+
/** One pending order. */
|
|
13
|
+
export type PendingOrder = components["schemas"]["PendingOrder"];
|
|
14
|
+
/** One closed deal. */
|
|
15
|
+
export type Deal = components["schemas"]["Deal"];
|
|
16
|
+
/** The window {@link Accounts.deals} reads. Both ends are required. */
|
|
17
|
+
export interface DealWindow {
|
|
18
|
+
readonly from: Date;
|
|
19
|
+
readonly to: Date;
|
|
20
|
+
/** Only the deals of this position. */
|
|
21
|
+
readonly position?: number;
|
|
22
|
+
/** How many to a page, 1 to 200. */
|
|
23
|
+
readonly limit?: number;
|
|
24
|
+
}
|
|
25
|
+
/** What {@link Accounts.dealsPage} takes. */
|
|
26
|
+
export interface DealPageOptions extends DealWindow {
|
|
27
|
+
/** The `nextCursor` of the page before; leave it out for the first page. */
|
|
28
|
+
readonly cursor?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Read the trading account behind a terminal: figures, positions, pending orders and deals. */
|
|
31
|
+
export declare class Accounts {
|
|
32
|
+
#private;
|
|
33
|
+
constructor(client: MfxClient);
|
|
34
|
+
/** The account's figures. Another tenant's terminal is not found. */
|
|
35
|
+
account(terminalId: string): Promise<Account>;
|
|
36
|
+
/** Every open position, as one snapshot with its `as_of`. */
|
|
37
|
+
positions(terminalId: string): Promise<PositionList>;
|
|
38
|
+
/** Every pending order, as one snapshot with its `as_of`. */
|
|
39
|
+
pendingOrders(terminalId: string): Promise<PendingOrderList>;
|
|
40
|
+
/**
|
|
41
|
+
* One page of closed deals inside a window, newest first. The window is required, by the API
|
|
42
|
+
* and so by this: an unbounded history read is the query that takes a database down.
|
|
43
|
+
*/
|
|
44
|
+
dealsPage(terminalId: string, options: DealPageOptions): Promise<Page<Deal>>;
|
|
45
|
+
/** Every closed deal inside the window, following the cursor, one request a page. */
|
|
46
|
+
deals(terminalId: string, window: DealWindow): AsyncGenerator<Deal, void, undefined>;
|
|
47
|
+
}
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// `client.accounts`: what a terminal's trading account holds.
|
|
2
|
+
//
|
|
3
|
+
// The account, its open positions and its pending orders are read from the terminal's latest
|
|
4
|
+
// snapshot, each with its `as_of`: a change made a moment ago can take a few seconds to show, so
|
|
5
|
+
// read again rather than treat its absence as a failure. Closed deals are history: read inside a
|
|
6
|
+
// window, a page at a time.
|
|
7
|
+
//
|
|
8
|
+
// As elsewhere in the SDK, what comes back is the API's own object, typed from the contract, with
|
|
9
|
+
// its field names as the API sends them.
|
|
10
|
+
import { limit as pageLimit, uuid } from "./arguments.js";
|
|
11
|
+
import { everyItem, pageOf } from "./pages.js";
|
|
12
|
+
function instant(name, value) {
|
|
13
|
+
if (!(value instanceof Date) || Number.isNaN(value.getTime())) {
|
|
14
|
+
throw new TypeError(`${name} is a Date; a deals window has both ends`);
|
|
15
|
+
}
|
|
16
|
+
return value.toISOString();
|
|
17
|
+
}
|
|
18
|
+
/** Read the trading account behind a terminal: figures, positions, pending orders and deals. */
|
|
19
|
+
export class Accounts {
|
|
20
|
+
#client;
|
|
21
|
+
constructor(client) {
|
|
22
|
+
this.#client = client;
|
|
23
|
+
}
|
|
24
|
+
/** The account's figures. Another tenant's terminal is not found. */
|
|
25
|
+
async account(terminalId) {
|
|
26
|
+
const id = uuid("terminalId", terminalId);
|
|
27
|
+
return (await this.#client.read({
|
|
28
|
+
method: "GET",
|
|
29
|
+
path: `/terminals/${id}/account`,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
/** Every open position, as one snapshot with its `as_of`. */
|
|
33
|
+
async positions(terminalId) {
|
|
34
|
+
const id = uuid("terminalId", terminalId);
|
|
35
|
+
const path = `/terminals/${id}/positions`;
|
|
36
|
+
return (await this.#client.read({ method: "GET", path }));
|
|
37
|
+
}
|
|
38
|
+
/** Every pending order, as one snapshot with its `as_of`. */
|
|
39
|
+
async pendingOrders(terminalId) {
|
|
40
|
+
const id = uuid("terminalId", terminalId);
|
|
41
|
+
const path = `/terminals/${id}/orders/pending`;
|
|
42
|
+
return (await this.#client.read({ method: "GET", path }));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One page of closed deals inside a window, newest first. The window is required, by the API
|
|
46
|
+
* and so by this: an unbounded history read is the query that takes a database down.
|
|
47
|
+
*/
|
|
48
|
+
async dealsPage(terminalId, options) {
|
|
49
|
+
const id = uuid("terminalId", terminalId);
|
|
50
|
+
const from = instant("from", options.from);
|
|
51
|
+
const to = instant("to", options.to);
|
|
52
|
+
if (options.to.getTime() < options.from.getTime()) {
|
|
53
|
+
throw new RangeError("the deals window ends before it starts");
|
|
54
|
+
}
|
|
55
|
+
if (options.cursor !== undefined && typeof options.cursor !== "string") {
|
|
56
|
+
throw new TypeError("cursor is the nextCursor of the page before");
|
|
57
|
+
}
|
|
58
|
+
if (options.position !== undefined &&
|
|
59
|
+
(!Number.isSafeInteger(options.position) || options.position < 1)) {
|
|
60
|
+
throw new TypeError("position is a positive whole number, the position's ticket");
|
|
61
|
+
}
|
|
62
|
+
const body = await this.#client.read({
|
|
63
|
+
method: "GET",
|
|
64
|
+
path: `/terminals/${id}/history/deals`,
|
|
65
|
+
query: {
|
|
66
|
+
from,
|
|
67
|
+
to,
|
|
68
|
+
position: options.position,
|
|
69
|
+
cursor: options.cursor,
|
|
70
|
+
limit: pageLimit(options.limit),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
return pageOf(body);
|
|
74
|
+
}
|
|
75
|
+
/** Every closed deal inside the window, following the cursor, one request a page. */
|
|
76
|
+
deals(terminalId, window) {
|
|
77
|
+
return everyItem((cursor) => this.dealsPage(terminalId, { ...window, ...(cursor === undefined ? {} : { cursor }) }));
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/catalog.d.ts
CHANGED
|
@@ -2,6 +2,12 @@ import type { MfxClient } from "./client.js";
|
|
|
2
2
|
import type { components } from "./generated/api.js";
|
|
3
3
|
/** One symbol's current quote, with its age and whether the market can be traded. */
|
|
4
4
|
export type Quote = components["schemas"]["SymbolQuote"];
|
|
5
|
+
/** One server and account type a tenant holds a catalogue for, with a hash of the whole list. */
|
|
6
|
+
export type BrokerCatalogue = components["schemas"]["BrokerCatalogue"];
|
|
7
|
+
/** A catalogue symbol, with its path, ISIN, exchange, category and calculation fields. */
|
|
8
|
+
export type CatalogSymbol = components["schemas"]["Symbol"];
|
|
9
|
+
/** A page of catalogue symbols; `source` says whether they are live or the last known. */
|
|
10
|
+
export type SymbolPage = components["schemas"]["SymbolPage"];
|
|
5
11
|
/** Which of a tenant's servers and account types a symbol is meant on. */
|
|
6
12
|
export interface QuoteOptions {
|
|
7
13
|
/** Needed when the symbol is on more than one of your servers (409 `ambiguous_symbol`). */
|
|
@@ -22,4 +28,11 @@ export declare class Catalog {
|
|
|
22
28
|
* @param symbol the broker's own symbol name (`EURUSD.sd`)
|
|
23
29
|
*/
|
|
24
30
|
quote(brokerId: string, symbol: string, options?: QuoteOptions): Promise<Quote>;
|
|
31
|
+
/**
|
|
32
|
+
* One row per server and account type the tenant holds a catalogue for on this broker, each
|
|
33
|
+
* with `catalogue_hash`. The hash changes when a symbol is added, removed, comes back, or its
|
|
34
|
+
* specification, description or identity changes, and never on a quote: compare it to the last
|
|
35
|
+
* one you saw to re-read only what changed. A broker with no catalogue of yours gives `[]`.
|
|
36
|
+
*/
|
|
37
|
+
catalogues(brokerId: string): Promise<readonly BrokerCatalogue[]>;
|
|
25
38
|
}
|
package/dist/catalog.js
CHANGED
|
@@ -36,4 +36,18 @@ export class Catalog {
|
|
|
36
36
|
});
|
|
37
37
|
return body;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* One row per server and account type the tenant holds a catalogue for on this broker, each
|
|
41
|
+
* with `catalogue_hash`. The hash changes when a symbol is added, removed, comes back, or its
|
|
42
|
+
* specification, description or identity changes, and never on a quote: compare it to the last
|
|
43
|
+
* one you saw to re-read only what changed. A broker with no catalogue of yours gives `[]`.
|
|
44
|
+
*/
|
|
45
|
+
async catalogues(brokerId) {
|
|
46
|
+
const broker = uuid("brokerId", brokerId);
|
|
47
|
+
const body = (await this.#client.read({
|
|
48
|
+
method: "GET",
|
|
49
|
+
path: `/brokers/${broker}/catalogues`,
|
|
50
|
+
}));
|
|
51
|
+
return body.items;
|
|
52
|
+
}
|
|
39
53
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { MfxApiError } from "./errors.js";
|
|
9
9
|
import { RequestSigner, type SigningKey } from "./signing.js";
|
|
10
|
+
import { Accounts } from "./accounts.js";
|
|
10
11
|
import { Catalog } from "./catalog.js";
|
|
11
12
|
import { Execution } from "./execution.js";
|
|
12
13
|
import { MfxStream, type StreamOptions } from "./stream.js";
|
|
@@ -40,7 +41,8 @@ export interface RequestSpec {
|
|
|
40
41
|
readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
41
42
|
/** The path after the version, such as `/terminals/{id}`, with ids already filled in. */
|
|
42
43
|
readonly path: string;
|
|
43
|
-
|
|
44
|
+
/** A list value is sent as a repeated parameter (`label=a:b&label=c:d`). */
|
|
45
|
+
readonly query?: Readonly<Record<string, string | number | boolean | readonly string[] | null | undefined>>;
|
|
44
46
|
readonly body?: unknown;
|
|
45
47
|
/** Required on every write; sent as the Idempotency-Key header. */
|
|
46
48
|
readonly idempotencyKey?: string;
|
|
@@ -60,6 +62,8 @@ export declare class MfxClient {
|
|
|
60
62
|
readonly execution: Execution;
|
|
61
63
|
/** Brokers, symbols and quotes. */
|
|
62
64
|
readonly catalog: Catalog;
|
|
65
|
+
/** The trading account behind a terminal: figures, positions, pending orders and deals. */
|
|
66
|
+
readonly accounts: Accounts;
|
|
63
67
|
/**
|
|
64
68
|
* @param baseUrl the API's base, including the version: `https://api.mfxhub.com/v1`, or
|
|
65
69
|
* `https://api.dev.mfxhub.com/v1` for development
|
package/dist/client.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { MfxApiError, MfxTimeout, MfxTransportError, errorFor, retryAfterSeconds, } from "./errors.js";
|
|
9
9
|
import { RequestSigner } from "./signing.js";
|
|
10
|
+
import { Accounts } from "./accounts.js";
|
|
10
11
|
import { Catalog } from "./catalog.js";
|
|
11
12
|
import { Execution } from "./execution.js";
|
|
12
13
|
import { MfxStream } from "./stream.js";
|
|
@@ -45,6 +46,8 @@ export class MfxClient {
|
|
|
45
46
|
execution;
|
|
46
47
|
/** Brokers, symbols and quotes. */
|
|
47
48
|
catalog;
|
|
49
|
+
/** The trading account behind a terminal: figures, positions, pending orders and deals. */
|
|
50
|
+
accounts;
|
|
48
51
|
/**
|
|
49
52
|
* @param baseUrl the API's base, including the version: `https://api.mfxhub.com/v1`, or
|
|
50
53
|
* `https://api.dev.mfxhub.com/v1` for development
|
|
@@ -62,6 +65,7 @@ export class MfxClient {
|
|
|
62
65
|
this.terminals = new Terminals(this);
|
|
63
66
|
this.execution = new Execution(this);
|
|
64
67
|
this.catalog = new Catalog(this);
|
|
68
|
+
this.accounts = new Accounts(this);
|
|
65
69
|
}
|
|
66
70
|
/** The id of the key this client signs with. Safe to log, unlike the private key. */
|
|
67
71
|
get keyId() {
|
|
@@ -121,8 +125,15 @@ export class MfxClient {
|
|
|
121
125
|
async send(spec) {
|
|
122
126
|
const url = new URL(spec.path.replace(/^\//, ""), this.#base);
|
|
123
127
|
for (const [name, value] of Object.entries(spec.query ?? {})) {
|
|
124
|
-
if (value
|
|
128
|
+
if (value === null || value === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
if (typeof value === "object") {
|
|
131
|
+
for (const each of value)
|
|
132
|
+
url.searchParams.append(name, each);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
125
135
|
url.searchParams.set(name, String(value));
|
|
136
|
+
}
|
|
126
137
|
}
|
|
127
138
|
const body = spec.body === undefined ? new Uint8Array() : Buffer.from(JSON.stringify(spec.body));
|
|
128
139
|
const headers = {
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { components } from "./generated/api.js";
|
|
2
|
+
/** `terminal.condition_changed`: a condition's status moved; carries `broker_words` and `refusal_kind`. */
|
|
3
|
+
export type TerminalConditionChanged = components["schemas"]["TerminalConditionChanged"];
|
|
4
|
+
/** `maintenance.window_changed`: a window was announced, started, moved, ended or cancelled. */
|
|
5
|
+
export type MaintenanceWindowChanged = components["schemas"]["MaintenanceWindowChanged"];
|
|
6
|
+
/** `symbol.spec_changed`: a symbol on a catalogue of yours changed. */
|
|
7
|
+
export type SymbolSpecChanged = components["schemas"]["SymbolSpecChanged"];
|
|
8
|
+
/**
|
|
9
|
+
* What changed on a symbol: `spec`, `trading`, `sessions`, `removed`, `added`, `came_back`,
|
|
10
|
+
* `description` or `identity`. `before` is null for `added` and `after` is null for `removed`; for
|
|
11
|
+
* `came_back` and `description` both are specs. The platform does not send `identity` yet.
|
|
12
|
+
*/
|
|
13
|
+
export type SymbolSpecChange = SymbolSpecChanged["change"];
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The `data` of the platform events a tenant receives on the `events` channel and as webhooks,
|
|
2
|
+
// typed from the contract. The stream delivers every event type the platform sends, including
|
|
3
|
+
// ones this version has not heard of, with `type` as the platform wrote it; these types describe
|
|
4
|
+
// the payloads that are known. On the stream every value is text, as on every event.
|
|
5
|
+
export {};
|
package/dist/execution.d.ts
CHANGED
|
@@ -32,6 +32,58 @@ export interface PlaceOrder {
|
|
|
32
32
|
/** Up to 31 characters; longer is refused, because MT5 would cut it without saying so. */
|
|
33
33
|
readonly comment?: string;
|
|
34
34
|
}
|
|
35
|
+
/** Every command on an existing order or position takes its key; nothing else is required. */
|
|
36
|
+
export interface Keyed {
|
|
37
|
+
/** Required: the same key sent again returns the original command, not a second one. */
|
|
38
|
+
readonly idempotencyKey: string;
|
|
39
|
+
}
|
|
40
|
+
/** What {@link Execution.modifyOrder} takes: only what changes; at least one field. */
|
|
41
|
+
export interface ModifyOrder extends Keyed {
|
|
42
|
+
readonly price?: string;
|
|
43
|
+
readonly stopLimitPrice?: string;
|
|
44
|
+
readonly sl?: string;
|
|
45
|
+
readonly tp?: string;
|
|
46
|
+
/** A new expiry; `null` removes it and the order stays until cancelled. */
|
|
47
|
+
readonly expiresAt?: Date | string | null;
|
|
48
|
+
}
|
|
49
|
+
/** What {@link Execution.modifyPosition} takes: a stop loss, a take profit, or both. */
|
|
50
|
+
export interface ModifyPosition extends Keyed {
|
|
51
|
+
readonly sl?: string;
|
|
52
|
+
readonly tp?: string;
|
|
53
|
+
}
|
|
54
|
+
/** What {@link Execution.closePosition} takes. */
|
|
55
|
+
export interface ClosePosition extends Keyed {
|
|
56
|
+
/** How far from the price the close may fill, in points; the platform's default is 20. */
|
|
57
|
+
readonly deviationPoints?: number;
|
|
58
|
+
/** The closing deal's comment, up to 31 characters; absent, it takes the position's own. */
|
|
59
|
+
readonly comment?: string;
|
|
60
|
+
}
|
|
61
|
+
/** What {@link Execution.closePositionPartial} takes. */
|
|
62
|
+
export interface ClosePositionPartial extends ClosePosition {
|
|
63
|
+
/** The volume to close, in lots, as a decimal string, inside the symbol's step. */
|
|
64
|
+
readonly volume: string;
|
|
65
|
+
}
|
|
66
|
+
/** What {@link Execution.closePositionBy} takes. */
|
|
67
|
+
export interface ClosePositionBy extends Keyed {
|
|
68
|
+
/** The opposite position, in the same symbol, that this one is closed against. */
|
|
69
|
+
readonly byTicket: number;
|
|
70
|
+
}
|
|
71
|
+
/** What {@link Execution.setTrailingStop} takes. */
|
|
72
|
+
export interface SetTrailingStop extends Keyed {
|
|
73
|
+
/** How far behind the best price the stop loss follows, as a price distance: `"0.0020"`. */
|
|
74
|
+
readonly distance: string;
|
|
75
|
+
}
|
|
76
|
+
/** What {@link Execution.waitFor} takes. */
|
|
77
|
+
export interface WaitFor {
|
|
78
|
+
/** How long to keep reading, 1 ms to 10 minutes; 30 s unless given. */
|
|
79
|
+
readonly timeoutMs?: number;
|
|
80
|
+
/** Carried into the result only, so a retry can reuse it. */
|
|
81
|
+
readonly idempotencyKey?: string;
|
|
82
|
+
}
|
|
83
|
+
/** The first pause between reads of a command, doubled each time up to {@link POLL_CAP_MS}. */
|
|
84
|
+
export declare const POLL_FROM_MS = 200;
|
|
85
|
+
/** The longest pause between two reads of a command. */
|
|
86
|
+
export declare const POLL_CAP_MS = 2000;
|
|
35
87
|
/** Order entry for one tenant. Every method resolves to a result; none rejects. */
|
|
36
88
|
export declare class Execution {
|
|
37
89
|
#private;
|
|
@@ -44,6 +96,38 @@ export declare class Execution {
|
|
|
44
96
|
* belongs, a missing idempotency key, a comment over 31 characters.
|
|
45
97
|
*/
|
|
46
98
|
placeOrder(terminalId: string, order: PlaceOrder): Promise<ExecutionResult>;
|
|
99
|
+
/**
|
|
100
|
+
* Amend a pending order's price, stops or expiry; at least one must change. Its volume, side,
|
|
101
|
+
* type and symbol cannot be amended: cancel it and place another.
|
|
102
|
+
*/
|
|
103
|
+
modifyOrder(terminalId: string, ticket: number, change: ModifyOrder): Promise<ExecutionResult>;
|
|
104
|
+
/** Cancel a pending order. First class, not a modify with a special case. */
|
|
105
|
+
cancelOrder(terminalId: string, ticket: number, options: Keyed): Promise<ExecutionResult>;
|
|
106
|
+
/** Move a position's stop loss, take profit, or both. At least one is required. */
|
|
107
|
+
modifyPosition(terminalId: string, ticket: number, change: ModifyPosition): Promise<ExecutionResult>;
|
|
108
|
+
/** Close a position in full. */
|
|
109
|
+
closePosition(terminalId: string, ticket: number, options: ClosePosition): Promise<ExecutionResult>;
|
|
110
|
+
/** Close part of a position: the volume to close, inside the symbol's step. The rest stays. */
|
|
111
|
+
closePositionPartial(terminalId: string, ticket: number, options: ClosePositionPartial): Promise<ExecutionResult>;
|
|
112
|
+
/**
|
|
113
|
+
* Close a position against an opposite one in the same symbol (hedging accounts): the smaller
|
|
114
|
+
* disappears and the larger is reduced by its volume.
|
|
115
|
+
*/
|
|
116
|
+
closePositionBy(terminalId: string, ticket: number, options: ClosePositionBy): Promise<ExecutionResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Keep the position's stop loss `distance` behind the best price it reaches, only ever tighter.
|
|
119
|
+
* Setting it again replaces the distance and keeps what the stop has reached.
|
|
120
|
+
*/
|
|
121
|
+
setTrailingStop(terminalId: string, ticket: number, options: SetTrailingStop): Promise<ExecutionResult>;
|
|
122
|
+
/** Remove the position's trailing stop. The stop loss stays where the trail left it. */
|
|
123
|
+
clearTrailingStop(terminalId: string, ticket: number, options: Keyed): Promise<ExecutionResult>;
|
|
124
|
+
/**
|
|
125
|
+
* Read the command until it is final, or until `timeoutMs` (30 s unless given), pausing 200 ms
|
|
126
|
+
* and doubling up to 2 s between reads. Returns the last answer either way: a command still
|
|
127
|
+
* EXECUTING when the time runs out is reported as it is, not guessed at. **A result that is not
|
|
128
|
+
* final is never a reason to send again**: an UNKNOWN command may already be a position.
|
|
129
|
+
*/
|
|
130
|
+
waitFor(commandId: string, options?: WaitFor): Promise<ExecutionResult>;
|
|
47
131
|
/**
|
|
48
132
|
* One command and its result, as the platform has it now. A command of another tenant is not
|
|
49
133
|
* found, never forbidden. Like every method here it resolves to a result and never rejects:
|
package/dist/execution.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// `client.execution`: placing orders
|
|
1
|
+
// `client.execution`: placing orders, changing and cancelling them, and changing and closing
|
|
2
|
+
// positions.
|
|
2
3
|
//
|
|
3
4
|
// Every method here resolves to an ExecutionResult and never rejects for anything the platform or
|
|
4
5
|
// the network does. A transport fault, a timeout, a rate limit, a refusal and a broker rejection
|
|
@@ -57,6 +58,38 @@ function orderBody(order) {
|
|
|
57
58
|
...Object.fromEntries(Object.entries(optional).filter(([, v]) => v !== undefined)),
|
|
58
59
|
};
|
|
59
60
|
}
|
|
61
|
+
/** The first pause between reads of a command, doubled each time up to {@link POLL_CAP_MS}. */
|
|
62
|
+
export const POLL_FROM_MS = 200;
|
|
63
|
+
/** The longest pause between two reads of a command. */
|
|
64
|
+
export const POLL_CAP_MS = 2000;
|
|
65
|
+
function ticketOf(name, value) {
|
|
66
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
67
|
+
throw new TypeError(`${name} is a positive whole number, the ticket MT5 gave`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
function commentOf(value) {
|
|
72
|
+
if (value === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (typeof value !== "string")
|
|
75
|
+
throw new TypeError("comment is a string");
|
|
76
|
+
if (value.length > 31) {
|
|
77
|
+
throw new RangeError("comment is at most 31 characters; MT5 would cut a longer one");
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function present(fields) {
|
|
82
|
+
return Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
|
|
83
|
+
}
|
|
84
|
+
function maybe(name, value) {
|
|
85
|
+
return value === undefined ? undefined : decimal(name, value);
|
|
86
|
+
}
|
|
87
|
+
function closing(options) {
|
|
88
|
+
return present({
|
|
89
|
+
deviation_points: whole("deviationPoints", options.deviationPoints, 0, 2 ** 31 - 1),
|
|
90
|
+
comment: commentOf(options.comment),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
60
93
|
/** Order entry for one tenant. Every method resolves to a result; none rejects. */
|
|
61
94
|
export class Execution {
|
|
62
95
|
#client;
|
|
@@ -76,6 +109,101 @@ export class Execution {
|
|
|
76
109
|
const body = orderBody(order);
|
|
77
110
|
return this.#command({ method: "POST", path: `/terminals/${id}/orders`, body, idempotencyKey: key }, key);
|
|
78
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Amend a pending order's price, stops or expiry; at least one must change. Its volume, side,
|
|
114
|
+
* type and symbol cannot be amended: cancel it and place another.
|
|
115
|
+
*/
|
|
116
|
+
async modifyOrder(terminalId, ticket, change) {
|
|
117
|
+
const expiresAt = change.expiresAt instanceof Date ? change.expiresAt.toISOString() : change.expiresAt;
|
|
118
|
+
const body = present({
|
|
119
|
+
price: maybe("price", change.price),
|
|
120
|
+
stop_limit_price: maybe("stopLimitPrice", change.stopLimitPrice),
|
|
121
|
+
sl: maybe("sl", change.sl),
|
|
122
|
+
tp: maybe("tp", change.tp),
|
|
123
|
+
expires_at: expiresAt,
|
|
124
|
+
});
|
|
125
|
+
if (Object.keys(body).length === 0) {
|
|
126
|
+
throw new TypeError("modifyOrder changes nothing: give a price, stops or an expiry");
|
|
127
|
+
}
|
|
128
|
+
return this.#on(terminalId, ticket, "orders", "modify", change, body);
|
|
129
|
+
}
|
|
130
|
+
/** Cancel a pending order. First class, not a modify with a special case. */
|
|
131
|
+
async cancelOrder(terminalId, ticket, options) {
|
|
132
|
+
return this.#on(terminalId, ticket, "orders", "cancel", options);
|
|
133
|
+
}
|
|
134
|
+
/** Move a position's stop loss, take profit, or both. At least one is required. */
|
|
135
|
+
async modifyPosition(terminalId, ticket, change) {
|
|
136
|
+
const body = present({ sl: maybe("sl", change.sl), tp: maybe("tp", change.tp) });
|
|
137
|
+
if (Object.keys(body).length === 0) {
|
|
138
|
+
throw new TypeError("modifyPosition changes nothing: give sl, tp or both");
|
|
139
|
+
}
|
|
140
|
+
return this.#on(terminalId, ticket, "positions", "modify", change, body);
|
|
141
|
+
}
|
|
142
|
+
/** Close a position in full. */
|
|
143
|
+
async closePosition(terminalId, ticket, options) {
|
|
144
|
+
return this.#on(terminalId, ticket, "positions", "close", options, closing(options));
|
|
145
|
+
}
|
|
146
|
+
/** Close part of a position: the volume to close, inside the symbol's step. The rest stays. */
|
|
147
|
+
async closePositionPartial(terminalId, ticket, options) {
|
|
148
|
+
const body = { volume: decimal("volume", options.volume), ...closing(options) };
|
|
149
|
+
return this.#on(terminalId, ticket, "positions", "close-partial", options, body);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Close a position against an opposite one in the same symbol (hedging accounts): the smaller
|
|
153
|
+
* disappears and the larger is reduced by its volume.
|
|
154
|
+
*/
|
|
155
|
+
async closePositionBy(terminalId, ticket, options) {
|
|
156
|
+
const by = ticketOf("byTicket", options.byTicket);
|
|
157
|
+
if (by === ticket)
|
|
158
|
+
throw new RangeError("a position cannot be closed by itself");
|
|
159
|
+
return this.#on(terminalId, ticket, "positions", "close-by", options, { by_ticket: by });
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Keep the position's stop loss `distance` behind the best price it reaches, only ever tighter.
|
|
163
|
+
* Setting it again replaces the distance and keeps what the stop has reached.
|
|
164
|
+
*/
|
|
165
|
+
async setTrailingStop(terminalId, ticket, options) {
|
|
166
|
+
const body = { distance: decimal("distance", options.distance) };
|
|
167
|
+
return this.#on(terminalId, ticket, "positions", "trailing-stop", options, body, "PUT");
|
|
168
|
+
}
|
|
169
|
+
/** Remove the position's trailing stop. The stop loss stays where the trail left it. */
|
|
170
|
+
async clearTrailingStop(terminalId, ticket, options) {
|
|
171
|
+
return this.#on(terminalId, ticket, "positions", "trailing-stop", options, undefined, "DELETE");
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Read the command until it is final, or until `timeoutMs` (30 s unless given), pausing 200 ms
|
|
175
|
+
* and doubling up to 2 s between reads. Returns the last answer either way: a command still
|
|
176
|
+
* EXECUTING when the time runs out is reported as it is, not guessed at. **A result that is not
|
|
177
|
+
* final is never a reason to send again**: an UNKNOWN command may already be a position.
|
|
178
|
+
*/
|
|
179
|
+
async waitFor(commandId, options = {}) {
|
|
180
|
+
const timeout = options.timeoutMs ?? 30_000;
|
|
181
|
+
if (!Number.isFinite(timeout) || timeout < 1 || timeout > 600_000) {
|
|
182
|
+
throw new RangeError("timeoutMs is 1 to 600,000");
|
|
183
|
+
}
|
|
184
|
+
const key = options.idempotencyKey ?? "";
|
|
185
|
+
let result = await this.getCommand(commandId, key);
|
|
186
|
+
// DECISION: the time is counted in the pauses the client takes, not on a wall clock, so a
|
|
187
|
+
// scripted sleep in a test gives the same schedule a caller gets.
|
|
188
|
+
let waited = 0;
|
|
189
|
+
let pause = POLL_FROM_MS;
|
|
190
|
+
while (!result.final && waited < timeout) {
|
|
191
|
+
const step = Math.min(pause, POLL_CAP_MS, timeout - waited);
|
|
192
|
+
await this.#client.pause(step);
|
|
193
|
+
waited += step;
|
|
194
|
+
pause *= 2;
|
|
195
|
+
result = await this.getCommand(commandId, key);
|
|
196
|
+
}
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
/** One command on an existing order or position: checked, then sent once. */
|
|
200
|
+
async #on(terminalId, ticket, kind, action, options, body, method = "POST") {
|
|
201
|
+
const id = uuid("terminalId", terminalId);
|
|
202
|
+
const number = ticketOf("ticket", ticket);
|
|
203
|
+
const key = idempotencyKey(options?.idempotencyKey);
|
|
204
|
+
const path = `/terminals/${id}/${kind}/${String(number)}/${action}`;
|
|
205
|
+
return this.#command({ method, path, ...(body === undefined ? {} : { body }), idempotencyKey: key }, key);
|
|
206
|
+
}
|
|
79
207
|
/**
|
|
80
208
|
* One command and its result, as the platform has it now. A command of another tenant is not
|
|
81
209
|
* found, never forbidden. Like every method here it resolves to a result and never rejects:
|