@forgezero/providers 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/binance.js DELETED
@@ -1,704 +0,0 @@
1
- // src/index.ts
2
- class ProviderError extends Error {
3
- code;
4
- details;
5
- constructor(code, message, details) {
6
- super(message);
7
- this.code = code;
8
- this.details = details;
9
- this.name = "ProviderError";
10
- }
11
- }
12
- function envCredentials(env) {
13
- return {
14
- name: "env",
15
- async get(reference, field) {
16
- const key = `${reference}_${field}`.replace(/[.-]/g, "_").toUpperCase();
17
- const value = env[key];
18
- if (value === undefined) {
19
- throw new ProviderError("CREDENTIAL_MISSING", `Set ${key} in the environment.`);
20
- }
21
- return value;
22
- }
23
- };
24
- }
25
- function chainCredentials(...sources) {
26
- return {
27
- name: sources.map((source) => source.name).join("+"),
28
- async get(reference, field) {
29
- let last;
30
- for (const source of sources) {
31
- try {
32
- return await source.get(reference, field);
33
- } catch (error) {
34
- last = error;
35
- }
36
- }
37
- throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No source held ${reference}.${field}.`);
38
- }
39
- };
40
- }
41
- function scopeCredentials(source, reference) {
42
- return {
43
- name: `${source.name}:${reference}`,
44
- get: (field) => source.get(reference, field)
45
- };
46
- }
47
- function chainScopedCredentials(...sources) {
48
- return {
49
- name: sources.map((source) => source.name).join("+"),
50
- async get(field) {
51
- let last;
52
- for (const source of sources) {
53
- try {
54
- return await source.get(field);
55
- } catch (error) {
56
- last = error;
57
- }
58
- }
59
- throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No scoped source held field "${field}".`);
60
- }
61
- };
62
- }
63
- function staticConfig(config) {
64
- const providers = new Map(config.providers.map((provider) => [provider.instanceKey, provider]));
65
- if (providers.size !== config.providers.length) {
66
- throw new ProviderError("CONFIG_DUPLICATE_INSTANCE", "Provider instance keys must be unique.");
67
- }
68
- const health = new Map;
69
- return {
70
- name: "static",
71
- async provider(instanceKey) {
72
- const provider = providers.get(instanceKey);
73
- return provider ? { ...provider, config: { ...provider.config } } : undefined;
74
- },
75
- async list(serviceKey, methodKey) {
76
- return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
77
- ...attachment,
78
- health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:${attachment.providerMethodVersion ?? "current"}`) ?? (attachment.providerMethodVersion === undefined ? [...health.entries()].find(([key]) => key.startsWith(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:`))?.[1] : undefined) ?? attachment.health
79
- }));
80
- },
81
- async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, providerMethodVersion, next) {
82
- health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}:${providerMethodVersion}`, next);
83
- }
84
- };
85
- }
86
- function defineProviderMethod(method) {
87
- return method;
88
- }
89
- function defineProviderMethodBranches(branches) {
90
- const entries = Object.entries(branches.versions);
91
- if (entries.length === 0 || !branches.versions[branches.currentVersion]) {
92
- throw new ProviderError("PROVIDER_METHOD_VERSION", "A versioned provider method needs a current branch.");
93
- }
94
- for (const [version, method] of entries) {
95
- if (method.version !== version) {
96
- throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method branch "${version}" declares version "${method.version}".`);
97
- }
98
- if (version === branches.currentVersion && method.lifecycle !== "current") {
99
- throw new ProviderError("PROVIDER_METHOD_VERSION", `Current provider method branch "${version}" must have lifecycle current.`);
100
- }
101
- }
102
- return branches;
103
- }
104
- var methodBranch = (provider, methodName, version) => {
105
- const entry = provider?.methods[methodName];
106
- if (!entry)
107
- return;
108
- if ("versions" in entry)
109
- return entry.versions[version ?? entry.currentVersion];
110
- return version === undefined || version === entry.version ? entry : undefined;
111
- };
112
- function defineProvider(spec) {
113
- for (const [name, entry] of Object.entries(spec.methods)) {
114
- if ("versions" in entry)
115
- defineProviderMethodBranches(entry);
116
- else if (!entry.version || !["current", "legacy", "deprecated", "retired"].includes(entry.lifecycle)) {
117
- throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method "${spec.id}.${name}" needs a version and lifecycle.`);
118
- }
119
- }
120
- return spec;
121
- }
122
- function defineSingleMethodProvider(spec) {
123
- const { method, version = "v1", lifecycle = "current", invoke, classify, ...identity } = spec;
124
- return defineProvider({
125
- ...identity,
126
- methods: { [method]: defineProviderMethod({ version, lifecycle, invoke, classify }) }
127
- });
128
- }
129
- var STRIKES_TO_OFFLINE = 3;
130
- function serviceMethod() {
131
- return Object.freeze({});
132
- }
133
- function defineService(definition) {
134
- return definition;
135
- }
136
- function nextHealth(current, kind) {
137
- if (kind === "success")
138
- return { strikes: 0, status: "ok" };
139
- if (kind === "backoff")
140
- return current ?? { strikes: 0, status: "ok" };
141
- const strikes = (current?.strikes ?? 0) + 1;
142
- return {
143
- strikes,
144
- status: strikes >= STRIKES_TO_OFFLINE ? "offline" : "degraded",
145
- lastFailureAtTs: Date.now()
146
- };
147
- }
148
- function createRegistry(options) {
149
- const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
150
- async function call(serviceKey, methodKey, args, callOptions = {}) {
151
- const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
152
- const attempts = [];
153
- for (const attachment of configured) {
154
- const requestedVersion = attachment.providerMethodVersion;
155
- if (callOptions.signal?.aborted) {
156
- const cancelled = {
157
- ok: false,
158
- fallbackUsed: attempts.length > 0,
159
- attempts,
160
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
161
- };
162
- await options.after?.(cancelled);
163
- return cancelled;
164
- }
165
- const instance = await options.config.provider(attachment.instanceKey);
166
- if (!instance) {
167
- attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: "instance not registered" });
168
- continue;
169
- }
170
- const spec = byId.get(instance.providerId);
171
- const method = methodBranch(spec, attachment.providerMethod, requestedVersion);
172
- if (!spec || !method) {
173
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: !spec ? "provider not registered" : "method version not supported" });
174
- continue;
175
- }
176
- if (method.lifecycle === "retired") {
177
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "method version retired" });
178
- continue;
179
- }
180
- if (!instance.enabled) {
181
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "instance disabled" });
182
- continue;
183
- }
184
- if (attachment.health?.status === "offline") {
185
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "offline" });
186
- continue;
187
- }
188
- await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
189
- const startedAt = performance.now();
190
- try {
191
- const result = await method.invoke({
192
- config: instance.config,
193
- secret: (field) => options.credentials.get(instance.secretRef, field),
194
- signal: callOptions.signal
195
- }, args);
196
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "sent", durationMs: performance.now() - startedAt });
197
- await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, "success"));
198
- const sent = {
199
- ok: true,
200
- result,
201
- provider: instance.providerId,
202
- instance: instance.instanceKey,
203
- method: attachment.providerMethod,
204
- selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version },
205
- fallbackUsed: attempts.length > 1,
206
- attempts
207
- };
208
- await options.after?.(sent);
209
- return sent;
210
- } catch (error) {
211
- if (callOptions.signal?.aborted) {
212
- const cancelled = {
213
- ok: false,
214
- fallbackUsed: attempts.length > 0,
215
- attempts,
216
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
217
- };
218
- await options.after?.(cancelled);
219
- return cancelled;
220
- }
221
- const kind = method.classify(error);
222
- const message = error instanceof Error ? error.message : String(error);
223
- const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
224
- attempts.push({
225
- providerId: instance.providerId,
226
- instanceKey: instance.instanceKey,
227
- providerMethod: attachment.providerMethod,
228
- providerMethodVersion: method.version,
229
- outcome: "failed",
230
- kind,
231
- durationMs: performance.now() - startedAt,
232
- failure: { kind, ...code ? { code } : {}, message },
233
- error: message
234
- });
235
- await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, kind));
236
- if (kind === "terminal") {
237
- const refused = {
238
- ok: false,
239
- fallbackUsed: attempts.length > 1,
240
- attempts,
241
- error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
242
- };
243
- await options.after?.(refused);
244
- return refused;
245
- }
246
- }
247
- }
248
- const failed = {
249
- ok: false,
250
- fallbackUsed: attempts.length > 1,
251
- attempts,
252
- error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider method is configured for "${serviceKey}.${methodKey}".` : `Every provider method for "${serviceKey}.${methodKey}" failed or was skipped.`)
253
- };
254
- await options.after?.(failed);
255
- return failed;
256
- }
257
- function service(definition) {
258
- return {
259
- call(method, args, callOptions) {
260
- return call(definition.key, method, args, callOptions);
261
- }
262
- };
263
- }
264
- return { call, service };
265
- }
266
- function createService(definition, methods, options = {}) {
267
- const providers = new Map;
268
- const credentials = new Map;
269
- const instances = [];
270
- const serviceMethods = {};
271
- for (const methodKey of Object.keys(definition.methods)) {
272
- const attachments = methods[methodKey];
273
- if (!Array.isArray(attachments)) {
274
- throw new ProviderError("CONFIG_METHOD_MISSING", `Service method "${definition.key}.${methodKey}" needs an attachment array.`);
275
- }
276
- const priorities = new Set;
277
- serviceMethods[methodKey] = attachments.map((attachment, index) => {
278
- if (!Number.isInteger(attachment.priority) || attachment.priority < 1 || attachment.priority > 1000) {
279
- throw new ProviderError("CONFIG_PRIORITY", `Priority for "${definition.key}.${methodKey}" must be an integer from 1 to 1000.`);
280
- }
281
- if (priorities.has(attachment.priority)) {
282
- throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
283
- }
284
- priorities.add(attachment.priority);
285
- const branch = methodBranch(attachment.provider, attachment.method, attachment.version);
286
- if (!branch || branch.lifecycle === "retired") {
287
- throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support active method "${attachment.method}@${attachment.version ?? "current"}".`);
288
- }
289
- const existing = providers.get(attachment.provider.id);
290
- if (existing && existing !== attachment.provider) {
291
- throw new ProviderError("CONFIG_PROVIDER_DUPLICATE", `Provider id "${attachment.provider.id}" has more than one definition.`);
292
- }
293
- providers.set(attachment.provider.id, attachment.provider);
294
- const internalKey = `${definition.key}:${methodKey}:${index}`;
295
- credentials.set(internalKey, attachment.credentials);
296
- instances.push({
297
- instanceKey: internalKey,
298
- providerId: attachment.provider.id,
299
- enabled: attachment.enabled ?? true,
300
- config: { ...attachment.config ?? {} },
301
- secretRef: internalKey
302
- });
303
- return {
304
- instanceKey: internalKey,
305
- providerMethod: attachment.method,
306
- providerMethodVersion: branch.version,
307
- priority: attachment.priority,
308
- enabled: attachment.enabled ?? true
309
- };
310
- });
311
- }
312
- const registry = createRegistry({
313
- providers: [...providers.values()],
314
- config: staticConfig({ providers: instances, services: { [definition.key]: serviceMethods } }),
315
- credentials: {
316
- name: [...new Set([...credentials.values()].map((source) => source.name))].join("+"),
317
- async get(reference, field) {
318
- const source = credentials.get(reference);
319
- if (!source)
320
- throw new ProviderError("CREDENTIAL_SOURCE_MISSING", "The provider credential source is unavailable.");
321
- return source.get(field);
322
- }
323
- },
324
- ...options
325
- });
326
- const dynamic = registry.service(definition);
327
- return {
328
- async call(method, args, callOptions) {
329
- const result = await dynamic.call(method, args, callOptions);
330
- const { instance: _instance, selected, attempts, ...rest } = result;
331
- return {
332
- ...rest,
333
- ...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod, providerMethodVersion: selected.providerMethodVersion } } : {},
334
- attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
335
- };
336
- }
337
- };
338
- }
339
- var VERSION = "0.1.23";
340
-
341
- // src/http.ts
342
- class BudgetExhausted extends ProviderError {
343
- host;
344
- retryAfterMs;
345
- constructor(host, retryAfterMs) {
346
- super("RATE_BUDGET_EXHAUSTED", `The ${host} budget is spent. Retry in ${retryAfterMs}ms.`);
347
- this.host = host;
348
- this.retryAfterMs = retryAfterMs;
349
- }
350
- }
351
- var windows = new Map;
352
- function resetBudgets() {
353
- windows.clear();
354
- }
355
- function spend(budget, cost, nowMs) {
356
- const ceiling = Math.floor(budget.limit * (budget.headroom ?? 0.9));
357
- const current = windows.get(budget.host);
358
- if (!current || current.resetAtMs <= nowMs) {
359
- windows.set(budget.host, { spent: cost, resetAtMs: nowMs + budget.windowMs });
360
- return;
361
- }
362
- if (current.spent + cost > ceiling) {
363
- throw new BudgetExhausted(budget.host, current.resetAtMs - nowMs);
364
- }
365
- current.spent += cost;
366
- }
367
- function settle(host, reserved, actual) {
368
- const current = windows.get(host);
369
- if (!current)
370
- return;
371
- current.spent = Math.max(0, current.spent - reserved + actual);
372
- }
373
- var budgetState = (host) => windows.get(host);
374
- function createHttpClient(config) {
375
- const doFetch = config.fetch ?? globalThis.fetch;
376
- const timeoutMs = config.timeoutMs ?? 1e4;
377
- return {
378
- async call(request) {
379
- const reserved = request.weight ?? config.budget.defaultCost;
380
- spend(config.budget, reserved, Date.now());
381
- const url = new URL(config.baseUrl + request.path);
382
- for (const [key, value] of Object.entries(request.query ?? {})) {
383
- url.searchParams.set(key, String(value));
384
- }
385
- const controller = new AbortController;
386
- const timer = setTimeout(() => controller.abort(), timeoutMs);
387
- try {
388
- const response = await doFetch(url.toString(), {
389
- method: request.method ?? "GET",
390
- signal: controller.signal,
391
- headers: {
392
- ...request.body === undefined ? {} : { "content-type": "application/json" },
393
- ...request.headers
394
- },
395
- ...request.body === undefined ? {} : { body: JSON.stringify(request.body) }
396
- });
397
- const cost = config.costOf?.(response);
398
- if (cost !== undefined)
399
- settle(config.budget.host, reserved, cost);
400
- const text = await response.text();
401
- let body;
402
- try {
403
- body = text ? JSON.parse(text) : null;
404
- } catch {
405
- body = text;
406
- }
407
- if (!response.ok) {
408
- throw Object.assign(new Error(`${config.budget.host} ${response.status}`), {
409
- status: response.status,
410
- body,
411
- retryAfter: response.headers.get("retry-after")
412
- });
413
- }
414
- return { status: response.status, body, cost };
415
- } finally {
416
- clearTimeout(timer);
417
- }
418
- },
419
- budget: () => budgetState(config.budget.host)
420
- };
421
- }
422
- var http = defineSingleMethodProvider({
423
- id: "http",
424
- method: "request",
425
- label: "HTTP",
426
- multiInstance: true,
427
- credentials: {
428
- type: "object",
429
- additionalProperties: false,
430
- properties: {
431
- apiKey: { type: "string", title: "API key", writeOnly: true },
432
- apiSecret: { type: "string", title: "API secret", writeOnly: true }
433
- }
434
- },
435
- config: {
436
- type: "object",
437
- additionalProperties: false,
438
- required: ["baseUrl"],
439
- properties: {
440
- baseUrl: { type: "string", title: "Base URL" },
441
- limit: { type: "integer", default: 6000, title: "Units per window" },
442
- windowMs: { type: "integer", default: 60000 },
443
- headroom: {
444
- type: "number",
445
- default: 0.9,
446
- description: "Stop at this fraction of the limit. The venue's window boundary is not ours, and the penalty for crossing is a ban rather than a rejection."
447
- }
448
- }
449
- },
450
- async invoke(context, request) {
451
- const config = context.config;
452
- const client = createHttpClient({
453
- baseUrl: config.baseUrl,
454
- fetch: config.fetch,
455
- costOf: config.costOf,
456
- budget: {
457
- host: new URL(config.baseUrl).host,
458
- limit: config.limit ?? 6000,
459
- windowMs: config.windowMs ?? 60000,
460
- defaultCost: 1,
461
- headroom: config.headroom
462
- }
463
- });
464
- return client.call(request);
465
- },
466
- classify(error) {
467
- const status = error.status;
468
- if (status === 418)
469
- return "backoff";
470
- if (status === 429)
471
- return "backoff";
472
- if (status === 400 || status === 422)
473
- return "terminal";
474
- if (status === 401 || status === 403)
475
- return "retryable";
476
- return "retryable";
477
- }
478
- });
479
- var binanceWeight = (response) => {
480
- const header = response.headers.get("x-mbx-used-weight-1m");
481
- return header === null ? undefined : Number(header);
482
- };
483
- var httpProviders = [http];
484
-
485
- // src/binance.ts
486
- import { parseAmount, formatAmount, zero, assetSpec } from "@forgezero/runtime/finance/money";
487
- import {
488
- parseSymbol,
489
- VenueError
490
- } from "@forgezero/runtime/finance/venues";
491
- var BUDGET_HOST = "binance";
492
- var DEFAULT_HOSTS = {
493
- spot: "https://api.binance.com",
494
- margin: "https://api.binance.com",
495
- futures: "https://fapi.binance.com"
496
- };
497
- var binanceSymbol = (symbol) => {
498
- const { base, quote } = parseSymbol(symbol);
499
- return `${base}${quote}`;
500
- };
501
- var PATHS = {
502
- spot: {
503
- order: "/api/v3/order",
504
- openOrders: "/api/v3/openOrders",
505
- account: "/api/v3/account",
506
- exchangeInfo: "/api/v3/exchangeInfo"
507
- },
508
- margin: {
509
- order: "/sapi/v1/margin/order",
510
- openOrders: "/sapi/v1/margin/openOrders",
511
- account: "/sapi/v1/margin/account",
512
- exchangeInfo: "/api/v3/exchangeInfo"
513
- },
514
- futures: {
515
- order: "/fapi/v1/order",
516
- openOrders: "/fapi/v1/openOrders",
517
- account: "/fapi/v2/account",
518
- exchangeInfo: "/fapi/v1/exchangeInfo"
519
- }
520
- };
521
- function toOrderStatus(status) {
522
- switch (status) {
523
- case "NEW":
524
- case "PENDING_NEW":
525
- return "accepted";
526
- case "PARTIALLY_FILLED":
527
- return "partial";
528
- case "FILLED":
529
- return "filled";
530
- case "CANCELED":
531
- case "PENDING_CANCEL":
532
- case "EXPIRED":
533
- case "EXPIRED_IN_MATCH":
534
- return "cancelled";
535
- default:
536
- return "rejected";
537
- }
538
- }
539
- var SIDE = { buy: "BUY", sell: "SELL" };
540
- var TYPE = { market: "MARKET", limit: "LIMIT", "stop-limit": "STOP_LOSS_LIMIT" };
541
- async function sign(secret, query) {
542
- const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
543
- const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(query));
544
- return [...new Uint8Array(mac)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
545
- }
546
- function createBinanceAdapter(options) {
547
- const hosts = { ...DEFAULT_HOSTS, ...options.hosts };
548
- const recvWindow = options.recvWindowMs ?? 5000;
549
- const now = options.now ?? Date.now;
550
- const clients = new Map;
551
- const clientFor = (type) => {
552
- const base = hosts[type];
553
- const existing = clients.get(base);
554
- if (existing)
555
- return existing;
556
- const client = createHttpClient({
557
- baseUrl: base,
558
- fetch: options.fetch,
559
- costOf: binanceWeight,
560
- budget: { host: BUDGET_HOST, limit: 6000, windowMs: 60000, defaultCost: 1, headroom: 0.9 }
561
- });
562
- clients.set(base, client);
563
- return client;
564
- };
565
- async function signed(args) {
566
- const entries = Object.entries(args.params).filter(([, value]) => value !== undefined);
567
- const base = new URLSearchParams(entries.map(([name, value]) => [name, String(value)])).toString();
568
- const withTiming = `${base}${base ? "&" : ""}recvWindow=${recvWindow}&timestamp=${now()}`;
569
- const signature = await sign(options.credentials.apiSecret, withTiming);
570
- const response = await clientFor(args.type).call({
571
- path: `${args.path}?${withTiming}&signature=${signature}`,
572
- method: args.method,
573
- weight: args.weight,
574
- headers: { "X-MBX-APIKEY": options.credentials.apiKey }
575
- });
576
- return response.body;
577
- }
578
- return {
579
- venue: "binance",
580
- symbolFor: (symbol) => binanceSymbol(symbol),
581
- async markets(type) {
582
- const info = await clientFor(type).call({ path: PATHS[type].exchangeInfo, weight: 20 });
583
- return (info.body.symbols ?? []).filter((entry) => entry.status === "TRADING").map((entry) => {
584
- const filter = (name) => entry.filters.find((candidate) => candidate.filterType === name);
585
- return {
586
- venue: "binance",
587
- type,
588
- symbol: `${entry.baseAsset}/${entry.quoteAsset}`,
589
- base: entry.baseAsset,
590
- quote: entry.quoteAsset,
591
- lotStep: filter("LOT_SIZE")?.stepSize ?? "0.00000001",
592
- tickStep: filter("PRICE_FILTER")?.tickSize ?? "0.00000001",
593
- minNotional: filter("NOTIONAL")?.minNotional ?? filter("MIN_NOTIONAL")?.minNotional ?? "0",
594
- ...type === "spot" ? {} : { maxLeverage: 125 }
595
- };
596
- });
597
- },
598
- async placeOrder(request, market) {
599
- if (request.type === "futures" && request.leverage) {
600
- await signed({
601
- type: "futures",
602
- path: "/fapi/v1/leverage",
603
- method: "POST",
604
- params: { symbol: binanceSymbol(request.symbol), leverage: request.leverage }
605
- });
606
- }
607
- const raw = await signed({
608
- type: request.type,
609
- path: PATHS[request.type].order,
610
- method: "POST",
611
- weight: 1,
612
- params: {
613
- symbol: binanceSymbol(request.symbol),
614
- side: SIDE[request.side],
615
- type: TYPE[request.orderType],
616
- quantity: formatAmount(request.quantity, { trim: true }),
617
- price: request.price ? formatAmount(request.price, { trim: true }) : undefined,
618
- stopPrice: request.stopPrice ? formatAmount(request.stopPrice, { trim: true }) : undefined,
619
- timeInForce: request.orderType === "market" ? undefined : (request.timeInForce ?? "gtc").toUpperCase(),
620
- newClientOrderId: request.clientOrderId,
621
- ...request.type === "margin" ? { sideEffectType: "NO_SIDE_EFFECT" } : {},
622
- ...request.dryRun ? { test: "true" } : {}
623
- }
624
- });
625
- return readOrder(raw, market);
626
- },
627
- async cancelOrder(args) {
628
- await signed({
629
- type: args.type,
630
- path: PATHS[args.type].order,
631
- method: "DELETE",
632
- params: { symbol: binanceSymbol(args.symbol), orderId: args.venueOrderId }
633
- });
634
- },
635
- async openOrders(args) {
636
- const raw = await signed({
637
- type: args.type,
638
- path: PATHS[args.type].openOrders,
639
- method: "GET",
640
- weight: args.symbol ? 3 : 40,
641
- params: { symbol: args.symbol ? binanceSymbol(args.symbol) : undefined }
642
- });
643
- return (raw ?? []).map((entry) => readOrder(entry, undefined));
644
- },
645
- async balances(type) {
646
- const raw = await signed({ type, path: PATHS[type].account, method: "GET", weight: 10, params: {} });
647
- const rows = raw.balances ?? raw.userAssets ?? (raw.assets ?? []).map((entry) => ({ asset: entry.asset, free: entry.availableBalance }));
648
- return rows.filter((entry) => Number(entry.free) > 0).map((entry) => {
649
- try {
650
- return parseAmount(entry.free, entry.asset);
651
- } catch {
652
- return null;
653
- }
654
- }).filter((amount) => amount !== null);
655
- }
656
- };
657
- }
658
- function readOrder(raw, market) {
659
- const asset = market?.base ?? "BTC";
660
- const executed = String(raw.executedQty ?? raw.origQty ?? "0");
661
- let filledQuantity;
662
- try {
663
- filledQuantity = parseAmount(executed, asset);
664
- } catch {
665
- filledQuantity = zero(asset);
666
- }
667
- const quoteFilled = Number(raw.cummulativeQuoteQty ?? 0);
668
- const filled = Number(executed);
669
- return {
670
- venueOrderId: String(raw.orderId ?? ""),
671
- clientOrderId: raw.clientOrderId ? String(raw.clientOrderId) : undefined,
672
- status: toOrderStatus(String(raw.status ?? "NEW")),
673
- filledQuantity,
674
- ...market && filled > 0 && quoteFilled > 0 ? {
675
- averagePrice: (() => {
676
- try {
677
- return parseAmount((quoteFilled / filled).toFixed(assetSpec(market.quote).decimals), market.quote);
678
- } catch {
679
- return;
680
- }
681
- })()
682
- } : {},
683
- raw
684
- };
685
- }
686
- function readBinanceError(error) {
687
- const body = error.body;
688
- if (!body?.code)
689
- return;
690
- const known = {
691
- [-1013]: ["MIN_NOTIONAL", "The order is below the venue minimum, or off its lot or tick step."],
692
- [-2010]: ["MIN_NOTIONAL", "Rejected: insufficient balance, or below the minimum."],
693
- [-1111]: ["LOT_STEP", "More decimal places than this market accepts."],
694
- [-1121]: ["UNKNOWN_MARKET", "That symbol is not traded on this venue."]
695
- };
696
- const match = known[body.code];
697
- return match ? new VenueError(match[0], `${match[1]} (${body.msg})`) : undefined;
698
- }
699
- export {
700
- toOrderStatus,
701
- readBinanceError,
702
- createBinanceAdapter,
703
- binanceSymbol
704
- };