@cexyio/cexy 0.1.0-dev.3 → 0.1.0-dev.5
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/README.md +33 -9
- package/dist/index.cjs +157 -44
- package/dist/index.d.cts +155 -29
- package/dist/index.d.ts +155 -29
- package/dist/index.js +155 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,27 @@ await cexy.trading.cancelAll({ symbol: "BTC/USDT" }); // { symbol: null } = ever
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
`cancelAll` requires `symbol`: the server treats a missing symbol as "every market", so the SDK makes
|
|
63
|
-
you say so with `{ symbol: null }`.
|
|
63
|
+
you say so with `{ symbol: null }`. An unknown symbol throws `NotFoundError`.
|
|
64
|
+
|
|
65
|
+
One call handles at most 500 orders and puts each in exactly one list: `cancelled`, `already_closed`
|
|
66
|
+
(it filled, was refused or was cancelled elsewhere first; not an error) or `failed`, with the reason in
|
|
67
|
+
`failures` (`INVALID_STATE` for an order still being placed). `has_more: true` means more orders remain.
|
|
68
|
+
To let the SDK repeat the call for you:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const res = await cexy.trading.cancelAll({ symbol: null, untilDone: true });
|
|
72
|
+
// res.stopped: "done" | "max_rounds" | "time_budget"; res.rounds: calls made
|
|
73
|
+
if (res.stopped !== "done" || res.failed.length) console.warn("left over:", res.failures);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`untilDone` repeats while `has_more` is true or a failure is `INVALID_STATE` / `SERVICE_UNAVAILABLE`.
|
|
77
|
+
After a call without progress it waits 1, 2, 4, 8, then 15 s, and it stops after `maxRounds` calls
|
|
78
|
+
(default 20) or before a wait would pass `timeBudgetMs` (default 120 000). Every round is exactly one
|
|
79
|
+
request (the loop owns the retries, so it never sends more than `maxRounds` requests): a 429 round waits its
|
|
80
|
+
Retry-After, which counts against the budget; another retryable error (5xx, network) takes the next backoff
|
|
81
|
+
step; a wait that would pass the budget ends the loop with `stopped: "time_budget"` and `last_error_code`.
|
|
82
|
+
A non-retryable error (e.g. a key without the trade scope) throws `CancelAllInterruptedError` with the error
|
|
83
|
+
and the partial result. The server allows 30 cancel-all calls per minute per account.
|
|
64
84
|
|
|
65
85
|
Give both `apiKey` and `apiSecret`, or neither: passing only one throws at construction.
|
|
66
86
|
|
|
@@ -110,7 +130,7 @@ Every API failure throws a `CexyApiError` (or a subclass) with `status`, `code`,
|
|
|
110
130
|
| `UnprocessableError` | 422: `INSUFFICIENT_FUNDS`, `MARKET_UNAVAILABLE`, ... |
|
|
111
131
|
| `RateLimitError` | 429, with `retryAfterMs` |
|
|
112
132
|
| `ServerError` | 5xx |
|
|
113
|
-
| `CexyApiError` | any code this SDK version does not know yet |
|
|
133
|
+
| `CexyApiError` | any code this SDK version does not know yet, and `UNEXPECTED_REDIRECT` (the server answered with a 3xx; set by the SDK, see `CLIENT_ERROR_CODES`) |
|
|
114
134
|
|
|
115
135
|
Local problems use `CexyConfigError`, `InvalidAmountError`, `CexyConnectionError` / `CexyTimeoutError`
|
|
116
136
|
and `OrderStateUnknownError`. `ErrorCode` is a union of the known codes plus `string`, because new codes
|
|
@@ -129,10 +149,13 @@ try {
|
|
|
129
149
|
|
|
130
150
|
- Timeout per attempt: `timeoutMs` (default 10 s). Retries: `maxRetries` (default 3), exponential backoff with full jitter.
|
|
131
151
|
- Retried: network errors, timeouts and responses with `retryable: true`.
|
|
132
|
-
- 429 waits at least `Retry-After` / `details.retry_after_seconds`.
|
|
152
|
+
- 429 waits at least `Retry-After` / `details.retry_after_seconds`. Server wait hints are untrusted: unusable
|
|
153
|
+
values are ignored, and a hint longer than 120 s (`MAX_SERVER_WAIT_MS`) is never waited: the call fails at
|
|
154
|
+
once with `RateLimitError`, whose `retryAfterMs` still has the server's value. The client-side rate limiter
|
|
155
|
+
never blocks longer than 120 s because of a server hint.
|
|
133
156
|
- GETs retry freely.
|
|
134
|
-
- **Orders:** safety rests on `client_order_id
|
|
135
|
-
|
|
157
|
+
- **Orders:** safety rests on `client_order_id`. The server does not honour `Idempotency-Key` on
|
|
158
|
+
`POST /trading/orders`, order cancels or cancel-all, so the SDK does not send it there. `placeOrder` always sends a
|
|
136
159
|
`client_order_id` (a UUID if you do not set one); it is unique per account and a repeat is refused
|
|
137
160
|
before any funds move. After an ambiguous failure (network error, timeout, 5xx) the SDK first looks the
|
|
138
161
|
order up by that id. If the order exists it is returned with `recovered: true`; only if it does not
|
|
@@ -141,9 +164,9 @@ try {
|
|
|
141
164
|
- **Cancels:** `cancelOrder` retries network errors; if a *retry* gets `INVALID_STATE`, the first attempt
|
|
142
165
|
already cancelled the order, so the SDK fetches and returns it. `cancelAll` is naturally repeatable and
|
|
143
166
|
is retried the same way (a retry reports only what it cancelled).
|
|
144
|
-
- **Pool join/exit** send an
|
|
145
|
-
it there, so they execute once. A 409 `CONCURRENT_MODIFICATION` (the same key
|
|
146
|
-
retried with the same key. Pass `{ idempotencyKey }` to control it yourself.
|
|
167
|
+
- **Pool join/exit** are the only requests that send an `Idempotency-Key` (auto-generated, reused on every
|
|
168
|
+
retry); the server honours it there, so they execute once. A 409 `CONCURRENT_MODIFICATION` (the same key
|
|
169
|
+
still in flight) is retried with the same key. Pass `{ idempotencyKey }` to control it yourself.
|
|
147
170
|
- `onRetry` lets you log retries.
|
|
148
171
|
|
|
149
172
|
Every method takes a last `RequestOptions` argument: `{ signal, timeoutMs, maxRetries, idempotencyKey }`.
|
|
@@ -231,7 +254,8 @@ endpoints from a server. Browsers do not let scripts set `User-Agent`, so the SD
|
|
|
231
254
|
`http://` / `ws://` solely for `localhost`, `127.0.0.1` or `::1` (local test servers).
|
|
232
255
|
- The SDK **never follows HTTP redirects**. A 3xx answer throws a `CexyApiError` with code
|
|
233
256
|
`UNEXPECTED_REDIRECT` (not retried), so credentials are never re-sent to another host and an order
|
|
234
|
-
is never re-posted to a redirect target. If you pass your own `fetch`, it must honour `redirect: "manual"
|
|
257
|
+
is never re-posted to a redirect target. If you pass your own `fetch`, it must honour `redirect: "manual"`: one that follows redirects
|
|
258
|
+
anyway has already sent your credentials by the time the SDK notices.
|
|
235
259
|
- The SDK redacts the secret from `toString()`, `util.inspect`, `JSON.stringify` and error messages.
|
|
236
260
|
- Keep keys in environment variables or a secret manager, not in code.
|
|
237
261
|
|
package/dist/index.cjs
CHANGED
|
@@ -31,6 +31,16 @@ var OrderStateUnknownError = class extends CexyError {
|
|
|
31
31
|
this.clientOrderId = clientOrderId;
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
+
var CancelAllInterruptedError = class extends CexyError {
|
|
35
|
+
error;
|
|
36
|
+
partial;
|
|
37
|
+
constructor(error, partial) {
|
|
38
|
+
const what = error instanceof CexyApiError ? `${error.code}: ${error.message}` : String(error);
|
|
39
|
+
super(`cancelAll stopped after ${partial.rounds} round(s): ${what}`, { cause: error });
|
|
40
|
+
this.error = error;
|
|
41
|
+
this.partial = partial;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
34
44
|
var CexyApiError = class extends CexyError {
|
|
35
45
|
status;
|
|
36
46
|
code;
|
|
@@ -131,24 +141,27 @@ var KNOWN_CODES = /* @__PURE__ */ new Set([
|
|
|
131
141
|
"UNDER_MAINTENANCE",
|
|
132
142
|
"ENGINE_OVERLOADED"
|
|
133
143
|
]);
|
|
144
|
+
var CLIENT_ERROR_CODES = { UNEXPECTED_REDIRECT: "UNEXPECTED_REDIRECT" };
|
|
134
145
|
function isKnownErrorCode(code) {
|
|
135
146
|
return KNOWN_CODES.has(code);
|
|
136
147
|
}
|
|
137
148
|
var DEFAULT_RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
|
|
149
|
+
var MAX_SERVER_WAIT_MS = 12e4;
|
|
138
150
|
function retryAfterMs(headers, details) {
|
|
139
151
|
let best = null;
|
|
140
|
-
const h = headers?.get("retry-after");
|
|
152
|
+
const h = headers?.get("retry-after")?.trim();
|
|
141
153
|
if (h) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
if (/^\d+(\.\d+)?$/.test(h)) {
|
|
155
|
+
const ms = Number(h) * 1e3;
|
|
156
|
+
if (Number.isFinite(ms)) best = ms;
|
|
157
|
+
} else if (/[a-z]/i.test(h)) {
|
|
145
158
|
const at = Date.parse(h);
|
|
146
|
-
if (
|
|
159
|
+
if (Number.isFinite(at)) best = Math.max(0, at - Date.now());
|
|
147
160
|
}
|
|
148
161
|
}
|
|
149
162
|
const d = details?.["retry_after_seconds"];
|
|
150
|
-
const ds = typeof d === "number" ? d : typeof d === "string" ? Number(d) : NaN;
|
|
151
|
-
if (Number.isFinite(ds)) best = Math.max(best ?? 0, ds * 1e3);
|
|
163
|
+
const ds = typeof d === "number" ? d : typeof d === "string" && /^\d+(\.\d+)?$/.test(d.trim()) ? Number(d) : NaN;
|
|
164
|
+
if (Number.isFinite(ds) && ds >= 0 && Number.isFinite(ds * 1e3)) best = Math.max(best ?? 0, ds * 1e3);
|
|
152
165
|
return best;
|
|
153
166
|
}
|
|
154
167
|
function errorFromResponse(status, body, headers, redact = (t) => t) {
|
|
@@ -311,6 +324,7 @@ var OPERATIONS = {
|
|
|
311
324
|
};
|
|
312
325
|
|
|
313
326
|
// src/http.ts
|
|
327
|
+
var IDEMPOTENT_OPS = /* @__PURE__ */ new Set(["join_pool", "exit_pool"]);
|
|
314
328
|
var BACKOFF_BASE_MS = 500;
|
|
315
329
|
var BACKOFF_MAX_MS = 1e4;
|
|
316
330
|
var Transport = class {
|
|
@@ -319,16 +333,15 @@ var Transport = class {
|
|
|
319
333
|
this.config = config;
|
|
320
334
|
}
|
|
321
335
|
/**
|
|
322
|
-
* Sends a request with the standard retry policy:
|
|
323
|
-
*
|
|
324
|
-
* honours it
|
|
325
|
-
*
|
|
336
|
+
* Sends a request with the standard retry policy: retryable errors and network failures are
|
|
337
|
+
* retried. Pool join/exit carry an `Idempotency-Key` reused on every attempt (the server
|
|
338
|
+
* honours it there, which makes their retries safe); the other mutations routed here
|
|
339
|
+
* (cancel-all) are naturally repeatable and send no key. `placeOrder` and `cancelOrder` use
|
|
326
340
|
* `attempt()` with their own policies.
|
|
327
341
|
*/
|
|
328
342
|
async request(spec, opts = {}) {
|
|
329
343
|
const info = OPERATIONS[spec.op];
|
|
330
|
-
const
|
|
331
|
-
const idempotencyKey = isMutation ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
|
|
344
|
+
const idempotencyKey = IDEMPOTENT_OPS.has(spec.op) ? spec.idempotencyKey ?? opts.idempotencyKey ?? newId() : void 0;
|
|
332
345
|
const maxRetries = opts.maxRetries ?? this.config.maxRetries;
|
|
333
346
|
for (let attempt = 0; ; attempt++) {
|
|
334
347
|
try {
|
|
@@ -339,8 +352,13 @@ var Transport = class {
|
|
|
339
352
|
}
|
|
340
353
|
}
|
|
341
354
|
}
|
|
342
|
-
/**
|
|
355
|
+
/**
|
|
356
|
+
* Waits before retry number `attempt + 1`, honouring server hints. A server hint longer than
|
|
357
|
+
* `MAX_SERVER_WAIT_MS` is not waited: `err` is thrown at once (it still carries the hint).
|
|
358
|
+
*/
|
|
343
359
|
async backoff(op2, info, attempt, err, idempotencyKey, signal) {
|
|
360
|
+
const hint = serverHintMs(err);
|
|
361
|
+
if (hint !== null && hint > MAX_SERVER_WAIT_MS) throw err;
|
|
344
362
|
const delayMs = this.retryDelay(attempt, err);
|
|
345
363
|
this.config.onRetry?.({
|
|
346
364
|
operation: op2,
|
|
@@ -355,8 +373,8 @@ var Transport = class {
|
|
|
355
373
|
}
|
|
356
374
|
/** Full-jitter exponential backoff, or the server's hint plus a little jitter. */
|
|
357
375
|
retryDelay(attempt, err) {
|
|
358
|
-
const hint =
|
|
359
|
-
if (hint !== null && hint > 0) return Math.ceil(hint + this.config.random() * 250);
|
|
376
|
+
const hint = serverHintMs(err);
|
|
377
|
+
if (hint !== null && hint > 0 && hint <= MAX_SERVER_WAIT_MS) return Math.ceil(hint + this.config.random() * 250);
|
|
360
378
|
const cap = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** attempt);
|
|
361
379
|
return Math.ceil(this.config.random() * cap);
|
|
362
380
|
}
|
|
@@ -371,7 +389,7 @@ var Transport = class {
|
|
|
371
389
|
body = JSON.stringify(spec.body);
|
|
372
390
|
headers.set("Content-Type", "application/json");
|
|
373
391
|
}
|
|
374
|
-
if (
|
|
392
|
+
if (IDEMPOTENT_OPS.has(spec.op) && spec.idempotencyKey) headers.set("Idempotency-Key", spec.idempotencyKey);
|
|
375
393
|
if (info.auth === "api_key") {
|
|
376
394
|
const auth = this.config.authenticator;
|
|
377
395
|
if (!auth) {
|
|
@@ -415,7 +433,7 @@ var Transport = class {
|
|
|
415
433
|
if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400 || res.redirected) {
|
|
416
434
|
throw new CexyApiError({
|
|
417
435
|
status: res.status,
|
|
418
|
-
code:
|
|
436
|
+
code: CLIENT_ERROR_CODES.UNEXPECTED_REDIRECT,
|
|
419
437
|
message: `${info.method} ${info.path}: the server answered with a redirect (HTTP ${res.status}); the SDK does not follow redirects. Check baseUrl.`,
|
|
420
438
|
retryable: false
|
|
421
439
|
});
|
|
@@ -454,6 +472,11 @@ var Transport = class {
|
|
|
454
472
|
return this.config.authenticator ? this.config.authenticator.redact(text) : text;
|
|
455
473
|
}
|
|
456
474
|
};
|
|
475
|
+
function serverHintMs(err) {
|
|
476
|
+
if (err instanceof RateLimitError) return err.retryAfterMs;
|
|
477
|
+
if (err instanceof CexyApiError) return retryAfterMs(void 0, err.details);
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
457
480
|
function isRetryable(err) {
|
|
458
481
|
if (err instanceof CexyConnectionError) return true;
|
|
459
482
|
if (err instanceof CexyApiError) return err.retryable || err.code === "CONCURRENT_MODIFICATION";
|
|
@@ -485,6 +508,7 @@ function errMessage(err) {
|
|
|
485
508
|
}
|
|
486
509
|
|
|
487
510
|
// src/limiter.ts
|
|
511
|
+
var MAX_BLOCK_MS = 12e4;
|
|
488
512
|
var RateLimiter = class {
|
|
489
513
|
#rpm;
|
|
490
514
|
#tokens;
|
|
@@ -518,14 +542,14 @@ var RateLimiter = class {
|
|
|
518
542
|
return;
|
|
519
543
|
}
|
|
520
544
|
const msPerToken = 6e4 / this.#rpm;
|
|
521
|
-
await this.#sleep(Math.ceil((1 - this.#tokens) * msPerToken), signal);
|
|
545
|
+
await this.#sleep(Math.min(MAX_BLOCK_MS, Math.ceil((1 - this.#tokens) * msPerToken)), signal);
|
|
522
546
|
}
|
|
523
547
|
}
|
|
524
548
|
/** Adapts to the server's rate-limit headers. Never raises the configured limit. */
|
|
525
549
|
update(headers) {
|
|
526
550
|
this.#refill();
|
|
527
551
|
const limit = num(headers.get("x-ratelimit-limit"));
|
|
528
|
-
if (limit !== null && limit
|
|
552
|
+
if (limit !== null && limit >= 1 && limit < this.#rpm) {
|
|
529
553
|
this.#rpm = limit;
|
|
530
554
|
this.#tokens = Math.min(this.#tokens, limit);
|
|
531
555
|
}
|
|
@@ -537,10 +561,13 @@ var RateLimiter = class {
|
|
|
537
561
|
else this.blockFor(6e4 / this.#rpm);
|
|
538
562
|
}
|
|
539
563
|
}
|
|
540
|
-
/**
|
|
564
|
+
/**
|
|
565
|
+
* Blocks all requests for `ms` (used for 429 Retry-After and `X-RateLimit-Reset`). Server
|
|
566
|
+
* hints are untrusted: non-finite values are ignored and the block is capped at 120 s.
|
|
567
|
+
*/
|
|
541
568
|
blockFor(ms) {
|
|
542
|
-
if (!(ms > 0)) return;
|
|
543
|
-
this.#blockedUntil = Math.max(this.#blockedUntil, this.#now() + ms);
|
|
569
|
+
if (!(ms > 0) || !Number.isFinite(ms)) return;
|
|
570
|
+
this.#blockedUntil = Math.max(this.#blockedUntil, this.#now() + Math.min(ms, MAX_BLOCK_MS));
|
|
544
571
|
}
|
|
545
572
|
#refill() {
|
|
546
573
|
const now = this.#now();
|
|
@@ -790,6 +817,14 @@ var WalletResource = class extends Resource {
|
|
|
790
817
|
return this.data({ op: "deposit_address", query: params }, opts);
|
|
791
818
|
}
|
|
792
819
|
};
|
|
820
|
+
var CANCEL_RETRY_CODES = /* @__PURE__ */ new Set(["INVALID_STATE", "SERVICE_UNAVAILABLE"]);
|
|
821
|
+
var CANCEL_BACKOFF_S = [1, 2, 4, 8, 15];
|
|
822
|
+
function errorCode(err) {
|
|
823
|
+
if (err instanceof CexyApiError) return err.code;
|
|
824
|
+
if (err instanceof CexyTimeoutError) return "TIMEOUT";
|
|
825
|
+
if (err instanceof CexyConnectionError) return "CONNECTION_ERROR";
|
|
826
|
+
return "ERROR";
|
|
827
|
+
}
|
|
793
828
|
var ORDER_AMOUNT_FIELDS = ["price", "quantity", "quote_quantity", "stop_price"];
|
|
794
829
|
var TradingResource = class extends Resource {
|
|
795
830
|
/** Open orders, optionally filtered by market/status. */
|
|
@@ -820,7 +855,7 @@ var TradingResource = class extends Resource {
|
|
|
820
855
|
*
|
|
821
856
|
* Retry safety rests on `client_order_id` (generated as a UUID when absent): it is unique
|
|
822
857
|
* per account and the server refuses a repeat before any funds move. The server does NOT
|
|
823
|
-
* honour `Idempotency-Key` on orders
|
|
858
|
+
* honour `Idempotency-Key` on orders, so none is sent. After an
|
|
824
859
|
* ambiguous failure (network error, timeout or 5xx) the SDK first looks the order up by
|
|
825
860
|
* `client_order_id` and returns it if it exists (`recovered: true`); only if it does not
|
|
826
861
|
* exist does it send the order again, with the same `client_order_id`, so a late-arriving
|
|
@@ -835,12 +870,11 @@ var TradingResource = class extends Resource {
|
|
|
835
870
|
assertAmountFields(order, ORDER_AMOUNT_FIELDS, "placeOrder");
|
|
836
871
|
const clientOrderId = order.client_order_id ?? newId();
|
|
837
872
|
const body = { ...order, client_order_id: clientOrderId };
|
|
838
|
-
const idempotencyKey = opts.idempotencyKey ?? newId();
|
|
839
873
|
const maxRetries = opts.maxRetries ?? this.t.config.maxRetries;
|
|
840
874
|
const info = OPERATIONS.place_order;
|
|
841
875
|
for (let attempt = 0; ; attempt++) {
|
|
842
876
|
try {
|
|
843
|
-
const raw = await this.t.attempt({ op: "place_order", body
|
|
877
|
+
const raw = await this.t.attempt({ op: "place_order", body }, opts);
|
|
844
878
|
const data = raw.data.data;
|
|
845
879
|
return { ...data, client_order_id: clientOrderId, recovered: false };
|
|
846
880
|
} catch (err) {
|
|
@@ -850,11 +884,11 @@ var TradingResource = class extends Resource {
|
|
|
850
884
|
const existing = await this.#lookup(clientOrderId, err, opts);
|
|
851
885
|
if (existing) return { order: existing, fills: [], client_order_id: clientOrderId, recovered: true };
|
|
852
886
|
if (duplicateAfterRetry || attempt >= maxRetries) throw err;
|
|
853
|
-
await this.t.backoff("place_order", info, attempt, err,
|
|
887
|
+
await this.t.backoff("place_order", info, attempt, err, void 0, opts.signal);
|
|
854
888
|
continue;
|
|
855
889
|
}
|
|
856
890
|
if (isRetryable(err) && attempt < maxRetries) {
|
|
857
|
-
await this.t.backoff("place_order", info, attempt, err,
|
|
891
|
+
await this.t.backoff("place_order", info, attempt, err, void 0, opts.signal);
|
|
858
892
|
continue;
|
|
859
893
|
}
|
|
860
894
|
throw err;
|
|
@@ -878,10 +912,9 @@ var TradingResource = class extends Resource {
|
|
|
878
912
|
async cancelOrder(orderId, opts = {}) {
|
|
879
913
|
const maxRetries = opts.maxRetries ?? this.t.config.maxRetries;
|
|
880
914
|
const info = OPERATIONS.cancel_order;
|
|
881
|
-
const idempotencyKey = opts.idempotencyKey ?? newId();
|
|
882
915
|
for (let attempt = 0; ; attempt++) {
|
|
883
916
|
try {
|
|
884
|
-
const raw = await this.t.attempt({ op: "cancel_order", pathParams: { order_id: orderId }
|
|
917
|
+
const raw = await this.t.attempt({ op: "cancel_order", pathParams: { order_id: orderId } }, opts);
|
|
885
918
|
return raw.data.data;
|
|
886
919
|
} catch (err) {
|
|
887
920
|
if (opts.signal?.aborted) throw err;
|
|
@@ -889,23 +922,13 @@ var TradingResource = class extends Resource {
|
|
|
889
922
|
return this.order(orderId, { signal: opts.signal, timeoutMs: opts.timeoutMs });
|
|
890
923
|
}
|
|
891
924
|
if (isRetryable(err) && attempt < maxRetries) {
|
|
892
|
-
await this.t.backoff("cancel_order", info, attempt, err,
|
|
925
|
+
await this.t.backoff("cancel_order", info, attempt, err, void 0, opts.signal);
|
|
893
926
|
continue;
|
|
894
927
|
}
|
|
895
928
|
throw err;
|
|
896
929
|
}
|
|
897
930
|
}
|
|
898
931
|
}
|
|
899
|
-
/**
|
|
900
|
-
* Cancels every open order in one market: `cancelAll({ symbol: "BTC/USDT" })`.
|
|
901
|
-
* To cancel across ALL markets, pass `symbol: null` explicitly: `cancelAll({ symbol: null })`.
|
|
902
|
-
* Omitting `symbol` is an error, so an account-wide cancel never happens by accident
|
|
903
|
-
* (the server itself treats `{}` as every market).
|
|
904
|
-
*
|
|
905
|
-
* The server limits cancel-all to 30 calls per minute per account. It is naturally
|
|
906
|
-
* repeatable, so it is retried after network errors; a retry reports only what that retry
|
|
907
|
-
* cancelled.
|
|
908
|
-
*/
|
|
909
932
|
async cancelAll(params, opts) {
|
|
910
933
|
const hasSymbol = !!params && typeof params === "object" && Object.prototype.hasOwnProperty.call(params, "symbol");
|
|
911
934
|
const symbol = hasSymbol ? params.symbol : void 0;
|
|
@@ -913,12 +936,97 @@ var TradingResource = class extends Resource {
|
|
|
913
936
|
if (typeof symbol === "string" && symbol !== "") body = { symbol };
|
|
914
937
|
else if (hasSymbol && symbol === null) body = {};
|
|
915
938
|
else throw new CexyConfigError('cancelAll(): pass { symbol: "BASE/QUOTE" }, or { symbol: null } to cancel in every market');
|
|
916
|
-
|
|
939
|
+
const once = (o) => this.data({ op: "cancel_all", body }, o ?? opts);
|
|
940
|
+
if (params.untilDone !== true) return once();
|
|
941
|
+
const maxRounds = params.maxRounds ?? 20;
|
|
942
|
+
const budgetMs = params.timeBudgetMs ?? 12e4;
|
|
943
|
+
if (!(maxRounds >= 1)) throw new CexyConfigError("cancelAll(): maxRounds must be >= 1");
|
|
944
|
+
if (!(budgetMs > 0)) throw new CexyConfigError("cancelAll(): timeBudgetMs must be > 0");
|
|
945
|
+
const now = this.t.config.now ?? Date.now;
|
|
946
|
+
const start = now();
|
|
947
|
+
const state = /* @__PURE__ */ new Map();
|
|
948
|
+
const roundOpts = { ...opts, maxRetries: 0 };
|
|
949
|
+
let rounds = 0;
|
|
950
|
+
let idle = 0;
|
|
951
|
+
let hasMore = false;
|
|
952
|
+
let lastErrorCode;
|
|
953
|
+
let stopped;
|
|
954
|
+
const result = () => {
|
|
955
|
+
const pick = (list) => [...state].filter(([, v]) => v.list === list).map(([id]) => id);
|
|
956
|
+
return {
|
|
957
|
+
cancelled: pick("cancelled"),
|
|
958
|
+
already_closed: pick("already_closed"),
|
|
959
|
+
failed: pick("failed"),
|
|
960
|
+
failures: [...state.values()].flatMap((v) => v.list === "failed" && v.failure ? [v.failure] : []),
|
|
961
|
+
has_more: hasMore,
|
|
962
|
+
rounds,
|
|
963
|
+
stopped,
|
|
964
|
+
...lastErrorCode !== void 0 ? { last_error_code: lastErrorCode } : {}
|
|
965
|
+
};
|
|
966
|
+
};
|
|
967
|
+
for (; ; ) {
|
|
968
|
+
let waitMs = 0;
|
|
969
|
+
let res;
|
|
970
|
+
try {
|
|
971
|
+
res = await once(roundOpts);
|
|
972
|
+
} catch (err) {
|
|
973
|
+
rounds++;
|
|
974
|
+
if (opts?.signal?.aborted) throw err;
|
|
975
|
+
if (!isRetryable(err)) {
|
|
976
|
+
stopped = "done";
|
|
977
|
+
throw new CancelAllInterruptedError(err, result());
|
|
978
|
+
}
|
|
979
|
+
lastErrorCode = errorCode(err);
|
|
980
|
+
if (rounds >= maxRounds) {
|
|
981
|
+
stopped = "max_rounds";
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
const hint = err instanceof RateLimitError ? err.retryAfterMs : null;
|
|
985
|
+
if (hint !== null) {
|
|
986
|
+
waitMs = hint;
|
|
987
|
+
if (hint > MAX_SERVER_WAIT_MS) {
|
|
988
|
+
stopped = "time_budget";
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
if (res) {
|
|
996
|
+
rounds++;
|
|
997
|
+
lastErrorCode = void 0;
|
|
998
|
+
hasMore = res.has_more;
|
|
999
|
+
const failures = res.failures ?? [];
|
|
1000
|
+
for (const id of res.cancelled ?? []) state.set(id, { list: "cancelled" });
|
|
1001
|
+
for (const id of res.already_closed ?? []) state.set(id, { list: "already_closed" });
|
|
1002
|
+
for (const id of res.failed ?? []) {
|
|
1003
|
+
const failure = failures.find((f) => f.order_id === id);
|
|
1004
|
+
state.set(id, failure ? { list: "failed", failure } : { list: "failed" });
|
|
1005
|
+
}
|
|
1006
|
+
const progress = (res.cancelled?.length ?? 0) + (res.already_closed?.length ?? 0) > 0;
|
|
1007
|
+
if (!res.has_more && !failures.some((f) => CANCEL_RETRY_CODES.has(f.code))) {
|
|
1008
|
+
stopped = "done";
|
|
1009
|
+
break;
|
|
1010
|
+
}
|
|
1011
|
+
if (rounds >= maxRounds) {
|
|
1012
|
+
stopped = "max_rounds";
|
|
1013
|
+
break;
|
|
1014
|
+
}
|
|
1015
|
+
if (progress) idle = 0;
|
|
1016
|
+
else waitMs = (CANCEL_BACKOFF_S[Math.min(idle++, CANCEL_BACKOFF_S.length - 1)] ?? 15) * 1e3;
|
|
1017
|
+
}
|
|
1018
|
+
if (now() - start + waitMs >= budgetMs) {
|
|
1019
|
+
stopped = "time_budget";
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
if (waitMs > 0) await this.t.config.sleep(waitMs, opts?.signal);
|
|
1023
|
+
}
|
|
1024
|
+
return result();
|
|
917
1025
|
}
|
|
918
1026
|
};
|
|
919
1027
|
|
|
920
1028
|
// src/version.ts
|
|
921
|
-
var VERSION = "0.1.0-dev.
|
|
1029
|
+
var VERSION = "0.1.0-dev.5";
|
|
922
1030
|
var USER_AGENT = `cexy-typescript/${VERSION}`;
|
|
923
1031
|
|
|
924
1032
|
// src/ws/emitter.ts
|
|
@@ -1686,7 +1794,8 @@ var CexyClient = class {
|
|
|
1686
1794
|
const sleep2 = options.sleep ?? sleep;
|
|
1687
1795
|
const limiter = options.rateLimit === false ? null : new RateLimiter({
|
|
1688
1796
|
requestsPerMinute: options.rateLimit?.requestsPerMinute ?? (authenticator ? DEFAULT_RPM_WITH_KEY : DEFAULT_RPM_ANONYMOUS),
|
|
1689
|
-
sleep: sleep2
|
|
1797
|
+
sleep: sleep2,
|
|
1798
|
+
...options.now ? { now: options.now } : {}
|
|
1690
1799
|
});
|
|
1691
1800
|
this.#baseUrl = baseUrl;
|
|
1692
1801
|
this.#transport = new Transport({
|
|
@@ -1699,6 +1808,7 @@ var CexyClient = class {
|
|
|
1699
1808
|
userAgent: canSetUserAgent() ? [USER_AGENT, options.userAgentSuffix].filter(Boolean).join(" ") : null,
|
|
1700
1809
|
sleep: sleep2,
|
|
1701
1810
|
random: options.random ?? Math.random,
|
|
1811
|
+
now: options.now ?? Date.now,
|
|
1702
1812
|
onRetry: options.onRetry
|
|
1703
1813
|
});
|
|
1704
1814
|
const t = this.#transport;
|
|
@@ -1772,6 +1882,8 @@ exports.AccountResource = AccountResource;
|
|
|
1772
1882
|
exports.ApiKeyAuthenticator = ApiKeyAuthenticator;
|
|
1773
1883
|
exports.AssetsResource = AssetsResource;
|
|
1774
1884
|
exports.AuthenticationError = AuthenticationError;
|
|
1885
|
+
exports.CLIENT_ERROR_CODES = CLIENT_ERROR_CODES;
|
|
1886
|
+
exports.CancelAllInterruptedError = CancelAllInterruptedError;
|
|
1775
1887
|
exports.CexyApiError = CexyApiError;
|
|
1776
1888
|
exports.CexyClient = CexyClient;
|
|
1777
1889
|
exports.CexyConfigError = CexyConfigError;
|
|
@@ -1792,6 +1904,7 @@ exports.InvalidAmountError = InvalidAmountError;
|
|
|
1792
1904
|
exports.JurisdictionBlockedError = JurisdictionBlockedError;
|
|
1793
1905
|
exports.KNOWN_EVENT_TYPES = KNOWN_EVENT_TYPES;
|
|
1794
1906
|
exports.LiveOrderBook = LiveOrderBook;
|
|
1907
|
+
exports.MAX_SERVER_WAIT_MS = MAX_SERVER_WAIT_MS;
|
|
1795
1908
|
exports.MarketsResource = MarketsResource;
|
|
1796
1909
|
exports.NetworksResource = NetworksResource;
|
|
1797
1910
|
exports.NotFoundError = NotFoundError;
|