@tradejs/connectors 1.0.9 → 1.0.10

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/index.mjs CHANGED
@@ -1,11 +1,8 @@
1
1
  // src/ByBit/index.ts
2
2
  import _ from "lodash";
3
3
  import chalk from "chalk";
4
- import { delay } from "@tradejs/core/async";
5
- import {
6
- MARKET_CATEGORY as MARKET_CATEGORY2,
7
- PRELOAD_FALLBACK_DAYS as PRELOAD_FALLBACK_DAYS2
8
- } from "@tradejs/core/constants";
4
+ import { delay as delay2 } from "@tradejs/core/async";
5
+ import { PRELOAD_FALLBACK_DAYS as PRELOAD_FALLBACK_DAYS2 } from "@tradejs/core/constants";
9
6
  import { toJson } from "@tradejs/core/data";
10
7
  import { round } from "@tradejs/core/math";
11
8
  import { normalizeTickerData } from "@tradejs/core/tickers";
@@ -15,24 +12,43 @@ import { logger as logger3 } from "@tradejs/infra/logger";
15
12
  // src/ByBit/client.ts
16
13
  import { RestClientV5 } from "bybit-api";
17
14
  import { logger } from "@tradejs/infra/logger";
18
- import { getData, redisKeys } from "@tradejs/infra/redis";
19
- var useTestnet = false;
20
- var getClient = async ({ userName }, access = "private") => {
15
+ import { resolveTradingAccount } from "@tradejs/infra/tradingAccounts";
16
+ var PRIVATE_RECV_WINDOW_MS = 1e4;
17
+ var getClient = async (config, access = "private") => {
18
+ const account = access === "public" && !config.accountId ? null : await resolveTradingAccount({
19
+ userName: config.userName,
20
+ accountId: config.accountId,
21
+ provider: "bybit",
22
+ universe: config.universe
23
+ });
24
+ const useTestnet = account?.environment === "testnet";
21
25
  if (access === "public") {
22
26
  return new RestClientV5({
23
27
  parseAPIRateLimits: true,
24
28
  testnet: useTestnet
25
29
  });
26
30
  }
27
- const user = await getData(redisKeys.user(userName));
28
- if (!user) {
29
- logger.log("error", "connection config not found: %s", userName);
31
+ if (!account?.apiKey || !account.apiSecret) {
32
+ if (config.accountId) {
33
+ logger.log(
34
+ "error",
35
+ "Bybit trading account config not found: user=%s account=%s",
36
+ config.userName,
37
+ config.accountId
38
+ );
39
+ } else {
40
+ logger.log("error", "connection config not found: %s", config.userName);
41
+ }
30
42
  return null;
31
43
  }
32
44
  const client = new RestClientV5({
33
- key: user.BYBIT_API_KEY,
34
- secret: user.BYBIT_API_SECRET,
45
+ key: account.apiKey,
46
+ secret: account.apiSecret,
35
47
  parseAPIRateLimits: true,
48
+ recv_window: PRIVATE_RECV_WINDOW_MS,
49
+ // Avoid noisy bybit-api console.error dumps when its internal
50
+ // /v5/market/time sync probe hits transient network resets.
51
+ syncTimeBeforePrivateRequests: false,
36
52
  testnet: useTestnet
37
53
  });
38
54
  return client;
@@ -42,6 +58,7 @@ var getClient = async ({ userName }, access = "private") => {
42
58
  import { PRELOAD_FALLBACK_DAYS } from "@tradejs/core/constants";
43
59
  import { mergeData } from "@tradejs/core/data";
44
60
  import { getItemTimestamp, getTimestamp } from "@tradejs/core/time";
61
+ import { delay } from "@tradejs/core/async";
45
62
  import { logger as logger2 } from "@tradejs/infra/logger";
46
63
  import {
47
64
  getCandlesRange,
@@ -51,6 +68,23 @@ import {
51
68
  } from "@tradejs/infra/timescale";
52
69
  var DEFAULT_LIMIT = 1e3;
53
70
  var DEFAULT_CACHE_FALLBACK_WINDOW = 1e3;
71
+ var DEFAULT_TIMESCALE_RETRIES = 2;
72
+ var DEFAULT_TIMESCALE_RETRY_DELAY_MS = 1e3;
73
+ var resolveNonNegativeInt = (value, fallback) => {
74
+ const parsed = Number(value);
75
+ if (!Number.isFinite(parsed) || parsed < 0) {
76
+ return fallback;
77
+ }
78
+ return Math.floor(parsed);
79
+ };
80
+ var getTimescaleRetryCount = () => resolveNonNegativeInt(
81
+ process.env.TIMESCALE_KLINE_RETRIES,
82
+ DEFAULT_TIMESCALE_RETRIES
83
+ );
84
+ var getTimescaleRetryDelayMs = () => resolveNonNegativeInt(
85
+ process.env.TIMESCALE_KLINE_RETRY_DELAY_MS,
86
+ DEFAULT_TIMESCALE_RETRY_DELAY_MS
87
+ );
54
88
  var intervalMsOf = (interval) => interval * 6e4;
55
89
  var clampToClosedCandle = (value, intervalMs) => Math.floor(value / intervalMs) * intervalMs;
56
90
  var normalizeRangeToClosed = (intervalMs, start, end) => {
@@ -72,10 +106,29 @@ var createTimescaleCachedKline = ({
72
106
  cacheFallbackWindow = DEFAULT_CACHE_FALLBACK_WINDOW
73
107
  }) => {
74
108
  let isTimescaleFallbackMode = false;
75
- const loadData = async (direction, pointer, limitBoundary, requestParams, intervalMs) => {
76
- if (pointer === void 0) return [];
109
+ const runTimescaleOperation = async (operation) => {
110
+ const retries = getTimescaleRetryCount();
111
+ const retryDelayMs = getTimescaleRetryDelayMs();
112
+ for (let attempt = 0; ; attempt += 1) {
113
+ try {
114
+ return await operation();
115
+ } catch (error) {
116
+ if (attempt >= retries) {
117
+ throw error;
118
+ }
119
+ const waitMs = retryDelayMs * 2 ** attempt;
120
+ if (waitMs > 0) {
121
+ await delay(waitMs);
122
+ }
123
+ }
124
+ }
125
+ };
126
+ const loadData = async (direction, pointer, limitBoundary, requestParams, intervalMs, options = {}) => {
127
+ if (pointer === void 0) return { data: [], loaded: false };
77
128
  let accumulated = [];
78
129
  let fulfilled = false;
130
+ let loaded = false;
131
+ const shouldAccumulate = options.accumulate ?? true;
79
132
  while (!fulfilled) {
80
133
  const currentPointer = pointer;
81
134
  const params = {
@@ -96,7 +149,13 @@ var createTimescaleCachedKline = ({
96
149
  fulfilled = true;
97
150
  break;
98
151
  }
99
- accumulated = direction === "older" ? mergeData(partData, accumulated) : mergeData(accumulated, partData);
152
+ loaded = true;
153
+ if (options.onPartData) {
154
+ await options.onPartData(partData);
155
+ }
156
+ if (shouldAccumulate) {
157
+ accumulated = direction === "older" ? mergeData(partData, accumulated) : mergeData(accumulated, partData);
158
+ }
100
159
  const boundaryReached = limitBoundary !== void 0 && (direction === "older" && currentPointer <= limitBoundary || direction === "newer" && currentPointer >= limitBoundary);
101
160
  if (partData.length < limit || boundaryReached) {
102
161
  fulfilled = true;
@@ -109,7 +168,7 @@ var createTimescaleCachedKline = ({
109
168
  }
110
169
  pointer = nextPointer;
111
170
  }
112
- return accumulated;
171
+ return { data: accumulated, loaded };
113
172
  };
114
173
  const refreshTail = async ({
115
174
  symbol,
@@ -134,7 +193,9 @@ var createTimescaleCachedKline = ({
134
193
  silent
135
194
  });
136
195
  if (part.length) {
137
- await upsertCandles(toRows(provider, symbol, intMinutes, part));
196
+ await runTimescaleOperation(
197
+ () => upsertCandles(toRows(provider, symbol, intMinutes, part))
198
+ );
138
199
  }
139
200
  };
140
201
  return async ({
@@ -156,7 +217,9 @@ var createTimescaleCachedKline = ({
156
217
  }
157
218
  const intervalMs = intervalMsOf(intMinutes);
158
219
  try {
159
- const edges = await getDataEdges(provider, symbol, intMinutes);
220
+ const edges = await runTimescaleOperation(
221
+ () => getDataEdges(provider, symbol, intMinutes)
222
+ );
160
223
  let dataStart = edges.min;
161
224
  let dataEnd = edges.max;
162
225
  const { normStart, normEnd } = normalizeRangeToClosed(
@@ -171,20 +234,18 @@ var createTimescaleCachedKline = ({
171
234
  0
172
235
  );
173
236
  const e = defaultEnd ?? base;
174
- const dbData2 = await getCandlesRange(
175
- provider,
176
- symbol,
177
- intMinutes,
178
- s,
179
- e
237
+ const dbData2 = await runTimescaleOperation(
238
+ () => getCandlesRange(provider, symbol, intMinutes, s, e)
180
239
  );
181
240
  return rowsToKline(dbData2);
182
241
  }
183
242
  const needOlderData = defaultStart !== void 0 && (dataStart === void 0 || normStart < dataStart);
184
- const needNewerData = defaultEnd !== void 0 && (dataEnd === void 0 || normEnd > dataEnd);
243
+ const persistPartData = warmOnly ? (partData) => runTimescaleOperation(
244
+ () => upsertCandles(toRows(provider, symbol, intMinutes, partData))
245
+ ) : void 0;
185
246
  if (needOlderData) {
186
247
  const pointerForOlder = dataStart ?? normEnd ?? Date.now();
187
- const olderData = await loadData(
248
+ const olderResult = await loadData(
188
249
  "older",
189
250
  pointerForOlder,
190
251
  normStart,
@@ -195,17 +256,34 @@ var createTimescaleCachedKline = ({
195
256
  start: normStart,
196
257
  end: pointerForOlder
197
258
  },
198
- intervalMs
259
+ intervalMs,
260
+ {
261
+ accumulate: !warmOnly,
262
+ onPartData: persistPartData
263
+ }
199
264
  );
200
- if (olderData.length) {
201
- await upsertCandles(toRows(provider, symbol, intMinutes, olderData));
265
+ if (warmOnly && olderResult.loaded) {
202
266
  dataStart = normStart;
267
+ if (dataEnd === void 0) {
268
+ dataEnd = normEnd;
269
+ }
270
+ } else if (olderResult.data.length) {
271
+ await runTimescaleOperation(
272
+ () => upsertCandles(
273
+ toRows(provider, symbol, intMinutes, olderResult.data)
274
+ )
275
+ );
276
+ dataStart = normStart;
277
+ if (dataEnd === void 0) {
278
+ dataEnd = normEnd;
279
+ }
203
280
  }
204
281
  }
282
+ const needNewerData = defaultEnd !== void 0 && (dataEnd === void 0 || normEnd > dataEnd);
205
283
  if (needNewerData) {
206
284
  const fallbackStart = getTimestamp(PRELOAD_FALLBACK_DAYS);
207
285
  const pointerForNewer = dataEnd ?? (defaultStart !== void 0 ? normStart : fallbackStart) ?? 0;
208
- const newerData = await loadData(
286
+ const newerResult = await loadData(
209
287
  "newer",
210
288
  pointerForNewer,
211
289
  normEnd,
@@ -216,10 +294,20 @@ var createTimescaleCachedKline = ({
216
294
  start: pointerForNewer,
217
295
  end: normEnd
218
296
  },
219
- intervalMs
297
+ intervalMs,
298
+ {
299
+ accumulate: !warmOnly,
300
+ onPartData: persistPartData
301
+ }
220
302
  );
221
- if (newerData.length) {
222
- await upsertCandles(toRows(provider, symbol, intMinutes, newerData));
303
+ if (warmOnly && newerResult.loaded) {
304
+ dataEnd = normEnd;
305
+ } else if (newerResult.data.length) {
306
+ await runTimescaleOperation(
307
+ () => upsertCandles(
308
+ toRows(provider, symbol, intMinutes, newerResult.data)
309
+ )
310
+ );
223
311
  dataEnd = normEnd;
224
312
  }
225
313
  }
@@ -237,12 +325,8 @@ var createTimescaleCachedKline = ({
237
325
  const rangeStart = defaultStart ?? dataStart ?? 0;
238
326
  const rangeEnd = defaultEnd ?? dataEnd ?? Date.now();
239
327
  const { normStart: finalStart, normEnd: finalEnd } = normalizeRangeToClosed(intervalMs, rangeStart, rangeEnd);
240
- const dbData = await getCandlesRange(
241
- provider,
242
- symbol,
243
- intMinutes,
244
- finalStart,
245
- finalEnd
328
+ const dbData = await runTimescaleOperation(
329
+ () => getCandlesRange(provider, symbol, intMinutes, finalStart, finalEnd)
246
330
  );
247
331
  if (isTimescaleFallbackMode) {
248
332
  isTimescaleFallbackMode = false;
@@ -275,8 +359,8 @@ var createTimescaleCachedKline = ({
275
359
  };
276
360
 
277
361
  // src/ByBit/utils.ts
278
- import { MARKET_CATEGORY } from "@tradejs/core/constants";
279
362
  import { formatUnix } from "@tradejs/core/time";
363
+ var BYBIT_CATEGORY = "linear";
280
364
  var parseKlineItem = (item) => ({
281
365
  dt: formatUnix(parseInt(item[0])),
282
366
  timestamp: parseInt(item[0]),
@@ -304,7 +388,7 @@ var getSymbolMeta = async (client, symbol) => {
304
388
  const cached = symbolMetaCache.get(symbol);
305
389
  if (cached) return cached;
306
390
  const res = await client.getInstrumentsInfo({
307
- category: MARKET_CATEGORY,
391
+ category: BYBIT_CATEGORY,
308
392
  symbol
309
393
  });
310
394
  const item = res?.result?.list?.[0];
@@ -319,7 +403,10 @@ var getSymbolMeta = async (client, symbol) => {
319
403
  qtyStep: Number(qtyStepStr),
320
404
  minOrderQty: Number(minOrderQtyStr),
321
405
  pricePrecision: stepToPrecision(tickSizeStr),
322
- qtyPrecision: stepToPrecision(qtyStepStr)
406
+ qtyPrecision: stepToPrecision(qtyStepStr),
407
+ maxLeverage: Number.isFinite(
408
+ Number(item.leverageFilter?.maxLeverage)
409
+ ) ? Number(item.leverageFilter.maxLeverage) : null
323
410
  };
324
411
  symbolMetaCache.set(symbol, meta);
325
412
  return meta;
@@ -366,13 +453,31 @@ var mapPositionData = (data) => {
366
453
  };
367
454
 
368
455
  // src/ByBit/index.ts
456
+ import {
457
+ resolveConnectorUniverse
458
+ } from "@tradejs/types";
369
459
  var LIMIT = 1e3;
460
+ var BYBIT_CATEGORY2 = "linear";
461
+ var BYBIT_CAPABILITIES = {
462
+ supportedUniverses: ["crypto", "tradfi"],
463
+ defaultUniverse: "crypto"
464
+ };
465
+ var BYBIT_TRADFI_SYMBOL_TYPES = ["stock", "commodity", "forex"];
466
+ var FUNDING_HISTORY_LIMIT = 200;
370
467
  var BYBIT_RATE_LIMIT_RETCODE = 10006;
371
468
  var BYBIT_TRADING_STOP_NOT_MODIFIED_RETCODE = 34040;
372
469
  var KLINE_RATE_LIMIT_MAX_ATTEMPTS = 3;
373
470
  var KLINE_RATE_LIMIT_BASE_DELAY_MS = 800;
374
471
  var KLINE_RATE_LIMIT_MAX_DELAY_MS = 1e4;
375
472
  var KLINE_RATE_LIMIT_RESET_BUFFER_MS = 50;
473
+ var POSITION_SNAPSHOT_TIMEOUT_MS = Math.max(
474
+ 1e3,
475
+ Number(process.env.TRADEJS_BYBIT_POSITION_TIMEOUT_MS ?? 1e4)
476
+ );
477
+ var POSITION_SNAPSHOT_CACHE_TTL_MS = Math.max(
478
+ 250,
479
+ Number(process.env.TRADEJS_BYBIT_POSITION_CACHE_TTL_MS ?? 1500)
480
+ );
376
481
  var INTERVAL_TO_MINUTES = {
377
482
  "1": 1,
378
483
  "3": 3,
@@ -392,6 +497,144 @@ var getLogLevel = (res) => res.retCode === 0 ? "info" : "error";
392
497
  var isTradingStopNotModified = (res) => res?.retCode === BYBIT_TRADING_STOP_NOT_MODIFIED_RETCODE;
393
498
  var isTradingStopAccepted = (res) => res?.retCode === 0 || isTradingStopNotModified(res);
394
499
  var isKlineRateLimited = (res) => res?.retCode === BYBIT_RATE_LIMIT_RETCODE;
500
+ var mapBybitAssetClass = (symbolType) => {
501
+ if (symbolType === "stock") return "equity";
502
+ if (symbolType === "commodity") return "commodity";
503
+ if (symbolType === "forex") return "forex";
504
+ return "crypto";
505
+ };
506
+ var mapBybitInstrument = (item) => {
507
+ const assetClass = mapBybitAssetClass(item.symbolType);
508
+ return {
509
+ provider: "bybit",
510
+ symbol: String(item.symbol ?? "").trim().toUpperCase(),
511
+ kind: "perpetual",
512
+ assetClass,
513
+ universe: assetClass === "crypto" ? "crypto" : "tradfi",
514
+ status: String(item.status ?? "").toLowerCase() === "trading" ? "trading" : "inactive",
515
+ baseAsset: String(item.baseCoin ?? "").trim() || void 0,
516
+ quoteAsset: String(item.quoteCoin ?? "").trim() || void 0,
517
+ settleAsset: String(item.settleCoin ?? "").trim() || void 0,
518
+ displayName: String(item.displayName ?? "").trim() || void 0,
519
+ venueMetadata: {
520
+ category: BYBIT_CATEGORY2,
521
+ symbolType: String(item.symbolType ?? ""),
522
+ contractType: String(item.contractType ?? ""),
523
+ fundingInterval: Number(item.fundingInterval ?? Number.NaN),
524
+ maxLeverage: Number(item.leverageFilter?.maxLeverage ?? Number.NaN)
525
+ }
526
+ };
527
+ };
528
+ var getOptionalStringField = (source, key) => {
529
+ if (!source || typeof source !== "object") {
530
+ return null;
531
+ }
532
+ const value = source[key];
533
+ return typeof value === "string" && value.trim() ? value : null;
534
+ };
535
+ var toFiniteNumberOrNull = (value) => {
536
+ const parsed = Number(value ?? Number.NaN);
537
+ return Number.isFinite(parsed) ? parsed : null;
538
+ };
539
+ var sumOptionalFees = (values) => {
540
+ const finiteValues = values.filter(
541
+ (value) => typeof value === "number" && Number.isFinite(value)
542
+ );
543
+ return finiteValues.length ? Number(finiteValues.reduce((sum, value) => sum + value, 0).toFixed(12)) : null;
544
+ };
545
+ var FUNDING_TRANSACTION_LOG_PAGE_LIMIT = 100;
546
+ var FUNDING_TRANSACTION_LOG_MAX_PAGES = 20;
547
+ var CLOSED_PNL_PAGE_LIMIT = 100;
548
+ var CLOSED_PNL_MAX_PAGES = 20;
549
+ var EXECUTION_PAGE_LIMIT = 100;
550
+ var EXECUTION_MAX_PAGES = 20;
551
+ var loadFundingFeeRows = async ({
552
+ client,
553
+ startTime,
554
+ endTime,
555
+ symbol
556
+ }) => {
557
+ if (typeof client.getTransactionLog !== "function") {
558
+ return [];
559
+ }
560
+ try {
561
+ const rows = [];
562
+ let cursor;
563
+ for (let page = 0; page < FUNDING_TRANSACTION_LOG_MAX_PAGES; page += 1) {
564
+ const response = await client.getTransactionLog({
565
+ accountType: "UNIFIED",
566
+ category: BYBIT_CATEGORY2,
567
+ type: "SETTLEMENT",
568
+ startTime,
569
+ endTime,
570
+ limit: FUNDING_TRANSACTION_LOG_PAGE_LIMIT,
571
+ ...cursor ? { cursor } : {}
572
+ });
573
+ if (response?.retCode !== 0) {
574
+ logger3.log(
575
+ "debug",
576
+ "funding transaction log retCode: %s, %s",
577
+ response?.retCode,
578
+ response?.retMsg
579
+ );
580
+ return rows;
581
+ }
582
+ const pageRows = (response.result?.list ?? []).map((item) => {
583
+ const rowSymbol = String(item.symbol ?? "").trim();
584
+ const timestamp = Number(item.transactionTime ?? Number.NaN);
585
+ const fundingFee = Number(item.funding ?? Number.NaN);
586
+ if (!rowSymbol || !Number.isFinite(timestamp) || !Number.isFinite(fundingFee) || symbol && rowSymbol !== symbol) {
587
+ return null;
588
+ }
589
+ return {
590
+ symbol: rowSymbol,
591
+ timestamp,
592
+ fundingFee
593
+ };
594
+ });
595
+ rows.push(
596
+ ...pageRows.filter((item) => item != null)
597
+ );
598
+ const nextCursor = String(response.result?.nextPageCursor ?? "").trim();
599
+ if (!nextCursor || nextCursor === cursor) {
600
+ break;
601
+ }
602
+ cursor = nextCursor;
603
+ }
604
+ return rows;
605
+ } catch (error) {
606
+ logger3.log("debug", "funding transaction log failed: %s", error);
607
+ return [];
608
+ }
609
+ };
610
+ var sumFundingFeeForTrade = ({
611
+ rows,
612
+ symbol,
613
+ entryTimestamp,
614
+ closedAt
615
+ }) => {
616
+ if (entryTimestamp == null || !Number.isFinite(entryTimestamp)) {
617
+ return null;
618
+ }
619
+ const matched = rows.filter(
620
+ (row) => row.symbol === symbol && row.timestamp >= entryTimestamp && row.timestamp <= closedAt
621
+ );
622
+ return matched.length ? Number(matched.reduce((sum, row) => sum + row.fundingFee, 0).toFixed(12)) : null;
623
+ };
624
+ var inferClosedPnlDirection = ({
625
+ entryPrice,
626
+ exitPrice,
627
+ closedPnl
628
+ }) => {
629
+ const priceDelta = exitPrice - entryPrice;
630
+ if (!Number.isFinite(priceDelta) || priceDelta === 0) {
631
+ return null;
632
+ }
633
+ if (closedPnl === 0) {
634
+ return priceDelta > 0 ? "LONG" : "SHORT";
635
+ }
636
+ return priceDelta * closedPnl > 0 ? "LONG" : "SHORT";
637
+ };
395
638
  var resolveKlineRetryDelayMs = (res, attempt) => {
396
639
  const resetAtRaw = Number(res?.rateLimitApi?.resetAtTimestamp);
397
640
  if (Number.isFinite(resetAtRaw) && resetAtRaw > 0) {
@@ -409,8 +652,14 @@ var resolveKlineRetryDelayMs = (res, attempt) => {
409
652
  };
410
653
  var ByBitConnectorCreator = async (config) => {
411
654
  let state = {};
655
+ const universe = resolveConnectorUniverse(
656
+ BYBIT_CAPABILITIES,
657
+ config.universe
658
+ );
412
659
  let publicClientPromise = null;
413
660
  let privateClientPromise = null;
661
+ const positionSnapshotCache = /* @__PURE__ */ new Map();
662
+ const inflightPositionSnapshots = /* @__PURE__ */ new Map();
414
663
  const getPublicClient = async () => {
415
664
  publicClientPromise ??= getClient(config, "public");
416
665
  return publicClientPromise;
@@ -419,6 +668,123 @@ var ByBitConnectorCreator = async (config) => {
419
668
  privateClientPromise ??= getClient(config, "private");
420
669
  return privateClientPromise;
421
670
  };
671
+ const instrumentCache = /* @__PURE__ */ new Map();
672
+ const fetchInstrumentGroup = async (symbolType) => {
673
+ const client = await getPublicClient();
674
+ if (!client) return [];
675
+ if (typeof client.getInstrumentsInfo !== "function") {
676
+ if (universe === "tradfi" || symbolType) {
677
+ throw new Error("Bybit instrument discovery is unavailable");
678
+ }
679
+ return [];
680
+ }
681
+ const instruments = [];
682
+ let cursor;
683
+ for (; ; ) {
684
+ const response = await client.getInstrumentsInfo({
685
+ category: BYBIT_CATEGORY2,
686
+ limit: LIMIT,
687
+ ...symbolType ? { symbolType } : {},
688
+ ...cursor ? { cursor } : {}
689
+ });
690
+ if (response.retCode !== 0) {
691
+ throw new Error(
692
+ `Bybit instruments failed: ${response.retCode} ${response.retMsg}`
693
+ );
694
+ }
695
+ for (const raw of response.result?.list ?? []) {
696
+ const instrument = mapBybitInstrument(raw);
697
+ if (instrument.symbol) instruments.push(instrument);
698
+ }
699
+ const nextCursor = String(response.result?.nextPageCursor ?? "").trim();
700
+ if (!nextCursor || nextCursor === cursor) break;
701
+ cursor = nextCursor;
702
+ }
703
+ return instruments;
704
+ };
705
+ const loadInstrumentUniverse = (requestedUniverse) => {
706
+ const cached = instrumentCache.get(requestedUniverse);
707
+ if (cached && cached.expiresAt > Date.now()) {
708
+ return cached.promise;
709
+ }
710
+ const promise = requestedUniverse === "tradfi" ? Promise.all(
711
+ BYBIT_TRADFI_SYMBOL_TYPES.map(
712
+ (symbolType) => fetchInstrumentGroup(symbolType)
713
+ )
714
+ ).then((groups) => {
715
+ const bySymbol = /* @__PURE__ */ new Map();
716
+ for (const instrument of groups.flat()) {
717
+ bySymbol.set(instrument.symbol, instrument);
718
+ }
719
+ return [...bySymbol.values()];
720
+ }) : fetchInstrumentGroup().then(
721
+ (instruments) => instruments.filter(
722
+ (instrument) => instrument.universe === "crypto"
723
+ )
724
+ );
725
+ instrumentCache.set(requestedUniverse, {
726
+ expiresAt: Date.now() + 5 * 6e4,
727
+ promise
728
+ });
729
+ promise.catch(() => instrumentCache.delete(requestedUniverse));
730
+ return promise;
731
+ };
732
+ const listInstruments = async (query = {}) => {
733
+ const requestedUniverse = resolveConnectorUniverse(
734
+ BYBIT_CAPABILITIES,
735
+ query?.universe ?? universe
736
+ );
737
+ const [assetClasses, symbols] = [
738
+ query?.assetClasses?.length ? new Set(query.assetClasses) : null,
739
+ query?.symbols?.length ? new Set(query.symbols.map((symbol) => symbol.trim().toUpperCase())) : null
740
+ ];
741
+ return (await loadInstrumentUniverse(requestedUniverse)).filter(
742
+ (instrument) => (!assetClasses || assetClasses.has(instrument.assetClass)) && (!symbols || symbols.has(instrument.symbol))
743
+ );
744
+ };
745
+ const withTimeout = async (promise, timeoutMs, label) => {
746
+ let timeoutHandle = null;
747
+ try {
748
+ return await Promise.race([
749
+ promise,
750
+ new Promise((_2, reject) => {
751
+ timeoutHandle = setTimeout(() => {
752
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
753
+ }, timeoutMs);
754
+ })
755
+ ]);
756
+ } finally {
757
+ if (timeoutHandle) {
758
+ clearTimeout(timeoutHandle);
759
+ }
760
+ }
761
+ };
762
+ const setCachedPositionSnapshot = (symbol, value) => {
763
+ positionSnapshotCache.set(symbol, {
764
+ value,
765
+ expiresAt: Date.now() + POSITION_SNAPSHOT_CACHE_TTL_MS
766
+ });
767
+ };
768
+ const getCachedPositionSnapshot = (symbol) => {
769
+ const cached = positionSnapshotCache.get(symbol);
770
+ if (!cached) {
771
+ return void 0;
772
+ }
773
+ if (cached.expiresAt <= Date.now()) {
774
+ positionSnapshotCache.delete(symbol);
775
+ return void 0;
776
+ }
777
+ return cached.value;
778
+ };
779
+ const invalidatePositionSnapshot = (symbol) => {
780
+ if (typeof symbol === "string" && symbol.length > 0) {
781
+ positionSnapshotCache.delete(symbol);
782
+ inflightPositionSnapshots.delete(symbol);
783
+ return;
784
+ }
785
+ positionSnapshotCache.clear();
786
+ inflightPositionSnapshots.clear();
787
+ };
422
788
  const request = async ({
423
789
  symbol,
424
790
  interval,
@@ -439,7 +805,7 @@ var ByBitConnectorCreator = async (config) => {
439
805
  }
440
806
  for (let attempt = 1; attempt <= KLINE_RATE_LIMIT_MAX_ATTEMPTS; attempt += 1) {
441
807
  const kline = await client.getKline({
442
- category: MARKET_CATEGORY2,
808
+ category: BYBIT_CATEGORY2,
443
809
  symbol,
444
810
  interval,
445
811
  start: normalizedStart,
@@ -458,7 +824,7 @@ var ByBitConnectorCreator = async (config) => {
458
824
  KLINE_RATE_LIMIT_MAX_ATTEMPTS,
459
825
  waitMs
460
826
  );
461
- await delay(waitMs);
827
+ await delay2(waitMs);
462
828
  continue;
463
829
  }
464
830
  }
@@ -500,40 +866,67 @@ var ByBitConnectorCreator = async (config) => {
500
866
  const intervalToMinutes3 = (interval) => {
501
867
  return INTERVAL_TO_MINUTES[String(interval)] ?? null;
502
868
  };
503
- const getPositionSnapshot = async (symbol) => {
504
- const client = await getPrivateClient();
505
- if (!client) {
506
- return null;
507
- }
508
- const positionRes = await client.getPositionInfo({
509
- symbol,
510
- category: MARKET_CATEGORY2
511
- });
512
- if (positionRes.retCode !== 0) {
869
+ const fetchPositionSnapshot = async (symbol) => {
870
+ try {
871
+ const client = await getPrivateClient();
872
+ if (!client) {
873
+ return null;
874
+ }
875
+ const positionRes = await withTimeout(
876
+ client.getPositionInfo({
877
+ symbol,
878
+ category: BYBIT_CATEGORY2
879
+ }),
880
+ POSITION_SNAPSHOT_TIMEOUT_MS,
881
+ `bybit getPositionInfo ${symbol}`
882
+ );
883
+ if (positionRes.retCode !== 0) {
884
+ logger3.log(
885
+ getLogLevel(positionRes),
886
+ "position retCode: %s, %s",
887
+ symbol,
888
+ positionRes.retCode
889
+ );
890
+ return null;
891
+ }
892
+ const positions = mapPositionData(positionRes.result.list);
893
+ if (!positions || _.isEmpty(positions)) {
894
+ return null;
895
+ }
896
+ const position = positions[0];
513
897
  logger3.log(
514
- getLogLevel(positionRes),
515
- "position retCode: %s, %s",
516
- symbol,
517
- positionRes.retCode
898
+ "debug",
899
+ "position: %s %s qty=%s price=%s",
900
+ position.symbol,
901
+ position.direction,
902
+ position.qty,
903
+ position.price
518
904
  );
905
+ return {
906
+ ...position
907
+ };
908
+ } catch (error) {
909
+ logger3.log("error", "getPositionSnapshot failed: %s %s", symbol, error);
519
910
  return null;
520
911
  }
521
- const positions = mapPositionData(positionRes.result.list);
522
- if (!positions || _.isEmpty(positions)) {
523
- return null;
912
+ };
913
+ const getPositionSnapshot = async (symbol) => {
914
+ const cached = getCachedPositionSnapshot(symbol);
915
+ if (cached !== void 0) {
916
+ return cached;
524
917
  }
525
- const position = positions[0];
526
- logger3.log(
527
- "debug",
528
- "position: %s %s qty=%s price=%s",
529
- position.symbol,
530
- position.direction,
531
- position.qty,
532
- position.price
533
- );
534
- return {
535
- ...position
536
- };
918
+ const inflight = inflightPositionSnapshots.get(symbol);
919
+ if (inflight) {
920
+ return inflight;
921
+ }
922
+ const nextSnapshotPromise = fetchPositionSnapshot(symbol).then((position) => {
923
+ setCachedPositionSnapshot(symbol, position);
924
+ return position;
925
+ }).finally(() => {
926
+ inflightPositionSnapshots.delete(symbol);
927
+ });
928
+ inflightPositionSnapshots.set(symbol, nextSnapshotPromise);
929
+ return nextSnapshotPromise;
537
930
  };
538
931
  const setTakeProfits = async ({
539
932
  symbol,
@@ -584,7 +977,7 @@ var ByBitConnectorCreator = async (config) => {
584
977
  );
585
978
  const isFullMode = takeProfits.length === 1 && tp.rate === 1;
586
979
  const tpRes = await client.setTradingStop({
587
- category: MARKET_CATEGORY2,
980
+ category: BYBIT_CATEGORY2,
588
981
  symbol,
589
982
  tpSize: isFullMode ? void 0 : tpSizeStr,
590
983
  tpslMode: isFullMode ? "Full" : "Partial",
@@ -649,7 +1042,7 @@ var ByBitConnectorCreator = async (config) => {
649
1042
  meta
650
1043
  );
651
1044
  const slRes = await client.setTradingStop({
652
- category: MARKET_CATEGORY2,
1045
+ category: BYBIT_CATEGORY2,
653
1046
  symbol,
654
1047
  tpslMode: "Full",
655
1048
  stopLoss: slNormalized.priceStr,
@@ -685,6 +1078,11 @@ var ByBitConnectorCreator = async (config) => {
685
1078
  return isTradingStopAccepted(slRes);
686
1079
  };
687
1080
  return {
1081
+ capabilities: BYBIT_CAPABILITIES,
1082
+ universe,
1083
+ accountId: config.accountId,
1084
+ deploymentId: config.deploymentId,
1085
+ listInstruments,
688
1086
  getState: async () => state,
689
1087
  setState: async (newState) => {
690
1088
  state = { ...state, ...newState };
@@ -702,7 +1100,7 @@ var ByBitConnectorCreator = async (config) => {
702
1100
  return [];
703
1101
  }
704
1102
  const positionRes = await client.getPositionInfo({
705
- category: MARKET_CATEGORY2,
1103
+ category: BYBIT_CATEGORY2,
706
1104
  settleCoin: "USDT"
707
1105
  });
708
1106
  if (positionRes.retCode !== 0) {
@@ -725,7 +1123,7 @@ var ByBitConnectorCreator = async (config) => {
725
1123
  return [];
726
1124
  }
727
1125
  const positionRes = await client.getPositionInfo({
728
- category: MARKET_CATEGORY2,
1126
+ category: BYBIT_CATEGORY2,
729
1127
  settleCoin: "USDT"
730
1128
  });
731
1129
  if (positionRes.retCode !== 0) {
@@ -741,6 +1139,8 @@ var ByBitConnectorCreator = async (config) => {
741
1139
  const entryPrice = Number(item.avgPrice ?? Number.NaN);
742
1140
  const currentPrice = Number(item.markPrice ?? Number.NaN);
743
1141
  const unrealizedPnl = Number(item.unrealisedPnl ?? Number.NaN);
1142
+ const takeProfitPrice = toFiniteNumberOrNull(item.takeProfit);
1143
+ const stopLossPrice = toFiniteNumberOrNull(item.stopLoss);
744
1144
  const side = String(item.side ?? "");
745
1145
  if (!Number.isFinite(qty) || qty <= 0 || !Number.isFinite(entryPrice) || !Number.isFinite(currentPrice) || !Number.isFinite(unrealizedPnl) || side !== "Buy" && side !== "Sell") {
746
1146
  return null;
@@ -751,7 +1151,9 @@ var ByBitConnectorCreator = async (config) => {
751
1151
  price: entryPrice,
752
1152
  currentPrice,
753
1153
  unrealizedPnl,
754
- direction: side === "Buy" ? "LONG" : "SHORT"
1154
+ direction: side === "Buy" ? "LONG" : "SHORT",
1155
+ ...takeProfitPrice != null && takeProfitPrice > 0 ? { takeProfitPrice } : {},
1156
+ ...stopLossPrice != null && stopLossPrice > 0 ? { stopLossPrice } : {}
755
1157
  };
756
1158
  }).filter(
757
1159
  (item) => item != null && item.symbol.length > 0
@@ -767,23 +1169,47 @@ var ByBitConnectorCreator = async (config) => {
767
1169
  if (!client) {
768
1170
  return [];
769
1171
  }
770
- const response = await client.getClosedPnL({
771
- category: MARKET_CATEGORY2,
1172
+ const closedPnlRows = [];
1173
+ let cursor;
1174
+ const pageLimit = Math.min(
1175
+ Math.max(1, Math.trunc(limit)),
1176
+ CLOSED_PNL_PAGE_LIMIT
1177
+ );
1178
+ for (let page = 0; page < CLOSED_PNL_MAX_PAGES; page += 1) {
1179
+ const response = await client.getClosedPnL({
1180
+ category: BYBIT_CATEGORY2,
1181
+ startTime,
1182
+ endTime,
1183
+ symbol,
1184
+ limit: pageLimit,
1185
+ ...cursor ? { cursor } : {}
1186
+ });
1187
+ if (response.retCode !== 0) {
1188
+ logger3.log(
1189
+ "error",
1190
+ "closedPnl retCode: %s, %s",
1191
+ response.retCode,
1192
+ response.retMsg
1193
+ );
1194
+ return [];
1195
+ }
1196
+ closedPnlRows.push(...response.result?.list ?? []);
1197
+ const nextCursor = String(response.result?.nextPageCursor ?? "").trim();
1198
+ if (!nextCursor || nextCursor === cursor) {
1199
+ break;
1200
+ }
1201
+ cursor = nextCursor;
1202
+ }
1203
+ if (!closedPnlRows.length) {
1204
+ return [];
1205
+ }
1206
+ const fundingRows = await loadFundingFeeRows({
1207
+ client,
772
1208
  startTime,
773
1209
  endTime,
774
- symbol,
775
- limit: Math.min(Math.max(1, Math.trunc(limit)), 100)
1210
+ symbol
776
1211
  });
777
- if (response.retCode !== 0) {
778
- logger3.log(
779
- "error",
780
- "closedPnl retCode: %s, %s",
781
- response.retCode,
782
- response.retMsg
783
- );
784
- return [];
785
- }
786
- return (response.result?.list ?? []).map((item) => {
1212
+ return closedPnlRows.map((item) => {
787
1213
  const qty = Number(item.qty ?? item.closedSize ?? Number.NaN);
788
1214
  const entryPrice = Number(item.avgEntryPrice ?? Number.NaN);
789
1215
  const exitPrice = Number(item.avgExitPrice ?? Number.NaN);
@@ -791,7 +1217,23 @@ var ByBitConnectorCreator = async (config) => {
791
1217
  const closedAt = Number(
792
1218
  item.updatedTime ?? item.createdTime ?? Number.NaN
793
1219
  );
1220
+ const entryTimestamp = Number(item.createdTime ?? Number.NaN);
794
1221
  const orderId = typeof item.orderId === "string" && item.orderId.trim() ? item.orderId : null;
1222
+ const orderLinkId = getOptionalStringField(item, "orderLinkId");
1223
+ const openFee = toFiniteNumberOrNull(item.openFee);
1224
+ const closeFee = toFiniteNumberOrNull(item.closeFee);
1225
+ const fundingFee = sumFundingFeeForTrade({
1226
+ rows: fundingRows,
1227
+ symbol: String(item.symbol ?? ""),
1228
+ entryTimestamp: Number.isFinite(entryTimestamp) ? entryTimestamp : null,
1229
+ closedAt
1230
+ });
1231
+ const totalFee = sumOptionalFees([openFee, closeFee, fundingFee]);
1232
+ const direction = Number.isFinite(entryPrice) && Number.isFinite(exitPrice) ? inferClosedPnlDirection({
1233
+ entryPrice,
1234
+ exitPrice,
1235
+ closedPnl
1236
+ }) : null;
795
1237
  if (!String(item.symbol ?? "").trim() || !Number.isFinite(qty) || !Number.isFinite(closedPnl) || !Number.isFinite(closedAt)) {
796
1238
  return null;
797
1239
  }
@@ -802,29 +1244,145 @@ var ByBitConnectorCreator = async (config) => {
802
1244
  exitPrice: Number.isFinite(exitPrice) ? exitPrice : null,
803
1245
  closedPnl,
804
1246
  closedAt,
805
- ...orderId ? { orderId } : {}
1247
+ ...direction ? { direction } : {},
1248
+ ...Number.isFinite(entryTimestamp) ? { entryTimestamp } : {},
1249
+ ...orderId ? { orderId } : {},
1250
+ ...orderLinkId ? { orderLinkId } : {},
1251
+ ...openFee != null ? { openFee } : {},
1252
+ ...closeFee != null ? { closeFee } : {},
1253
+ ...fundingFee != null ? { fundingFee } : {},
1254
+ ...totalFee != null ? { totalFee } : {}
806
1255
  };
807
1256
  }).filter((item) => item != null);
808
1257
  },
809
- placeOrder: async ({ symbol, price, qty, direction, isLimit, orderId }) => {
1258
+ getEntryExecutions: async ({
1259
+ startTime,
1260
+ endTime,
1261
+ symbol,
1262
+ limit = 100
1263
+ }) => {
1264
+ const client = await getPrivateClient();
1265
+ if (!client) {
1266
+ return [];
1267
+ }
1268
+ const executionRows = [];
1269
+ let cursor;
1270
+ const pageLimit = Math.min(
1271
+ Math.max(1, Math.trunc(limit)),
1272
+ EXECUTION_PAGE_LIMIT
1273
+ );
1274
+ for (let page = 0; page < EXECUTION_MAX_PAGES; page += 1) {
1275
+ const response = await client.getExecutionList({
1276
+ category: BYBIT_CATEGORY2,
1277
+ settleCoin: "USDT",
1278
+ startTime,
1279
+ endTime,
1280
+ symbol,
1281
+ limit: pageLimit,
1282
+ ...cursor ? { cursor } : {}
1283
+ });
1284
+ if (response.retCode !== 0) {
1285
+ logger3.log(
1286
+ "error",
1287
+ "entryExecutions retCode: %s, %s",
1288
+ response.retCode,
1289
+ response.retMsg
1290
+ );
1291
+ return [];
1292
+ }
1293
+ executionRows.push(...response.result?.list ?? []);
1294
+ const nextCursor = String(response.result?.nextPageCursor ?? "").trim();
1295
+ if (!nextCursor || nextCursor === cursor) {
1296
+ break;
1297
+ }
1298
+ cursor = nextCursor;
1299
+ }
1300
+ if (!executionRows.length) {
1301
+ return [];
1302
+ }
1303
+ const fundingRows = await loadFundingFeeRows({
1304
+ client,
1305
+ startTime,
1306
+ endTime,
1307
+ symbol
1308
+ });
1309
+ return executionRows.map((item) => {
1310
+ const qty = Number(item.execQty ?? item.orderQty ?? Number.NaN);
1311
+ const entryPrice = Number(item.execPrice ?? Number.NaN);
1312
+ const entryTimestamp = Number(item.execTime ?? Number.NaN);
1313
+ const side = String(item.side ?? "");
1314
+ const orderId = typeof item.orderId === "string" && item.orderId.trim() ? item.orderId : null;
1315
+ const orderLinkId = typeof item.orderLinkId === "string" && item.orderLinkId.trim() ? item.orderLinkId : null;
1316
+ const openFee = toFiniteNumberOrNull(item.execFeeV2 ?? item.execFee);
1317
+ const fundingFee = sumFundingFeeForTrade({
1318
+ rows: fundingRows,
1319
+ symbol: String(item.symbol ?? ""),
1320
+ entryTimestamp,
1321
+ closedAt: endTime
1322
+ });
1323
+ const totalFee = sumOptionalFees([openFee, fundingFee]);
1324
+ if (!String(item.symbol ?? "").trim() || !Number.isFinite(qty) || !Number.isFinite(entryTimestamp) || !Number.isFinite(entryPrice) || side !== "Buy" && side !== "Sell" || !orderLinkId) {
1325
+ return null;
1326
+ }
1327
+ return {
1328
+ symbol: String(item.symbol),
1329
+ qty,
1330
+ entryPrice,
1331
+ entryTimestamp,
1332
+ direction: side === "Buy" ? "LONG" : "SHORT",
1333
+ ...orderId ? { orderId } : {},
1334
+ ...orderLinkId ? { orderLinkId } : {},
1335
+ ...openFee != null ? { openFee } : {},
1336
+ ...fundingFee != null ? { fundingFee } : {},
1337
+ ...totalFee != null ? { totalFee } : {}
1338
+ };
1339
+ }).filter((item) => item != null).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
1340
+ },
1341
+ placeOrder: async ({
1342
+ symbol,
1343
+ price,
1344
+ qty,
1345
+ direction,
1346
+ isLimit,
1347
+ orderId,
1348
+ signal,
1349
+ leverage
1350
+ }) => {
1351
+ invalidatePositionSnapshot(symbol);
810
1352
  const client = await getPrivateClient();
811
1353
  const marketDataClient = await getPublicClient();
812
1354
  if (!client || !marketDataClient) {
1355
+ if (signal) {
1356
+ signal.orderFailureReason = "BYBIT_CLIENT_UNAVAILABLE";
1357
+ }
813
1358
  return false;
814
1359
  }
815
1360
  const isLong = direction === "LONG";
816
1361
  const meta = await getSymbolMeta(marketDataClient, symbol);
817
- const { qtyNum: orderQty, qtyStr: orderQtyStr } = normalizeQty(qty, meta);
818
- if (orderQty < meta.minOrderQty) {
1362
+ const normalizedQty = normalizeQty(qty, meta);
1363
+ const minNormalizedQty = normalizeQty(meta.minOrderQty, meta);
1364
+ const isQtyBelowMin = normalizedQty.qtyNum < meta.minOrderQty;
1365
+ const orderQty = isQtyBelowMin ? Math.max(minNormalizedQty.qtyNum, meta.minOrderQty) : normalizedQty.qtyNum;
1366
+ const orderQtyStr = isQtyBelowMin ? orderQty.toFixed(meta.qtyPrecision) : normalizedQty.qtyStr;
1367
+ if (isQtyBelowMin) {
819
1368
  logger3.log(
820
1369
  "warn",
821
- "placeOrder: qty too small: %s",
1370
+ "placeOrder: qty below min, using minOrderQty: %s",
822
1371
  toJson(
823
- { symbol, qty, orderQty, minOrderQty: meta.minOrderQty },
1372
+ {
1373
+ symbol,
1374
+ qty,
1375
+ requestedOrderQty: normalizedQty.qtyNum,
1376
+ orderQty,
1377
+ minOrderQty: meta.minOrderQty
1378
+ },
824
1379
  true
825
1380
  )
826
1381
  );
827
- return false;
1382
+ }
1383
+ if (signal) {
1384
+ signal.orderQty = orderQty;
1385
+ signal.orderValue = orderQty * price;
828
1386
  }
829
1387
  const entryNormalized = isLimit ? normalizePrice(price, "ENTRY", meta) : void 0;
830
1388
  logger3.log(
@@ -842,14 +1400,16 @@ var ByBitConnectorCreator = async (config) => {
842
1400
  true
843
1401
  )
844
1402
  );
1403
+ const requestedLeverage = typeof leverage === "number" && Number.isFinite(leverage) ? Math.max(1, leverage) : 10;
1404
+ const effectiveLeverage = meta.maxLeverage != null ? Math.min(requestedLeverage, meta.maxLeverage) : requestedLeverage;
845
1405
  await client.setLeverage({
846
- category: MARKET_CATEGORY2,
1406
+ category: BYBIT_CATEGORY2,
847
1407
  symbol,
848
- buyLeverage: "10",
849
- sellLeverage: "10"
1408
+ buyLeverage: String(effectiveLeverage),
1409
+ sellLeverage: String(effectiveLeverage)
850
1410
  });
851
1411
  const orderRes = await client.submitOrder({
852
- category: MARKET_CATEGORY2,
1412
+ category: BYBIT_CATEGORY2,
853
1413
  symbol,
854
1414
  price: entryNormalized?.priceStr || void 0,
855
1415
  side: isLong ? "Buy" : "Sell",
@@ -864,19 +1424,27 @@ var ByBitConnectorCreator = async (config) => {
864
1424
  toJson(orderRes, true)
865
1425
  );
866
1426
  if (orderRes.retCode !== 0) {
1427
+ if (signal) {
1428
+ signal.orderFailureReason = typeof orderRes.retMsg === "string" && orderRes.retMsg.trim() ? orderRes.retMsg.trim() : `BYBIT_RETCODE_${orderRes.retCode}`;
1429
+ }
867
1430
  return false;
868
1431
  }
1432
+ if (signal) {
1433
+ signal.orderFailureReason = void 0;
1434
+ }
1435
+ invalidatePositionSnapshot(symbol);
869
1436
  return true;
870
1437
  },
871
1438
  setTakeProfits,
872
1439
  setStopLoss,
873
1440
  closePosition: async ({ symbol, direction }) => {
1441
+ invalidatePositionSnapshot(symbol);
874
1442
  const client = await getPrivateClient();
875
1443
  if (!client) {
876
1444
  return false;
877
1445
  }
878
1446
  const closeRes = await client.submitOrder({
879
- category: MARKET_CATEGORY2,
1447
+ category: BYBIT_CATEGORY2,
880
1448
  symbol,
881
1449
  side: direction === "LONG" ? "Sell" : "Buy",
882
1450
  orderType: "Market",
@@ -893,23 +1461,111 @@ var ByBitConnectorCreator = async (config) => {
893
1461
  if (closeRes.retCode !== 0) {
894
1462
  return false;
895
1463
  }
1464
+ invalidatePositionSnapshot(symbol);
896
1465
  return true;
897
1466
  },
898
- getTickers: async () => {
1467
+ getFundingRateHistory: async (rateRequest) => {
1468
+ const client = await getPublicClient();
1469
+ if (!client) return [];
1470
+ const safeLimit = Math.min(
1471
+ FUNDING_HISTORY_LIMIT,
1472
+ Math.max(1, Math.trunc(rateRequest.limit ?? FUNDING_HISTORY_LIMIT))
1473
+ );
1474
+ const byTimestamp = /* @__PURE__ */ new Map();
1475
+ let cursorEnd = Math.trunc(rateRequest.endTime);
1476
+ while (Number.isFinite(cursorEnd) && cursorEnd >= (rateRequest.startTime ?? 0)) {
1477
+ const response = await client.getFundingRateHistory({
1478
+ category: BYBIT_CATEGORY2,
1479
+ symbol: rateRequest.symbol.trim().toUpperCase(),
1480
+ endTime: cursorEnd,
1481
+ limit: safeLimit
1482
+ });
1483
+ if (response.retCode !== 0) {
1484
+ throw new Error(
1485
+ `Bybit funding history failed: ${response.retCode} ${response.retMsg}`
1486
+ );
1487
+ }
1488
+ const page = response.result?.list ?? [];
1489
+ if (!page.length) break;
1490
+ let oldestTimestamp = Number.POSITIVE_INFINITY;
1491
+ for (const item of page) {
1492
+ const timestamp = Number(item.fundingRateTimestamp);
1493
+ const rate = Number(item.fundingRate);
1494
+ if (!Number.isFinite(timestamp) || !Number.isFinite(rate)) continue;
1495
+ oldestTimestamp = Math.min(oldestTimestamp, timestamp);
1496
+ if (timestamp >= (rateRequest.startTime ?? 0)) {
1497
+ byTimestamp.set(timestamp, {
1498
+ symbol: String(item.symbol ?? rateRequest.symbol),
1499
+ timestamp,
1500
+ rate
1501
+ });
1502
+ }
1503
+ }
1504
+ if (page.length < safeLimit || !Number.isFinite(oldestTimestamp) || oldestTimestamp <= (rateRequest.startTime ?? 0)) {
1505
+ break;
1506
+ }
1507
+ const nextEnd = oldestTimestamp - 1;
1508
+ if (nextEnd >= cursorEnd) break;
1509
+ cursorEnd = nextEnd;
1510
+ }
1511
+ return [...byTimestamp.values()].sort(
1512
+ (left, right) => left.timestamp - right.timestamp
1513
+ );
1514
+ },
1515
+ getTradingFeeRate: async (symbol) => {
1516
+ const client = await getPrivateClient();
1517
+ if (!client) return null;
1518
+ const normalizedSymbol = symbol.trim().toUpperCase();
1519
+ const response = await client.getFeeRate({
1520
+ category: BYBIT_CATEGORY2,
1521
+ symbol: normalizedSymbol
1522
+ });
1523
+ if (response.retCode !== 0) return null;
1524
+ const row = (response.result?.list ?? []).find(
1525
+ (item) => String(item.symbol ?? "").toUpperCase() === normalizedSymbol
1526
+ );
1527
+ const makerRate = Number(row?.makerFeeRate);
1528
+ const takerRate = Number(row?.takerFeeRate);
1529
+ if (!Number.isFinite(makerRate) || !Number.isFinite(takerRate)) {
1530
+ return null;
1531
+ }
1532
+ return {
1533
+ symbol: normalizedSymbol,
1534
+ makerRate,
1535
+ takerRate,
1536
+ source: "exchange-account",
1537
+ capturedAt: Date.now()
1538
+ };
1539
+ },
1540
+ getTickers: async (query = {}) => {
899
1541
  const client = await getPublicClient();
900
1542
  if (!client) {
901
1543
  return [];
902
1544
  }
1545
+ const requestedUniverse = resolveConnectorUniverse(
1546
+ BYBIT_CAPABILITIES,
1547
+ query.universe ?? universe
1548
+ );
1549
+ const instruments = await listInstruments({
1550
+ ...query,
1551
+ universe: requestedUniverse
1552
+ });
1553
+ const allowedSymbols = instruments.length ? new Set(instruments.map((instrument) => instrument.symbol)) : null;
903
1554
  const data = await client.getTickers({
904
- category: MARKET_CATEGORY2
1555
+ category: BYBIT_CATEGORY2
905
1556
  });
906
- return data.result.list.map((item) => normalizeTickerData(item));
1557
+ return data.result.list.filter(
1558
+ (item) => allowedSymbols == null || allowedSymbols.has(String(item.symbol ?? ""))
1559
+ ).map((item) => normalizeTickerData(item));
907
1560
  }
908
1561
  };
909
1562
  };
910
1563
 
911
1564
  // src/Binance/index.ts
912
1565
  import { fetchWithRetry } from "@tradejs/infra/http";
1566
+ import {
1567
+ resolveConnectorUniverse as resolveConnectorUniverse2
1568
+ } from "@tradejs/types";
913
1569
  var INTERVAL_MAP = {
914
1570
  "1": "1m",
915
1571
  "3": "3m",
@@ -944,12 +1600,21 @@ var toNum = (value, fallback = 0) => {
944
1600
  const num = Number(value);
945
1601
  return Number.isFinite(num) ? num : fallback;
946
1602
  };
1603
+ var toNullableNum = (value) => {
1604
+ const num = Number(value);
1605
+ return Number.isFinite(num) ? num : null;
1606
+ };
947
1607
  var intervalToMinutes = (interval) => {
948
1608
  const intervalMs = INTERVAL_MS[String(interval)];
949
1609
  return intervalMs ? Math.floor(intervalMs / 6e4) : null;
950
1610
  };
951
- var BinanceConnectorCreator = async () => {
1611
+ var capabilities = {
1612
+ supportedUniverses: ["crypto"],
1613
+ defaultUniverse: "crypto"
1614
+ };
1615
+ var BinanceConnectorCreator = async (config) => {
952
1616
  let state = {};
1617
+ const universe = resolveConnectorUniverse2(capabilities, config.universe);
953
1618
  const requestKline = async ({
954
1619
  symbol,
955
1620
  interval,
@@ -981,14 +1646,24 @@ var BinanceConnectorCreator = async () => {
981
1646
  if (!Array.isArray(item)) continue;
982
1647
  const ts = toNum(item[0], 0);
983
1648
  if (!ts) continue;
1649
+ const volume = toNum(item[5]);
1650
+ const turnover = toNum(item[7]);
1651
+ const trades = toNullableNum(item[8]);
1652
+ const takerBuyBaseVolume = toNullableNum(item[9]);
1653
+ const takerBuyQuoteVolume = toNullableNum(item[10]);
984
1654
  rows.push({
985
1655
  timestamp: ts,
986
1656
  open: toNum(item[1]),
987
1657
  high: toNum(item[2]),
988
1658
  low: toNum(item[3]),
989
1659
  close: toNum(item[4]),
990
- volume: toNum(item[5]),
991
- turnover: toNum(item[7]),
1660
+ volume,
1661
+ turnover,
1662
+ trades,
1663
+ takerBuyBaseVolume,
1664
+ takerBuyQuoteVolume,
1665
+ takerSellBaseVolume: takerBuyBaseVolume == null ? null : Math.max(0, volume - takerBuyBaseVolume),
1666
+ takerSellQuoteVolume: takerBuyQuoteVolume == null ? null : Math.max(0, turnover - takerBuyQuoteVolume),
992
1667
  dt: new Date(ts).toISOString()
993
1668
  });
994
1669
  lastTs = ts;
@@ -999,7 +1674,64 @@ var BinanceConnectorCreator = async () => {
999
1674
  rows.sort((a, b) => a.timestamp - b.timestamp);
1000
1675
  return rows;
1001
1676
  };
1677
+ const loadTickers = async () => {
1678
+ const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
1679
+ const response = await fetchWithRetry(`${baseUrl}/api/v3/ticker/24hr`, {
1680
+ headers: { "User-Agent": "tradejs/binance-connector" }
1681
+ });
1682
+ if (!response.ok) return [];
1683
+ const payload = await response.json();
1684
+ if (!Array.isArray(payload)) return [];
1685
+ return payload.map((row) => {
1686
+ const symbol = String(row.symbol ?? "");
1687
+ return {
1688
+ symbol,
1689
+ lastPrice: toNum(row.lastPrice),
1690
+ indexPrice: toNum(row.lastPrice),
1691
+ markPrice: toNum(row.lastPrice),
1692
+ prevPrice24h: toNum(row.openPrice),
1693
+ price24hPcnt: toNum(row.priceChangePercent) / 100,
1694
+ highPrice24h: toNum(row.highPrice),
1695
+ lowPrice24h: toNum(row.lowPrice),
1696
+ prevPrice1h: 0,
1697
+ openInterest: 0,
1698
+ openInterestValue: 0,
1699
+ turnover24h: toNum(row.quoteVolume),
1700
+ volume24h: toNum(row.volume),
1701
+ fundingRate: 0,
1702
+ nextFundingTime: 0,
1703
+ predictedDeliveryPrice: "",
1704
+ basisRate: "",
1705
+ deliveryFeeRate: "",
1706
+ deliveryTime: 0,
1707
+ ask1Size: toNum(row.askQty),
1708
+ bid1Price: toNum(row.bidPrice),
1709
+ ask1Price: toNum(row.askPrice),
1710
+ bid1Size: toNum(row.bidQty),
1711
+ basis: "",
1712
+ preOpenPrice: "",
1713
+ preQty: ""
1714
+ };
1715
+ });
1716
+ };
1002
1717
  return {
1718
+ capabilities,
1719
+ universe,
1720
+ accountId: config.accountId,
1721
+ deploymentId: config.deploymentId,
1722
+ listInstruments: async (query) => {
1723
+ resolveConnectorUniverse2(capabilities, query?.universe ?? universe);
1724
+ const symbols = query?.symbols?.length ? new Set(query.symbols.map((symbol) => symbol.trim().toUpperCase())) : null;
1725
+ const tickers = await loadTickers();
1726
+ return tickers.filter((ticker) => !symbols || symbols.has(ticker.symbol)).map((ticker) => ({
1727
+ provider: "binance",
1728
+ symbol: ticker.symbol,
1729
+ kind: "spot",
1730
+ assetClass: "crypto",
1731
+ universe: "crypto",
1732
+ status: "trading"
1733
+ }));
1734
+ },
1003
1735
  getState: async () => state,
1004
1736
  setState: async (newState) => {
1005
1737
  state = { ...state, ...newState };
@@ -1016,51 +1748,93 @@ var BinanceConnectorCreator = async () => {
1016
1748
  setTakeProfits: async () => false,
1017
1749
  setStopLoss: async () => false,
1018
1750
  closePosition: async () => false,
1019
- getTickers: async () => {
1751
+ getTickers: async (query) => {
1752
+ resolveConnectorUniverse2(capabilities, query?.universe ?? universe);
1753
+ return loadTickers();
1754
+ },
1755
+ getTopOfBookTicker: async (symbol) => {
1756
+ const normalizedSymbol = String(symbol || "").trim().toUpperCase();
1757
+ if (!normalizedSymbol) return null;
1758
+ const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
1759
+ const url = new URL(`${baseUrl}/api/v3/ticker/bookTicker`);
1760
+ url.searchParams.set("symbol", normalizedSymbol);
1761
+ const response = await fetchWithRetry(url.toString(), {
1762
+ headers: { "User-Agent": "tradejs/binance-connector" }
1763
+ });
1764
+ if (!response.ok) return null;
1765
+ const row = await response.json();
1766
+ return {
1767
+ symbol: String(row.symbol ?? normalizedSymbol),
1768
+ bidPrice: toNum(row.bidPrice),
1769
+ bidQty: toNum(row.bidQty),
1770
+ askPrice: toNum(row.askPrice),
1771
+ askQty: toNum(row.askQty),
1772
+ timestamp: Date.now()
1773
+ };
1774
+ },
1775
+ getAggTrades: async ({ symbol, startTime, endTime, limit = 1e3 }) => {
1776
+ const normalizedSymbol = String(symbol || "").trim().toUpperCase();
1777
+ if (!normalizedSymbol) return [];
1778
+ const safeLimit = Math.min(Math.max(Math.floor(limit), 1), 1e3);
1020
1779
  const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
1021
- const response = await fetchWithRetry(`${baseUrl}/api/v3/ticker/24hr`, {
1780
+ const url = new URL(`${baseUrl}/api/v3/aggTrades`);
1781
+ url.searchParams.set("symbol", normalizedSymbol);
1782
+ url.searchParams.set("startTime", String(Math.floor(startTime)));
1783
+ url.searchParams.set("endTime", String(Math.floor(endTime)));
1784
+ url.searchParams.set("limit", String(safeLimit));
1785
+ const response = await fetchWithRetry(url.toString(), {
1022
1786
  headers: { "User-Agent": "tradejs/binance-connector" }
1023
1787
  });
1024
1788
  if (!response.ok) return [];
1025
1789
  const payload = await response.json();
1026
1790
  if (!Array.isArray(payload)) return [];
1027
- return payload.map((row) => {
1028
- const symbol = String(row.symbol ?? "");
1029
- return {
1030
- symbol,
1031
- lastPrice: toNum(row.lastPrice),
1032
- indexPrice: toNum(row.lastPrice),
1033
- markPrice: toNum(row.lastPrice),
1034
- prevPrice24h: toNum(row.openPrice),
1035
- price24hPcnt: toNum(row.priceChangePercent) / 100,
1036
- highPrice24h: toNum(row.highPrice),
1037
- lowPrice24h: toNum(row.lowPrice),
1038
- prevPrice1h: 0,
1039
- openInterest: 0,
1040
- openInterestValue: 0,
1041
- turnover24h: toNum(row.quoteVolume),
1042
- volume24h: toNum(row.volume),
1043
- fundingRate: 0,
1044
- nextFundingTime: 0,
1045
- predictedDeliveryPrice: "",
1046
- basisRate: "",
1047
- deliveryFeeRate: "",
1048
- deliveryTime: 0,
1049
- ask1Size: toNum(row.askQty),
1050
- bid1Price: toNum(row.bidPrice),
1051
- ask1Price: toNum(row.askPrice),
1052
- bid1Size: toNum(row.bidQty),
1053
- basis: "",
1054
- preOpenPrice: "",
1055
- preQty: ""
1056
- };
1791
+ return payload.map((row) => ({
1792
+ aggregateTradeId: toNum(row.a, Number.NaN),
1793
+ price: toNum(row.p, Number.NaN),
1794
+ quantity: toNum(row.q, Number.NaN),
1795
+ firstTradeId: toNum(row.f, Number.NaN),
1796
+ lastTradeId: toNum(row.l, Number.NaN),
1797
+ timestamp: toNum(row.T, Number.NaN),
1798
+ isBuyerMaker: Boolean(row.m)
1799
+ })).filter(
1800
+ (row) => Number.isFinite(row.aggregateTradeId) && Number.isFinite(row.price) && Number.isFinite(row.quantity) && Number.isFinite(row.timestamp)
1801
+ );
1802
+ },
1803
+ getOrderBookDepth: async ({ symbol, limit = 100 }) => {
1804
+ const normalizedSymbol = String(symbol || "").trim().toUpperCase();
1805
+ if (!normalizedSymbol) return null;
1806
+ const allowedLimits = [5, 10, 20, 50, 100, 500, 1e3, 5e3];
1807
+ const safeLimit = allowedLimits.includes(limit) ? limit : 100;
1808
+ const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
1809
+ const url = new URL(`${baseUrl}/api/v3/depth`);
1810
+ url.searchParams.set("symbol", normalizedSymbol);
1811
+ url.searchParams.set("limit", String(safeLimit));
1812
+ const response = await fetchWithRetry(url.toString(), {
1813
+ headers: { "User-Agent": "tradejs/binance-connector" }
1057
1814
  });
1815
+ if (!response.ok) return null;
1816
+ const payload = await response.json();
1817
+ const parseLevels = (levels) => (Array.isArray(levels) ? levels : []).map(
1818
+ (level) => Array.isArray(level) ? [toNum(level[0], Number.NaN), toNum(level[1], Number.NaN)] : null
1819
+ ).filter(
1820
+ (level) => level != null && Number.isFinite(level[0]) && Number.isFinite(level[1])
1821
+ );
1822
+ return {
1823
+ symbol: normalizedSymbol,
1824
+ lastUpdateId: toNullableNum(payload.lastUpdateId),
1825
+ bids: parseLevels(payload.bids),
1826
+ asks: parseLevels(payload.asks),
1827
+ timestamp: Date.now()
1828
+ };
1058
1829
  }
1059
1830
  };
1060
1831
  };
1061
1832
 
1062
1833
  // src/Coinbase/index.ts
1063
1834
  import { fetchWithRetry as fetchWithRetry2 } from "@tradejs/infra/http";
1835
+ import {
1836
+ resolveConnectorUniverse as resolveConnectorUniverse3
1837
+ } from "@tradejs/types";
1064
1838
  var INTERVAL_MS2 = {
1065
1839
  "1": 6e4,
1066
1840
  "3": 18e4,
@@ -1123,8 +1897,13 @@ var MAJOR_PRODUCTS = [
1123
1897
  "LINK-USD",
1124
1898
  "AVAX-USD"
1125
1899
  ];
1126
- var CoinbaseConnectorCreator = async () => {
1900
+ var capabilities2 = {
1901
+ supportedUniverses: ["crypto"],
1902
+ defaultUniverse: "crypto"
1903
+ };
1904
+ var CoinbaseConnectorCreator = async (config) => {
1127
1905
  let state = {};
1906
+ const universe = resolveConnectorUniverse3(capabilities2, config.universe);
1128
1907
  const requestKline = async ({
1129
1908
  symbol,
1130
1909
  interval,
@@ -1176,7 +1955,76 @@ var CoinbaseConnectorCreator = async () => {
1176
1955
  }
1177
1956
  return [...dedup.values()].sort((a, b) => a.timestamp - b.timestamp);
1178
1957
  };
1958
+ const loadTickers = async () => {
1959
+ const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
1960
+ const entries = await Promise.all(
1961
+ MAJOR_PRODUCTS.map(async (product) => {
1962
+ const [tickerRes, statsRes] = await Promise.all([
1963
+ fetchWithRetry2(`${baseUrl}/products/${product}/ticker`, {
1964
+ headers: { "User-Agent": "tradejs/coinbase-connector" }
1965
+ }),
1966
+ fetchWithRetry2(`${baseUrl}/products/${product}/stats`, {
1967
+ headers: { "User-Agent": "tradejs/coinbase-connector" }
1968
+ })
1969
+ ]);
1970
+ if (!tickerRes.ok || !statsRes.ok) return null;
1971
+ const ticker = await tickerRes.json();
1972
+ const stats = await statsRes.json();
1973
+ const base = product.replace("-USD", "");
1974
+ const symbol = `${base}USDT`;
1975
+ const open = toNum2(stats.open);
1976
+ const last = toNum2(ticker.price);
1977
+ const pct = open > 0 ? (last - open) / open : 0;
1978
+ return {
1979
+ symbol,
1980
+ lastPrice: last,
1981
+ indexPrice: last,
1982
+ markPrice: last,
1983
+ prevPrice24h: open,
1984
+ price24hPcnt: pct,
1985
+ highPrice24h: toNum2(stats.high),
1986
+ lowPrice24h: toNum2(stats.low),
1987
+ prevPrice1h: 0,
1988
+ openInterest: 0,
1989
+ openInterestValue: 0,
1990
+ turnover24h: 0,
1991
+ volume24h: toNum2(stats.volume),
1992
+ fundingRate: 0,
1993
+ nextFundingTime: 0,
1994
+ predictedDeliveryPrice: "",
1995
+ basisRate: "",
1996
+ deliveryFeeRate: "",
1997
+ deliveryTime: 0,
1998
+ ask1Size: toNum2(ticker.ask_size),
1999
+ bid1Price: toNum2(ticker.bid),
2000
+ ask1Price: toNum2(ticker.ask),
2001
+ bid1Size: toNum2(ticker.bid_size),
2002
+ basis: "",
2003
+ preOpenPrice: "",
2004
+ preQty: ""
2005
+ };
2006
+ })
2007
+ );
2008
+ return entries.filter((item) => item != null);
2009
+ };
1179
2010
  return {
2011
+ capabilities: capabilities2,
2012
+ universe,
2013
+ accountId: config.accountId,
2014
+ deploymentId: config.deploymentId,
2015
+ listInstruments: async (query) => {
2016
+ resolveConnectorUniverse3(capabilities2, query?.universe ?? universe);
2017
+ const symbols = query?.symbols?.length ? new Set(query.symbols.map((symbol) => symbol.trim().toUpperCase())) : null;
2018
+ const tickers = await loadTickers();
2019
+ return tickers.filter((ticker) => !symbols || symbols.has(ticker.symbol)).map((ticker) => ({
2020
+ provider: "coinbase",
2021
+ symbol: ticker.symbol,
2022
+ kind: "spot",
2023
+ assetClass: "crypto",
2024
+ universe: "crypto",
2025
+ status: "trading"
2026
+ }));
2027
+ },
1180
2028
  getState: async () => state,
1181
2029
  setState: async (newState) => {
1182
2030
  state = { ...state, ...newState };
@@ -1193,57 +2041,9 @@ var CoinbaseConnectorCreator = async () => {
1193
2041
  setTakeProfits: async () => false,
1194
2042
  setStopLoss: async () => false,
1195
2043
  closePosition: async () => false,
1196
- getTickers: async () => {
1197
- const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
1198
- const entries = await Promise.all(
1199
- MAJOR_PRODUCTS.map(async (product) => {
1200
- const [tickerRes, statsRes] = await Promise.all([
1201
- fetchWithRetry2(`${baseUrl}/products/${product}/ticker`, {
1202
- headers: { "User-Agent": "tradejs/coinbase-connector" }
1203
- }),
1204
- fetchWithRetry2(`${baseUrl}/products/${product}/stats`, {
1205
- headers: { "User-Agent": "tradejs/coinbase-connector" }
1206
- })
1207
- ]);
1208
- if (!tickerRes.ok || !statsRes.ok) return null;
1209
- const ticker = await tickerRes.json();
1210
- const stats = await statsRes.json();
1211
- const base = product.replace("-USD", "");
1212
- const symbol = `${base}USDT`;
1213
- const open = toNum2(stats.open);
1214
- const last = toNum2(ticker.price);
1215
- const pct = open > 0 ? (last - open) / open : 0;
1216
- return {
1217
- symbol,
1218
- lastPrice: last,
1219
- indexPrice: last,
1220
- markPrice: last,
1221
- prevPrice24h: open,
1222
- price24hPcnt: pct,
1223
- highPrice24h: toNum2(stats.high),
1224
- lowPrice24h: toNum2(stats.low),
1225
- prevPrice1h: 0,
1226
- openInterest: 0,
1227
- openInterestValue: 0,
1228
- turnover24h: 0,
1229
- volume24h: toNum2(stats.volume),
1230
- fundingRate: 0,
1231
- nextFundingTime: 0,
1232
- predictedDeliveryPrice: "",
1233
- basisRate: "",
1234
- deliveryFeeRate: "",
1235
- deliveryTime: 0,
1236
- ask1Size: toNum2(ticker.ask_size),
1237
- bid1Price: toNum2(ticker.bid),
1238
- ask1Price: toNum2(ticker.ask),
1239
- bid1Size: toNum2(ticker.bid_size),
1240
- basis: "",
1241
- preOpenPrice: "",
1242
- preQty: ""
1243
- };
1244
- })
1245
- );
1246
- return entries.filter((item) => item != null);
2044
+ getTickers: async (query) => {
2045
+ resolveConnectorUniverse3(capabilities2, query?.universe ?? universe);
2046
+ return loadTickers();
1247
2047
  }
1248
2048
  };
1249
2049
  };
@@ -1263,18 +2063,26 @@ var mapBinanceKline = (payload) => payload.map((item) => {
1263
2063
  const low = Number(item[3]);
1264
2064
  const close = Number(item[4]);
1265
2065
  const volume = Number(item[5]);
2066
+ const turnover = Number(item[7]) || 0;
2067
+ const takerBuyBaseVolume = Number(item[9]);
2068
+ const takerBuyQuoteVolume = Number(item[10]);
1266
2069
  if (![ts, open, high, low, close, volume].every(Number.isFinite))
1267
2070
  return null;
1268
- return {
2071
+ const row = {
1269
2072
  timestamp: ts,
1270
2073
  open,
1271
2074
  high,
1272
2075
  low,
1273
2076
  close,
1274
2077
  volume,
1275
- turnover: Number(item[7]) || 0,
2078
+ turnover,
2079
+ takerBuyBaseVolume: Number.isFinite(takerBuyBaseVolume) ? takerBuyBaseVolume : null,
2080
+ takerBuyQuoteVolume: Number.isFinite(takerBuyQuoteVolume) ? takerBuyQuoteVolume : null,
2081
+ takerSellBaseVolume: Number.isFinite(takerBuyBaseVolume) ? Math.max(0, volume - takerBuyBaseVolume) : null,
2082
+ takerSellQuoteVolume: Number.isFinite(takerBuyQuoteVolume) ? Math.max(0, turnover - takerBuyQuoteVolume) : null,
1276
2083
  dt: new Date(ts).toISOString()
1277
2084
  };
2085
+ return row;
1278
2086
  }).filter((item) => item != null).sort((a, b) => a.timestamp - b.timestamp);
1279
2087
  var mapCoinbaseKline = (payload) => payload.map((item) => {
1280
2088
  if (!Array.isArray(item)) return null;
@@ -1347,6 +2155,144 @@ var spotKlineProviders = {
1347
2155
  }
1348
2156
  };
1349
2157
 
2158
+ // src/ByBit/klineStream.ts
2159
+ import { WebsocketClient } from "bybit-api";
2160
+ import { formatUnix as formatUnix3 } from "@tradejs/core/time";
2161
+ import { logger as logger4 } from "@tradejs/infra/logger";
2162
+ import { resolveTradingAccount as resolveTradingAccount2 } from "@tradejs/infra/tradingAccounts";
2163
+ var CATEGORY = "linear";
2164
+ var SUBSCRIPTION_BATCH_SIZE = 100;
2165
+ var KLINE_TOPIC_PATTERN = /^kline\.([^.]+)\.(.+)$/;
2166
+ var chunk = (items, size) => {
2167
+ const result = [];
2168
+ for (let index = 0; index < items.length; index += size) {
2169
+ result.push(items.slice(index, index + size));
2170
+ }
2171
+ return result;
2172
+ };
2173
+ var normalizeSymbols = (symbols) => [...new Set(symbols.map((symbol) => symbol.trim().toUpperCase()))].filter(
2174
+ Boolean
2175
+ );
2176
+ var buildBybitKlineTopic = (symbol, interval) => `kline.${interval}.${symbol.trim().toUpperCase()}`;
2177
+ var parseBybitKlineEvent = (value, receivedAt = Date.now()) => {
2178
+ if (!value || typeof value !== "object") return [];
2179
+ const event = value;
2180
+ const match = String(event.topic ?? "").match(KLINE_TOPIC_PATTERN);
2181
+ if (!match || !Array.isArray(event.data)) return [];
2182
+ const [, interval, symbol] = match;
2183
+ return event.data.flatMap((raw) => {
2184
+ const row = raw;
2185
+ const timestamp = Number(row.start);
2186
+ const open = Number(row.open);
2187
+ const high = Number(row.high);
2188
+ const low = Number(row.low);
2189
+ const close = Number(row.close);
2190
+ const volume = Number(row.volume);
2191
+ const turnover = Number(row.turnover);
2192
+ if (![timestamp, open, high, low, close, volume, turnover].every(
2193
+ Number.isFinite
2194
+ ) || timestamp < 0 || high < Math.max(open, close) || low > Math.min(open, close)) {
2195
+ return [];
2196
+ }
2197
+ return [
2198
+ {
2199
+ symbol: symbol.toUpperCase(),
2200
+ interval,
2201
+ candle: {
2202
+ dt: formatUnix3(timestamp),
2203
+ timestamp,
2204
+ open,
2205
+ high,
2206
+ low,
2207
+ close,
2208
+ volume,
2209
+ turnover
2210
+ },
2211
+ confirm: Boolean(row.confirm),
2212
+ receivedAt
2213
+ }
2214
+ ];
2215
+ });
2216
+ };
2217
+ var createBybitKlineStreamWithClient = ({
2218
+ client,
2219
+ onEvent
2220
+ }) => {
2221
+ let topics = /* @__PURE__ */ new Set();
2222
+ let closed = false;
2223
+ client.on("update", (value) => {
2224
+ for (const event of parseBybitKlineEvent(value)) {
2225
+ void Promise.resolve(onEvent(event)).catch((error) => {
2226
+ logger4.error("Bybit kline stream handler failed: %s", String(error));
2227
+ });
2228
+ }
2229
+ });
2230
+ client.on("reconnected", ({ wsKey }) => {
2231
+ logger4.info("Bybit kline stream reconnected: %s", wsKey);
2232
+ });
2233
+ client.on("exception", (error) => {
2234
+ logger4.error("Bybit kline stream exception: %s", String(error));
2235
+ });
2236
+ return {
2237
+ setSubscriptions: (symbols, interval) => {
2238
+ if (closed) return;
2239
+ const nextTopics = new Set(
2240
+ normalizeSymbols(symbols).map(
2241
+ (symbol) => buildBybitKlineTopic(symbol, interval)
2242
+ )
2243
+ );
2244
+ const added = [...nextTopics].filter((topic) => !topics.has(topic));
2245
+ const removed = [...topics].filter((topic) => !nextTopics.has(topic));
2246
+ for (const batch of chunk(removed, SUBSCRIPTION_BATCH_SIZE)) {
2247
+ void Promise.all(client.unsubscribeV5(batch, CATEGORY)).catch(
2248
+ (error) => logger4.error("Bybit kline unsubscribe failed: %s", String(error))
2249
+ );
2250
+ }
2251
+ for (const batch of chunk(added, SUBSCRIPTION_BATCH_SIZE)) {
2252
+ void Promise.all(client.subscribeV5(batch, CATEGORY)).catch(
2253
+ (error) => logger4.error("Bybit kline subscribe failed: %s", String(error))
2254
+ );
2255
+ }
2256
+ topics = nextTopics;
2257
+ logger4.info(
2258
+ "Bybit kline stream subscriptions: topics=%s added=%s removed=%s",
2259
+ topics.size,
2260
+ added.length,
2261
+ removed.length
2262
+ );
2263
+ },
2264
+ close: async () => {
2265
+ if (closed) return;
2266
+ closed = true;
2267
+ const currentTopics = [...topics];
2268
+ topics.clear();
2269
+ for (const batch of chunk(currentTopics, SUBSCRIPTION_BATCH_SIZE)) {
2270
+ await Promise.allSettled(client.unsubscribeV5(batch, CATEGORY));
2271
+ }
2272
+ client.closeAll();
2273
+ }
2274
+ };
2275
+ };
2276
+ var createBybitKlineStream = async ({
2277
+ config,
2278
+ onEvent
2279
+ }) => {
2280
+ const account = config.accountId ? await resolveTradingAccount2({
2281
+ userName: config.userName,
2282
+ accountId: config.accountId,
2283
+ provider: "bybit",
2284
+ universe: config.universe
2285
+ }) : null;
2286
+ const client = new WebsocketClient({
2287
+ testnet: account?.environment === "testnet",
2288
+ reconnectTimeout: Math.max(
2289
+ 500,
2290
+ Number(process.env.BYBIT_WS_RECONNECT_TIMEOUT_MS ?? 1e3)
2291
+ )
2292
+ });
2293
+ return createBybitKlineStreamWithClient({ client, onEvent });
2294
+ };
2295
+
1350
2296
  // src/marketData/providers/binanceCoinbaseSpread.ts
1351
2297
  import {
1352
2298
  alignSpreadRows,
@@ -1407,7 +2353,7 @@ var binanceCoinbaseSpreadProvider = {
1407
2353
  };
1408
2354
 
1409
2355
  // src/marketData/providers/coinalyze.ts
1410
- import { delay as delay2 } from "@tradejs/core/async";
2356
+ import { delay as delay3 } from "@tradejs/core/async";
1411
2357
  import {
1412
2358
  coinalyzePointsToRows,
1413
2359
  mergeCoinalyzeMetrics
@@ -1480,7 +2426,7 @@ var fetchCoinalyzeSeries = async (params) => {
1480
2426
  lastRequestTs + coinalyzeMinRequestDelayMs - now
1481
2427
  );
1482
2428
  if (waitMs > 0) {
1483
- await delay2(waitMs);
2429
+ await delay3(waitMs);
1484
2430
  }
1485
2431
  lastRequestTs = Date.now();
1486
2432
  const response = await fetch(url.toString(), { headers });
@@ -1494,7 +2440,7 @@ var fetchCoinalyzeSeries = async (params) => {
1494
2440
  const transient = response.status === 429 || response.status >= 500;
1495
2441
  if (attempt < coinalyzeMaxRetries && transient) {
1496
2442
  const backoffMs = Math.min(1e4, 750 * 2 ** attempt);
1497
- await delay2(retryAfterMs ?? backoffMs);
2443
+ await delay3(retryAfterMs ?? backoffMs);
1498
2444
  continue;
1499
2445
  }
1500
2446
  throw new Error(`Coinalyze ${endpoint} ${response.status}: ${text}`);
@@ -1622,12 +2568,16 @@ var index_default = { connectorEntries };
1622
2568
  export {
1623
2569
  ConnectorNames,
1624
2570
  ConnectorProviders,
2571
+ buildBybitKlineTopic,
1625
2572
  connectorEntries,
1626
2573
  connectors,
2574
+ createBybitKlineStream,
2575
+ createBybitKlineStreamWithClient,
1627
2576
  index_default as default,
1628
2577
  getConnectorCreatorByProvider,
1629
2578
  getConnectorProviders,
1630
2579
  marketDataProviders,
2580
+ parseBybitKlineEvent,
1631
2581
  providerToConnectorName,
1632
2582
  resolveConnectorNameByProvider,
1633
2583
  spotKlineProviders