@mfxhub/sdk 0.0.0 → 0.1.1

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.
@@ -0,0 +1,119 @@
1
+ // `client.execution`: placing orders.
2
+ //
3
+ // Every method here resolves to an ExecutionResult and never rejects for anything the platform or
4
+ // the network does. A transport fault, a timeout, a rate limit, a refusal and a broker rejection
5
+ // are all outcomes of trying to trade. Only a wrong argument throws, before anything is sent.
6
+ //
7
+ // Three rules show up in every signature:
8
+ // - The idempotency key is required and has no default. A retry reuses the same key and gets the
9
+ // original command back with `duplicate: true`, never a second order.
10
+ // - Nothing is retried automatically: not a timeout (the order may have been placed), not a rate
11
+ // limit, not a 5xx. The SDK reports; the caller decides, from `retryable`.
12
+ // - Prices and volumes are decimal strings; a number is refused (see decimals.ts).
13
+ import { idempotencyKey, uuid } from "./arguments.js";
14
+ import { throwFor } from "./client.js";
15
+ import { decimal } from "./decimals.js";
16
+ import { MaintenanceError, MfxApiError, MfxTimeout, MfxTransportError, RateLimitError, } from "./errors.js";
17
+ import { concluded, fromCommand } from "./results.js";
18
+ function whole(name, value, min, max) {
19
+ if (value === undefined)
20
+ return undefined;
21
+ if (typeof value !== "number" || !Number.isInteger(value)) {
22
+ throw new TypeError(`${name} is a whole number`);
23
+ }
24
+ if (value < min || value > max)
25
+ throw new RangeError(`${name} is ${min} to ${max}; got ${value}`);
26
+ return value;
27
+ }
28
+ function orderBody(order) {
29
+ const side = order.side;
30
+ if (side !== "buy" && side !== "sell")
31
+ throw new TypeError(`side is "buy" or "sell"`);
32
+ if (typeof order.symbol !== "string" || order.symbol === "") {
33
+ throw new TypeError("symbol is the broker's symbol name");
34
+ }
35
+ if (order.comment !== undefined && order.comment.length > 31) {
36
+ throw new RangeError("comment is at most 31 characters; MT5 would cut a longer one");
37
+ }
38
+ const expiresAt = order.expiresAt instanceof Date ? order.expiresAt.toISOString() : order.expiresAt;
39
+ const optional = {
40
+ order_type: order.orderType,
41
+ price: order.price === undefined ? undefined : decimal("price", order.price),
42
+ stop_limit_price: order.stopLimitPrice === undefined
43
+ ? undefined
44
+ : decimal("stopLimitPrice", order.stopLimitPrice),
45
+ sl: order.sl === undefined ? undefined : decimal("sl", order.sl),
46
+ tp: order.tp === undefined ? undefined : decimal("tp", order.tp),
47
+ deviation_points: whole("deviationPoints", order.deviationPoints, 0, 2 ** 31 - 1),
48
+ expires_at: expiresAt,
49
+ expires_in_ms: whole("expiresInMs", order.expiresInMs, 1000, 60_000),
50
+ magic: whole("magic", order.magic, 0, Number.MAX_SAFE_INTEGER),
51
+ comment: order.comment,
52
+ };
53
+ return {
54
+ symbol: order.symbol,
55
+ side,
56
+ volume: decimal("volume", order.volume),
57
+ ...Object.fromEntries(Object.entries(optional).filter(([, v]) => v !== undefined)),
58
+ };
59
+ }
60
+ /** Order entry for one tenant. Every method resolves to a result; none rejects. */
61
+ export class Execution {
62
+ #client;
63
+ constructor(client) {
64
+ this.#client = client;
65
+ }
66
+ /**
67
+ * Place a market or pending order. Resolves once the platform has persisted the command
68
+ * (state ACCEPTED, with its `commandId`); the outcome follows on the stream.
69
+ *
70
+ * Throws only for a wrong argument, before anything is sent: a number where a decimal string
71
+ * belongs, a missing idempotency key, a comment over 31 characters.
72
+ */
73
+ async placeOrder(terminalId, order) {
74
+ const id = uuid("terminalId", terminalId);
75
+ const key = idempotencyKey(order.idempotencyKey);
76
+ const body = orderBody(order);
77
+ return this.#command({ method: "POST", path: `/terminals/${id}/orders`, body, idempotencyKey: key }, key);
78
+ }
79
+ /** Sends one execution call, once, and turns everything it can do into a result. */
80
+ async #command(spec, key) {
81
+ const started = performance.now();
82
+ const elapsed = () => performance.now() - started;
83
+ const what = `${spec.method} ${spec.path}`;
84
+ let answer;
85
+ try {
86
+ answer = await this.#client.send(spec);
87
+ }
88
+ catch (error) {
89
+ if (error instanceof MfxTimeout) {
90
+ // Never retried, by the SDK or by anyone: the order may have reached the broker.
91
+ return concluded("timeout", key, elapsed(), `${what}: ${error.message}`);
92
+ }
93
+ // A connection that never opened cannot have placed anything; one that dropped after the
94
+ // order was sent may have, and then the result reads UNKNOWN.
95
+ const reached = !(error instanceof MfxTransportError) || error.mayHaveReached;
96
+ return concluded("transport_error", key, elapsed(), `${what}: ${String(error)}`, {
97
+ mayHaveReached: reached,
98
+ });
99
+ }
100
+ try {
101
+ throwFor(answer, spec);
102
+ }
103
+ catch (error) {
104
+ if (error instanceof MaintenanceError) {
105
+ const ends = error.details.ends_at;
106
+ const retryAt = typeof ends === "string" ? new Date(ends) : null;
107
+ return concluded("maintenance", key, elapsed(), error.message, {
108
+ retryAt: retryAt !== null && Number.isNaN(retryAt.getTime()) ? null : retryAt,
109
+ });
110
+ }
111
+ if (error instanceof RateLimitError) {
112
+ return concluded("rate_limited", key, elapsed(), error.message);
113
+ }
114
+ // throwFor throws only the API's typed errors: every other answer is a refusal.
115
+ return concluded("refused", key, elapsed(), `${what}: ${error.message}`);
116
+ }
117
+ return fromCommand(answer.body, key, elapsed());
118
+ }
119
+ }