@mfxhub/sdk 0.2.3 → 0.4.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 +66 -0
- package/dist/accounts.d.ts +5 -1
- package/dist/accounts.js +17 -9
- package/dist/client.d.ts +3 -0
- package/dist/client.js +7 -1
- package/dist/execution.d.ts +7 -1
- package/dist/execution.js +15 -5
- package/dist/generated/api.d.ts +9523 -3748
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -0
- package/dist/results.d.ts +27 -1
- package/dist/results.js +27 -0
- package/dist/terminals.d.ts +48 -1
- package/dist/terminals.js +106 -1
- package/dist/trading-accounts.d.ts +130 -0
- package/dist/trading-accounts.js +228 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -18,10 +18,12 @@ export type { Account, Deal, DealPageOptions, DealWindow, PendingOrder, PendingO
|
|
|
18
18
|
export { Execution, POLL_CAP_MS, POLL_FROM_MS } from "./execution.js";
|
|
19
19
|
export { CLOSE_LIFETIME, CLOSE_REAUTHENTICATE, CLOSE_SLOW_CONSUMER, Channel, ChannelRefused, MfxStream, Resync, StreamClosed, StreamMessage, wsConnector, } from "./stream.js";
|
|
20
20
|
export type { Connector, StreamConnection, StreamEvent, StreamOptions, StreamStats, Timeframe, } from "./stream.js";
|
|
21
|
-
export type { ClosePosition, ClosePositionBy, ClosePositionPartial, Keyed, ModifyOrder, ModifyPosition, OrderType, PlaceOrder, SetTrailingStop, Side, WaitFor, } from "./execution.js";
|
|
22
|
-
export type { CommandState, ExecutionResult, Reason, Tickets } from "./results.js";
|
|
21
|
+
export type { ClosePosition, ClosePositionBy, ClosePositionPartial, Keyed, ModifyOrder, ModifyPosition, OrderType, PlaceOrder, Resource, SetTrailingStop, Side, WaitFor, } from "./execution.js";
|
|
22
|
+
export type { CommandState, ExecutionResult, Reason, Tickets, VenueDeal } 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, CtraderGrant, CtraderGrantStarted, GrantScope, StartCtraderGrant, TradingAccount, TradingAccountFilter, TradingAccountPageOptions, TradingSymbol, TradingSymbolList, 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
|
@@ -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";
|
package/dist/results.d.ts
CHANGED
|
@@ -6,13 +6,24 @@ export type CommandState = components["schemas"]["CommandState"];
|
|
|
6
6
|
* (`transport_error`, `timeout`, `refused`, and `maintenance` when the API refuses outright).
|
|
7
7
|
* A reason the platform adds later arrives in `reasonText` as it was sent, with `reason` null.
|
|
8
8
|
*/
|
|
9
|
-
export type Reason = "success" | "requote" | "instrument_disabled" | "no_liquidity" | "market_closed" | "invalid_stops" | "invalid_volume" | "insufficient_funds" | "trade_disabled" | "rejected_other" | "rate_limited" | "expired" | "unknown" | "maintenance" | "transport_error" | "timeout" | "refused";
|
|
9
|
+
export type Reason = "success" | "requote" | "instrument_disabled" | "no_liquidity" | "market_closed" | "invalid_stops" | "invalid_volume" | "insufficient_funds" | "trade_disabled" | "rejected_other" | "rate_limited" | "expired" | "unknown" | "maintenance" | "venue_unavailable" | "not_on_this_venue" | "transport_error" | "timeout" | "refused";
|
|
10
10
|
/** MT5 relates one order, one deal and one position per fill; null where MT5 has none yet. */
|
|
11
11
|
export interface Tickets {
|
|
12
12
|
readonly order: number | null;
|
|
13
13
|
readonly deal: number | null;
|
|
14
14
|
readonly position: number | null;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* One deal a command made, as the venue reported it (cTrader): two for an emulated close-by, one
|
|
18
|
+
* for an order filled at once. `volumeUnits` is in units of the instrument, not lots.
|
|
19
|
+
*/
|
|
20
|
+
export interface VenueDeal {
|
|
21
|
+
readonly deal: number;
|
|
22
|
+
readonly order: number | null;
|
|
23
|
+
readonly position: number | null;
|
|
24
|
+
readonly volumeUnits: string | null;
|
|
25
|
+
readonly price: string | null;
|
|
26
|
+
}
|
|
16
27
|
/**
|
|
17
28
|
* The whole answer to one execution call. Never thrown, always returned.
|
|
18
29
|
*
|
|
@@ -60,6 +71,21 @@ export interface ExecutionResult {
|
|
|
60
71
|
readonly detail: string | null;
|
|
61
72
|
/** When sending it again can work: a maintenance window's end, where the platform said. */
|
|
62
73
|
readonly retryAt: Date | null;
|
|
74
|
+
/** The venue that answered, `mt5` or `ctrader`; null until there is a result. */
|
|
75
|
+
readonly venue: string | null;
|
|
76
|
+
/**
|
|
77
|
+
* The venue's own code for a refusal, verbatim, beside `reason` (cTrader: such as
|
|
78
|
+
* NOT_ENOUGH_MONEY). Null on MT5, whose code is `mt5Retcode`.
|
|
79
|
+
*/
|
|
80
|
+
readonly venueCode: string | null;
|
|
81
|
+
/** Every deal the command made, in order (cTrader). Empty on MT5, where `tickets.deal` is it. */
|
|
82
|
+
readonly deals: readonly VenueDeal[];
|
|
83
|
+
/** The volume filled in units of the instrument; `execVolume` stays in lots. Null on MT5. */
|
|
84
|
+
readonly volumeUnits: string | null;
|
|
85
|
+
/** A close-by the venue has no equivalent for, done as two closes (cTrader). */
|
|
86
|
+
readonly closeByEmulated: boolean;
|
|
87
|
+
/** A market order filled but its stop loss or take profit could not be set: it is open without. */
|
|
88
|
+
readonly protectionNotSet: boolean;
|
|
63
89
|
}
|
|
64
90
|
/** A result from a Command or a CommandAccepted body, whichever the call answered with. */
|
|
65
91
|
export declare function fromCommand(body: unknown, idempotencyKey: string, clientRttMs: number): ExecutionResult;
|
package/dist/results.js
CHANGED
|
@@ -40,10 +40,31 @@ const REASONS = new Set([
|
|
|
40
40
|
"expired",
|
|
41
41
|
"unknown",
|
|
42
42
|
"maintenance",
|
|
43
|
+
"venue_unavailable",
|
|
44
|
+
"not_on_this_venue",
|
|
43
45
|
"transport_error",
|
|
44
46
|
"timeout",
|
|
45
47
|
"refused",
|
|
46
48
|
]);
|
|
49
|
+
function deals(value) {
|
|
50
|
+
if (!Array.isArray(value))
|
|
51
|
+
return [];
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const item of value) {
|
|
54
|
+
const one = record(item);
|
|
55
|
+
const deal = int(one.deal);
|
|
56
|
+
if (deal === null)
|
|
57
|
+
continue;
|
|
58
|
+
out.push({
|
|
59
|
+
deal,
|
|
60
|
+
order: int(one.order),
|
|
61
|
+
position: int(one.position),
|
|
62
|
+
volumeUnits: decimalOut(one.volume_units),
|
|
63
|
+
price: decimalOut(one.price),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
47
68
|
function int(value) {
|
|
48
69
|
return typeof value === "number" && Number.isInteger(value) ? value : null;
|
|
49
70
|
}
|
|
@@ -95,6 +116,12 @@ export function fromCommand(body, idempotencyKey, clientRttMs) {
|
|
|
95
116
|
reroutedFrom: text(result.rerouted_from),
|
|
96
117
|
detail: null,
|
|
97
118
|
retryAt: null,
|
|
119
|
+
venue: text(result.venue),
|
|
120
|
+
venueCode: text(result.venue_code),
|
|
121
|
+
deals: deals(result.deals),
|
|
122
|
+
volumeUnits: decimalOut(result.volume_units),
|
|
123
|
+
closeByEmulated: result.close_by_emulated === true,
|
|
124
|
+
protectionNotSet: result.protection_not_set === true,
|
|
98
125
|
};
|
|
99
126
|
}
|
|
100
127
|
/**
|
package/dist/terminals.d.ts
CHANGED
|
@@ -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,130 @@
|
|
|
1
|
+
import { type Account, type Deal, type DealPageOptions, type DealWindow, type PendingOrderList, type PositionList } from "./accounts.js";
|
|
2
|
+
import type { MfxClient } from "./client.js";
|
|
3
|
+
import { type ClosePosition, type ClosePositionBy, type ClosePositionPartial, type Keyed, type ModifyOrder, type ModifyPosition, type PlaceOrder, type SetTrailingStop } from "./execution.js";
|
|
4
|
+
import type { components } from "./generated/api.js";
|
|
5
|
+
import { type Page } from "./pages.js";
|
|
6
|
+
import type { ExecutionResult } from "./results.js";
|
|
7
|
+
/** One trading account at one venue, as the API describes it. */
|
|
8
|
+
export type TradingAccount = components["schemas"]["TradingAccount"];
|
|
9
|
+
/** The trading technology an account is on. */
|
|
10
|
+
export type Venue = TradingAccount["venue"];
|
|
11
|
+
/** An account's state, in words that hold on every venue. */
|
|
12
|
+
export type AccountState = TradingAccount["state"];
|
|
13
|
+
/** Live or demo. */
|
|
14
|
+
export type AccountEnvironment = NonNullable<TradingAccount["environment"]>;
|
|
15
|
+
/** The symbols one cTrader account can trade, with their volume rules in lots and leverage. */
|
|
16
|
+
export type TradingSymbolList = components["schemas"]["TradingSymbolList"];
|
|
17
|
+
/** One symbol of a cTrader account. */
|
|
18
|
+
export type TradingSymbol = components["schemas"]["TradingSymbol"];
|
|
19
|
+
/** A cTrader grant: its state and, once ACTIVE, the accounts the trader shared. */
|
|
20
|
+
export type CtraderGrant = components["schemas"]["CtraderGrant"];
|
|
21
|
+
/** A started grant: the consent link to send the trader to, and until when it works. */
|
|
22
|
+
export type CtraderGrantStarted = components["schemas"]["CtraderGrantStarted"];
|
|
23
|
+
/** What a grant lets MFX Hub do: `accounts` reads only, `trading` reads and trades. */
|
|
24
|
+
export type GrantScope = components["schemas"]["CtraderGrantCreate"]["scope"];
|
|
25
|
+
/** What {@link TradingAccounts.startCtraderGrant} takes. */
|
|
26
|
+
export interface StartCtraderGrant {
|
|
27
|
+
/** One grant covers live or demo accounts. */
|
|
28
|
+
readonly environment: AccountEnvironment;
|
|
29
|
+
readonly scope: GrantScope;
|
|
30
|
+
/** Where the trader comes back to, https only, with `grant_id`, `result` and your state. */
|
|
31
|
+
readonly returnUrl: string;
|
|
32
|
+
/** Your own value, returned unchanged on the return URL; never read by MFX Hub. */
|
|
33
|
+
readonly tenantState?: string;
|
|
34
|
+
readonly idempotencyKey: string;
|
|
35
|
+
}
|
|
36
|
+
/** Narrow the list on the platform, before the page is cut. */
|
|
37
|
+
export interface TradingAccountFilter {
|
|
38
|
+
/** Only accounts on this venue, or on one of these. */
|
|
39
|
+
readonly venue?: Venue | readonly Venue[];
|
|
40
|
+
/** Only live, or only demo, accounts. */
|
|
41
|
+
readonly environment?: AccountEnvironment;
|
|
42
|
+
/** Only accounts in this state, or in one of these. */
|
|
43
|
+
readonly state?: AccountState | readonly AccountState[];
|
|
44
|
+
}
|
|
45
|
+
/** What {@link TradingAccounts.page} takes. */
|
|
46
|
+
export interface TradingAccountPageOptions extends TradingAccountFilter {
|
|
47
|
+
/** The `nextCursor` of the page before; leave it out for the first page. */
|
|
48
|
+
readonly cursor?: string;
|
|
49
|
+
/** How many to a page, 1 to 200. */
|
|
50
|
+
readonly limit?: number;
|
|
51
|
+
}
|
|
52
|
+
/** Read, trade and connect the trading accounts this key's tenant holds. */
|
|
53
|
+
export declare class TradingAccounts {
|
|
54
|
+
#private;
|
|
55
|
+
constructor(client: MfxClient);
|
|
56
|
+
/**
|
|
57
|
+
* One trading account, by its own id or by one of its terminals' ids. Another tenant's, or an id
|
|
58
|
+
* that is not an account of yours, is 404.
|
|
59
|
+
*/
|
|
60
|
+
get(accountId: string): Promise<TradingAccount>;
|
|
61
|
+
/** One page of this tenant's trading accounts, newest first. One request. */
|
|
62
|
+
page(options?: TradingAccountPageOptions): Promise<Page<TradingAccount>>;
|
|
63
|
+
/**
|
|
64
|
+
* Every trading account, following the cursor, one request a page. There is no total. The
|
|
65
|
+
* filters narrow the list on the platform, so a filtered page is never short for being filtered.
|
|
66
|
+
*/
|
|
67
|
+
list(options?: Omit<TradingAccountPageOptions, "cursor">): AsyncGenerator<TradingAccount, void, undefined>;
|
|
68
|
+
/**
|
|
69
|
+
* Balance, equity, margin and profile, answered by the account's lead runner. On cTrader the
|
|
70
|
+
* equity is computed by the platform (the venue sends none); `stale` says when it could not be.
|
|
71
|
+
*/
|
|
72
|
+
funds(accountId: string): Promise<Account>;
|
|
73
|
+
/** Every open position, as one snapshot. On cTrader `volume` is in lots. */
|
|
74
|
+
positions(accountId: string): Promise<PositionList>;
|
|
75
|
+
/** Every pending order, as one snapshot. */
|
|
76
|
+
pendingOrders(accountId: string): Promise<PendingOrderList>;
|
|
77
|
+
/**
|
|
78
|
+
* One page of deals inside a window of at most 31 days, newest first, optionally of one
|
|
79
|
+
* position. On cTrader every time is UTC and `magic` is the order's origin marker.
|
|
80
|
+
*/
|
|
81
|
+
dealsPage(accountId: string, options: DealPageOptions): Promise<Page<Deal>>;
|
|
82
|
+
/** Every deal inside the window, following the cursor, one request a page. */
|
|
83
|
+
deals(accountId: string, window: DealWindow): AsyncGenerator<Deal, void, undefined>;
|
|
84
|
+
/**
|
|
85
|
+
* The symbols a cTrader account can trade: a volume a command sends must be a whole
|
|
86
|
+
* `volume_step` between `volume_min` and `volume_max`, in lots. An MT5 account is refused.
|
|
87
|
+
*/
|
|
88
|
+
symbols(accountId: string): Promise<TradingSymbolList>;
|
|
89
|
+
/**
|
|
90
|
+
* Place a market or pending order on the account. `volume` is in lots on every venue, and on
|
|
91
|
+
* cTrader a whole step of the symbol (`symbols`). On cTrader `magic` stays on the order as its
|
|
92
|
+
* label, and a market order's stops are set just after the fill (`protectionNotSet` says when
|
|
93
|
+
* that failed).
|
|
94
|
+
*/
|
|
95
|
+
placeOrder(accountId: string, order: PlaceOrder): Promise<ExecutionResult>;
|
|
96
|
+
/** Amend a pending order's price, stops or expiry. */
|
|
97
|
+
modifyOrder(accountId: string, ticket: number, change: ModifyOrder): Promise<ExecutionResult>;
|
|
98
|
+
/** Cancel a pending order. */
|
|
99
|
+
cancelOrder(accountId: string, ticket: number, options: Keyed): Promise<ExecutionResult>;
|
|
100
|
+
/** Move a position's stop loss, take profit, or both. */
|
|
101
|
+
modifyPosition(accountId: string, ticket: number, change: ModifyPosition): Promise<ExecutionResult>;
|
|
102
|
+
/** Close a position in full. */
|
|
103
|
+
closePosition(accountId: string, ticket: number, options: ClosePosition): Promise<ExecutionResult>;
|
|
104
|
+
/** Close part of a position, in lots. */
|
|
105
|
+
closePositionPartial(accountId: string, ticket: number, options: ClosePositionPartial): Promise<ExecutionResult>;
|
|
106
|
+
/**
|
|
107
|
+
* Close a position against an opposite one. cTrader has no close-by: it is done as two closes,
|
|
108
|
+
* the result says `closeByEmulated` and `deals` holds both.
|
|
109
|
+
*/
|
|
110
|
+
closePositionBy(accountId: string, ticket: number, options: ClosePositionBy): Promise<ExecutionResult>;
|
|
111
|
+
/** Trail a position's stop loss. A venue without a trailing stop refuses it, as a result. */
|
|
112
|
+
setTrailingStop(accountId: string, ticket: number, options: SetTrailingStop): Promise<ExecutionResult>;
|
|
113
|
+
/** Remove a position's trailing stop. */
|
|
114
|
+
clearTrailingStop(accountId: string, ticket: number, options: Keyed): Promise<ExecutionResult>;
|
|
115
|
+
/**
|
|
116
|
+
* Start connecting a trader's cTrader accounts; resolves to the consent link to send them to.
|
|
117
|
+
* They sign in at cTrader (MFX Hub never sees the password), choose the accounts to share and
|
|
118
|
+
* come back to `returnUrl`. The link lives 15 minutes. A plan without cTrader is forbidden.
|
|
119
|
+
*/
|
|
120
|
+
startCtraderGrant(grant: StartCtraderGrant): Promise<CtraderGrantStarted>;
|
|
121
|
+
/** One grant: PENDING, ACTIVE, REVOKED or EXPIRED, and once ACTIVE the accounts shared. */
|
|
122
|
+
ctraderGrant(grantId: string): Promise<CtraderGrant>;
|
|
123
|
+
/**
|
|
124
|
+
* Make some of an ACTIVE grant's accounts trading accounts of yours, by cTrader's ids (1 to
|
|
125
|
+
* 50). Each starts PENDING and is CONNECTED once authorised at cTrader.
|
|
126
|
+
*/
|
|
127
|
+
connectCtraderAccounts(grantId: string, ctidTraderAccountIds: readonly (string | number)[], options: Keyed): Promise<TradingAccount[]>;
|
|
128
|
+
/** Revoke a grant: its accounts move to REAUTH_NEEDED; a new grant reconnects them. */
|
|
129
|
+
revokeCtraderGrant(grantId: string, options: Keyed): Promise<CtraderGrant>;
|
|
130
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
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 a cTrader account. An MT5 account takes its first terminal's id, and `get` also takes
|
|
5
|
+
// any of its terminals' ids. Its state is the venue-neutral word (CONNECTED, DEGRADED,
|
|
6
|
+
// REAUTH_NEEDED, ...); `client.terminals.get` has the MT5 detail.
|
|
7
|
+
//
|
|
8
|
+
// The same object trades an account and reads it on any venue: the nine commands of
|
|
9
|
+
// `client.execution` keyed on the account (`exec:write`), and its funds, positions, pending orders,
|
|
10
|
+
// deals and symbols (`acct:read`). The platform hands each to the account's lead runner. A cTrader
|
|
11
|
+
// account is connected through a grant: `startCtraderGrant` answers the consent link to send the
|
|
12
|
+
// trader to, `ctraderGrant` lists the accounts they shared, and `connectCtraderAccounts` makes the
|
|
13
|
+
// chosen ones trading accounts of yours (`term:write`).
|
|
14
|
+
//
|
|
15
|
+
// DECISION: a namespace of its own beside `client.accounts`, which stays what it was: the balance,
|
|
16
|
+
// positions and deals of a terminal's account. Folding the two together would give `accounts.get`
|
|
17
|
+
// two meanings.
|
|
18
|
+
import { Accounts, } from "./accounts.js";
|
|
19
|
+
import { idempotencyKey, limit as pageLimit, uuid } from "./arguments.js";
|
|
20
|
+
import { Execution, } from "./execution.js";
|
|
21
|
+
import { everyItem, pageOf } from "./pages.js";
|
|
22
|
+
function joined(name, value) {
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
const words = typeof value === "string" ? [value] : value;
|
|
26
|
+
if (words.length === 0)
|
|
27
|
+
throw new RangeError(`${name} is one or more values; got none`);
|
|
28
|
+
for (const word of words) {
|
|
29
|
+
if (typeof word !== "string" || word === "")
|
|
30
|
+
throw new TypeError(`${name} is a list of words`);
|
|
31
|
+
}
|
|
32
|
+
return words.join(",");
|
|
33
|
+
}
|
|
34
|
+
/** Read, trade and connect the trading accounts this key's tenant holds. */
|
|
35
|
+
export class TradingAccounts {
|
|
36
|
+
#client;
|
|
37
|
+
#orders;
|
|
38
|
+
#reads;
|
|
39
|
+
constructor(client) {
|
|
40
|
+
this.#client = client;
|
|
41
|
+
this.#orders = new Execution(client, "accounts");
|
|
42
|
+
this.#reads = new Accounts(client, "accounts");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* One trading account, by its own id or by one of its terminals' ids. Another tenant's, or an id
|
|
46
|
+
* that is not an account of yours, is 404.
|
|
47
|
+
*/
|
|
48
|
+
async get(accountId) {
|
|
49
|
+
const id = uuid("accountId", accountId);
|
|
50
|
+
return (await this.#client.read({ method: "GET", path: `/accounts/${id}` }));
|
|
51
|
+
}
|
|
52
|
+
/** One page of this tenant's trading accounts, newest first. One request. */
|
|
53
|
+
async page(options = {}) {
|
|
54
|
+
const limit = pageLimit(options.limit);
|
|
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.environment !== undefined && typeof options.environment !== "string") {
|
|
59
|
+
throw new TypeError("environment is live or demo");
|
|
60
|
+
}
|
|
61
|
+
const body = await this.#client.read({
|
|
62
|
+
method: "GET",
|
|
63
|
+
path: "/accounts",
|
|
64
|
+
query: {
|
|
65
|
+
cursor: options.cursor,
|
|
66
|
+
limit,
|
|
67
|
+
venue: joined("venue", options.venue),
|
|
68
|
+
environment: options.environment,
|
|
69
|
+
state: joined("state", options.state),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
return pageOf(body);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Every trading account, following the cursor, one request a page. There is no total. The
|
|
76
|
+
* filters narrow the list on the platform, so a filtered page is never short for being filtered.
|
|
77
|
+
*/
|
|
78
|
+
list(options = {}) {
|
|
79
|
+
const limit = pageLimit(options.limit);
|
|
80
|
+
const rest = {
|
|
81
|
+
...(limit === undefined ? {} : { limit }),
|
|
82
|
+
...(options.venue === undefined ? {} : { venue: options.venue }),
|
|
83
|
+
...(options.environment === undefined ? {} : { environment: options.environment }),
|
|
84
|
+
...(options.state === undefined ? {} : { state: options.state }),
|
|
85
|
+
};
|
|
86
|
+
return everyItem((cursor) => this.page({ ...rest, ...(cursor === undefined ? {} : { cursor }) }));
|
|
87
|
+
}
|
|
88
|
+
// ------------------------------------------------------------------------------------- reads
|
|
89
|
+
/**
|
|
90
|
+
* Balance, equity, margin and profile, answered by the account's lead runner. On cTrader the
|
|
91
|
+
* equity is computed by the platform (the venue sends none); `stale` says when it could not be.
|
|
92
|
+
*/
|
|
93
|
+
async funds(accountId) {
|
|
94
|
+
return this.#reads.account(accountId);
|
|
95
|
+
}
|
|
96
|
+
/** Every open position, as one snapshot. On cTrader `volume` is in lots. */
|
|
97
|
+
async positions(accountId) {
|
|
98
|
+
return this.#reads.positions(accountId);
|
|
99
|
+
}
|
|
100
|
+
/** Every pending order, as one snapshot. */
|
|
101
|
+
async pendingOrders(accountId) {
|
|
102
|
+
return this.#reads.pendingOrders(accountId);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* One page of deals inside a window of at most 31 days, newest first, optionally of one
|
|
106
|
+
* position. On cTrader every time is UTC and `magic` is the order's origin marker.
|
|
107
|
+
*/
|
|
108
|
+
async dealsPage(accountId, options) {
|
|
109
|
+
return this.#reads.dealsPage(accountId, options);
|
|
110
|
+
}
|
|
111
|
+
/** Every deal inside the window, following the cursor, one request a page. */
|
|
112
|
+
deals(accountId, window) {
|
|
113
|
+
return this.#reads.deals(accountId, window);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The symbols a cTrader account can trade: a volume a command sends must be a whole
|
|
117
|
+
* `volume_step` between `volume_min` and `volume_max`, in lots. An MT5 account is refused.
|
|
118
|
+
*/
|
|
119
|
+
async symbols(accountId) {
|
|
120
|
+
const id = uuid("accountId", accountId);
|
|
121
|
+
const path = `/accounts/${id}/symbols`;
|
|
122
|
+
return (await this.#client.read({ method: "GET", path }));
|
|
123
|
+
}
|
|
124
|
+
// ---------------------------------------------------------------------------------- commands
|
|
125
|
+
// The same bodies and rules as `client.execution`, sent to /accounts/{id}/...: each resolves to
|
|
126
|
+
// a result and never rejects. Read the outcome with `client.execution.waitFor`.
|
|
127
|
+
/**
|
|
128
|
+
* Place a market or pending order on the account. `volume` is in lots on every venue, and on
|
|
129
|
+
* cTrader a whole step of the symbol (`symbols`). On cTrader `magic` stays on the order as its
|
|
130
|
+
* label, and a market order's stops are set just after the fill (`protectionNotSet` says when
|
|
131
|
+
* that failed).
|
|
132
|
+
*/
|
|
133
|
+
async placeOrder(accountId, order) {
|
|
134
|
+
return this.#orders.placeOrder(accountId, order);
|
|
135
|
+
}
|
|
136
|
+
/** Amend a pending order's price, stops or expiry. */
|
|
137
|
+
async modifyOrder(accountId, ticket, change) {
|
|
138
|
+
return this.#orders.modifyOrder(accountId, ticket, change);
|
|
139
|
+
}
|
|
140
|
+
/** Cancel a pending order. */
|
|
141
|
+
async cancelOrder(accountId, ticket, options) {
|
|
142
|
+
return this.#orders.cancelOrder(accountId, ticket, options);
|
|
143
|
+
}
|
|
144
|
+
/** Move a position's stop loss, take profit, or both. */
|
|
145
|
+
async modifyPosition(accountId, ticket, change) {
|
|
146
|
+
return this.#orders.modifyPosition(accountId, ticket, change);
|
|
147
|
+
}
|
|
148
|
+
/** Close a position in full. */
|
|
149
|
+
async closePosition(accountId, ticket, options) {
|
|
150
|
+
return this.#orders.closePosition(accountId, ticket, options);
|
|
151
|
+
}
|
|
152
|
+
/** Close part of a position, in lots. */
|
|
153
|
+
async closePositionPartial(accountId, ticket, options) {
|
|
154
|
+
return this.#orders.closePositionPartial(accountId, ticket, options);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Close a position against an opposite one. cTrader has no close-by: it is done as two closes,
|
|
158
|
+
* the result says `closeByEmulated` and `deals` holds both.
|
|
159
|
+
*/
|
|
160
|
+
async closePositionBy(accountId, ticket, options) {
|
|
161
|
+
return this.#orders.closePositionBy(accountId, ticket, options);
|
|
162
|
+
}
|
|
163
|
+
/** Trail a position's stop loss. A venue without a trailing stop refuses it, as a result. */
|
|
164
|
+
async setTrailingStop(accountId, ticket, options) {
|
|
165
|
+
return this.#orders.setTrailingStop(accountId, ticket, options);
|
|
166
|
+
}
|
|
167
|
+
/** Remove a position's trailing stop. */
|
|
168
|
+
async clearTrailingStop(accountId, ticket, options) {
|
|
169
|
+
return this.#orders.clearTrailingStop(accountId, ticket, options);
|
|
170
|
+
}
|
|
171
|
+
// ----------------------------------------------------------------------------- cTrader grants
|
|
172
|
+
/**
|
|
173
|
+
* Start connecting a trader's cTrader accounts; resolves to the consent link to send them to.
|
|
174
|
+
* They sign in at cTrader (MFX Hub never sees the password), choose the accounts to share and
|
|
175
|
+
* come back to `returnUrl`. The link lives 15 minutes. A plan without cTrader is forbidden.
|
|
176
|
+
*/
|
|
177
|
+
async startCtraderGrant(grant) {
|
|
178
|
+
const key = idempotencyKey(grant.idempotencyKey);
|
|
179
|
+
if (typeof grant.returnUrl !== "string" || !grant.returnUrl.startsWith("https://")) {
|
|
180
|
+
throw new TypeError("returnUrl is an https:// URL");
|
|
181
|
+
}
|
|
182
|
+
const body = {
|
|
183
|
+
environment: grant.environment,
|
|
184
|
+
scope: grant.scope,
|
|
185
|
+
return_url: grant.returnUrl,
|
|
186
|
+
...(grant.tenantState === undefined ? {} : { tenant_state: grant.tenantState }),
|
|
187
|
+
};
|
|
188
|
+
return (await this.#client.write({
|
|
189
|
+
method: "POST",
|
|
190
|
+
path: "/accounts/ctrader/grants",
|
|
191
|
+
body,
|
|
192
|
+
idempotencyKey: key,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
/** One grant: PENDING, ACTIVE, REVOKED or EXPIRED, and once ACTIVE the accounts shared. */
|
|
196
|
+
async ctraderGrant(grantId) {
|
|
197
|
+
const id = uuid("grantId", grantId);
|
|
198
|
+
const path = `/accounts/ctrader/grants/${id}`;
|
|
199
|
+
return (await this.#client.read({ method: "GET", path }));
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Make some of an ACTIVE grant's accounts trading accounts of yours, by cTrader's ids (1 to
|
|
203
|
+
* 50). Each starts PENDING and is CONNECTED once authorised at cTrader.
|
|
204
|
+
*/
|
|
205
|
+
async connectCtraderAccounts(grantId, ctidTraderAccountIds, options) {
|
|
206
|
+
const id = uuid("grantId", grantId);
|
|
207
|
+
const key = idempotencyKey(options.idempotencyKey);
|
|
208
|
+
const ids = ctidTraderAccountIds.map(String);
|
|
209
|
+
if (ids.length === 0)
|
|
210
|
+
throw new RangeError("connect at least one account");
|
|
211
|
+
const body = (await this.#client.write({
|
|
212
|
+
method: "POST",
|
|
213
|
+
path: `/accounts/ctrader/grants/${id}/accounts`,
|
|
214
|
+
body: { ctid_trader_account_ids: ids },
|
|
215
|
+
idempotencyKey: key,
|
|
216
|
+
}));
|
|
217
|
+
return body.items;
|
|
218
|
+
}
|
|
219
|
+
/** Revoke a grant: its accounts move to REAUTH_NEEDED; a new grant reconnects them. */
|
|
220
|
+
async revokeCtraderGrant(grantId, options) {
|
|
221
|
+
const id = uuid("grantId", grantId);
|
|
222
|
+
return (await this.#client.write({
|
|
223
|
+
method: "DELETE",
|
|
224
|
+
path: `/accounts/ctrader/grants/${id}`,
|
|
225
|
+
idempotencyKey: idempotencyKey(options.idempotencyKey),
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
}
|