@tradejs/connectors 1.0.6 → 1.0.9

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.
Files changed (3) hide show
  1. package/dist/index.js +485 -339
  2. package/dist/index.mjs +477 -331
  3. package/package.json +7 -7
package/dist/index.mjs CHANGED
@@ -4,20 +4,13 @@ import chalk from "chalk";
4
4
  import { delay } from "@tradejs/core/async";
5
5
  import {
6
6
  MARKET_CATEGORY as MARKET_CATEGORY2,
7
- PRELOAD_FALLBACK_DAYS
7
+ PRELOAD_FALLBACK_DAYS as PRELOAD_FALLBACK_DAYS2
8
8
  } from "@tradejs/core/constants";
9
- import { mergeData } from "@tradejs/core/data";
10
9
  import { toJson } from "@tradejs/core/data";
11
10
  import { round } from "@tradejs/core/math";
12
11
  import { normalizeTickerData } from "@tradejs/core/tickers";
13
- import { formatUnix as formatUnix2, getItemTimestamp, getTimestamp } from "@tradejs/core/time";
14
- import { logger as logger2 } from "@tradejs/infra/logger";
15
- import {
16
- getCandlesRange,
17
- getDataEdges,
18
- upsertCandles,
19
- toRows
20
- } from "@tradejs/infra/timescale";
12
+ import { formatUnix as formatUnix2, getTimestamp as getTimestamp2 } from "@tradejs/core/time";
13
+ import { logger as logger3 } from "@tradejs/infra/logger";
21
14
 
22
15
  // src/ByBit/client.ts
23
16
  import { RestClientV5 } from "bybit-api";
@@ -45,6 +38,242 @@ var getClient = async ({ userName }, access = "private") => {
45
38
  return client;
46
39
  };
47
40
 
41
+ // src/shared/timescaleKlineCache.ts
42
+ import { PRELOAD_FALLBACK_DAYS } from "@tradejs/core/constants";
43
+ import { mergeData } from "@tradejs/core/data";
44
+ import { getItemTimestamp, getTimestamp } from "@tradejs/core/time";
45
+ import { logger as logger2 } from "@tradejs/infra/logger";
46
+ import {
47
+ getCandlesRange,
48
+ getDataEdges,
49
+ toRows,
50
+ upsertCandles
51
+ } from "@tradejs/infra/timescale";
52
+ var DEFAULT_LIMIT = 1e3;
53
+ var DEFAULT_CACHE_FALLBACK_WINDOW = 1e3;
54
+ var intervalMsOf = (interval) => interval * 6e4;
55
+ var clampToClosedCandle = (value, intervalMs) => Math.floor(value / intervalMs) * intervalMs;
56
+ var normalizeRangeToClosed = (intervalMs, start, end) => {
57
+ const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
58
+ const normStart = start !== void 0 ? clampToClosedCandle(start, intervalMs) : 0;
59
+ const cappedEnd = Math.min(end ?? Date.now(), lastClosed);
60
+ const normEnd = clampToClosedCandle(cappedEnd, intervalMs);
61
+ return { normStart, normEnd };
62
+ };
63
+ var rowsToKline = (rows) => rows.map(({ ts, ...data }) => ({
64
+ timestamp: new Date(ts).getTime(),
65
+ ...data
66
+ }));
67
+ var createTimescaleCachedKline = ({
68
+ provider,
69
+ request,
70
+ intervalToMinutes: intervalToMinutes3,
71
+ limit = DEFAULT_LIMIT,
72
+ cacheFallbackWindow = DEFAULT_CACHE_FALLBACK_WINDOW
73
+ }) => {
74
+ let isTimescaleFallbackMode = false;
75
+ const loadData = async (direction, pointer, limitBoundary, requestParams, intervalMs) => {
76
+ if (pointer === void 0) return [];
77
+ let accumulated = [];
78
+ let fulfilled = false;
79
+ while (!fulfilled) {
80
+ const currentPointer = pointer;
81
+ const params = {
82
+ symbol: requestParams.symbol,
83
+ interval: requestParams.interval,
84
+ silent: requestParams.silent,
85
+ end: requestParams.end
86
+ };
87
+ if (direction === "older") {
88
+ params.end = pointer;
89
+ if (limitBoundary !== void 0) params.start = limitBoundary;
90
+ } else {
91
+ params.start = pointer;
92
+ if (limitBoundary !== void 0) params.end = limitBoundary;
93
+ }
94
+ const partData = await request(params);
95
+ if (!partData.length) {
96
+ fulfilled = true;
97
+ break;
98
+ }
99
+ accumulated = direction === "older" ? mergeData(partData, accumulated) : mergeData(accumulated, partData);
100
+ const boundaryReached = limitBoundary !== void 0 && (direction === "older" && currentPointer <= limitBoundary || direction === "newer" && currentPointer >= limitBoundary);
101
+ if (partData.length < limit || boundaryReached) {
102
+ fulfilled = true;
103
+ break;
104
+ }
105
+ const nextPointer = direction === "older" ? getItemTimestamp(partData[0]) - intervalMs : getItemTimestamp(partData[partData.length - 1]) + intervalMs;
106
+ if (!Number.isFinite(nextPointer) || nextPointer === currentPointer) {
107
+ fulfilled = true;
108
+ break;
109
+ }
110
+ pointer = nextPointer;
111
+ }
112
+ return accumulated;
113
+ };
114
+ const refreshTail = async ({
115
+ symbol,
116
+ interval,
117
+ silent,
118
+ tailCount = 2
119
+ }) => {
120
+ const intMinutes = intervalToMinutes3(interval);
121
+ if (!intMinutes) {
122
+ logger2.log("error", "refreshTail: invalid interval %s", interval);
123
+ return;
124
+ }
125
+ const intervalMs = intervalMsOf(intMinutes);
126
+ const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
127
+ const tailEnd = lastClosed + intervalMs;
128
+ const tailStart = tailEnd - tailCount * intervalMs;
129
+ const part = await request({
130
+ symbol,
131
+ interval,
132
+ start: tailStart,
133
+ end: tailEnd,
134
+ silent
135
+ });
136
+ if (part.length) {
137
+ await upsertCandles(toRows(provider, symbol, intMinutes, part));
138
+ }
139
+ };
140
+ return async ({
141
+ symbol,
142
+ interval,
143
+ start: defaultStart,
144
+ end: defaultEnd,
145
+ silent = false,
146
+ cacheOnly = false,
147
+ warmOnly = false
148
+ }) => {
149
+ const intMinutes = intervalToMinutes3(interval);
150
+ if (!intMinutes) {
151
+ logger2.log("error", "kline: invalid interval %s", interval);
152
+ return [];
153
+ }
154
+ if (defaultStart !== void 0 && defaultEnd !== void 0 && defaultEnd <= defaultStart) {
155
+ return [];
156
+ }
157
+ const intervalMs = intervalMsOf(intMinutes);
158
+ try {
159
+ const edges = await getDataEdges(provider, symbol, intMinutes);
160
+ let dataStart = edges.min;
161
+ let dataEnd = edges.max;
162
+ const { normStart, normEnd } = normalizeRangeToClosed(
163
+ intervalMs,
164
+ defaultStart,
165
+ defaultEnd
166
+ );
167
+ if (cacheOnly) {
168
+ const base = edges.max ?? Date.now();
169
+ const s = Math.max(
170
+ defaultStart ?? base - cacheFallbackWindow * intervalMs,
171
+ 0
172
+ );
173
+ const e = defaultEnd ?? base;
174
+ const dbData2 = await getCandlesRange(
175
+ provider,
176
+ symbol,
177
+ intMinutes,
178
+ s,
179
+ e
180
+ );
181
+ return rowsToKline(dbData2);
182
+ }
183
+ const needOlderData = defaultStart !== void 0 && (dataStart === void 0 || normStart < dataStart);
184
+ const needNewerData = defaultEnd !== void 0 && (dataEnd === void 0 || normEnd > dataEnd);
185
+ if (needOlderData) {
186
+ const pointerForOlder = dataStart ?? normEnd ?? Date.now();
187
+ const olderData = await loadData(
188
+ "older",
189
+ pointerForOlder,
190
+ normStart,
191
+ {
192
+ symbol,
193
+ interval,
194
+ silent,
195
+ start: normStart,
196
+ end: pointerForOlder
197
+ },
198
+ intervalMs
199
+ );
200
+ if (olderData.length) {
201
+ await upsertCandles(toRows(provider, symbol, intMinutes, olderData));
202
+ dataStart = normStart;
203
+ }
204
+ }
205
+ if (needNewerData) {
206
+ const fallbackStart = getTimestamp(PRELOAD_FALLBACK_DAYS);
207
+ const pointerForNewer = dataEnd ?? (defaultStart !== void 0 ? normStart : fallbackStart) ?? 0;
208
+ const newerData = await loadData(
209
+ "newer",
210
+ pointerForNewer,
211
+ normEnd,
212
+ {
213
+ symbol,
214
+ interval,
215
+ silent,
216
+ start: pointerForNewer,
217
+ end: normEnd
218
+ },
219
+ intervalMs
220
+ );
221
+ if (newerData.length) {
222
+ await upsertCandles(toRows(provider, symbol, intMinutes, newerData));
223
+ dataEnd = normEnd;
224
+ }
225
+ }
226
+ const isRightEdgeQuery = defaultEnd === void 0 || defaultEnd && defaultEnd >= Date.now() - intervalMs;
227
+ if (!cacheOnly && isRightEdgeQuery) {
228
+ await refreshTail({ symbol, interval, silent });
229
+ }
230
+ if (warmOnly) {
231
+ if (isTimescaleFallbackMode) {
232
+ isTimescaleFallbackMode = false;
233
+ logger2.log("info", "TimescaleDB connection restored for kline cache");
234
+ }
235
+ return [];
236
+ }
237
+ const rangeStart = defaultStart ?? dataStart ?? 0;
238
+ const rangeEnd = defaultEnd ?? dataEnd ?? Date.now();
239
+ const { normStart: finalStart, normEnd: finalEnd } = normalizeRangeToClosed(intervalMs, rangeStart, rangeEnd);
240
+ const dbData = await getCandlesRange(
241
+ provider,
242
+ symbol,
243
+ intMinutes,
244
+ finalStart,
245
+ finalEnd
246
+ );
247
+ if (isTimescaleFallbackMode) {
248
+ isTimescaleFallbackMode = false;
249
+ logger2.log("info", "TimescaleDB connection restored for kline cache");
250
+ }
251
+ return rowsToKline(dbData);
252
+ } catch (error) {
253
+ if (!isTimescaleFallbackMode) {
254
+ isTimescaleFallbackMode = true;
255
+ logger2.log(
256
+ "warn",
257
+ "TimescaleDB unavailable for %s %s: %s. Falling back to exchange API.",
258
+ symbol,
259
+ interval,
260
+ String(error)
261
+ );
262
+ }
263
+ if (cacheOnly || warmOnly) {
264
+ return [];
265
+ }
266
+ return request({
267
+ symbol,
268
+ interval,
269
+ start: defaultStart,
270
+ end: defaultEnd,
271
+ silent
272
+ });
273
+ }
274
+ };
275
+ };
276
+
48
277
  // src/ByBit/utils.ts
49
278
  import { MARKET_CATEGORY } from "@tradejs/core/constants";
50
279
  import { formatUnix } from "@tradejs/core/time";
@@ -138,7 +367,6 @@ var mapPositionData = (data) => {
138
367
 
139
368
  // src/ByBit/index.ts
140
369
  var LIMIT = 1e3;
141
- var CACHE_FALLBACK_WINDOW = 1e3;
142
370
  var BYBIT_RATE_LIMIT_RETCODE = 10006;
143
371
  var BYBIT_TRADING_STOP_NOT_MODIFIED_RETCODE = 34040;
144
372
  var KLINE_RATE_LIMIT_MAX_ATTEMPTS = 3;
@@ -181,7 +409,6 @@ var resolveKlineRetryDelayMs = (res, attempt) => {
181
409
  };
182
410
  var ByBitConnectorCreator = async (config) => {
183
411
  let state = {};
184
- let isTimescaleFallbackMode = false;
185
412
  let publicClientPromise = null;
186
413
  let privateClientPromise = null;
187
414
  const getPublicClient = async () => {
@@ -200,7 +427,7 @@ var ByBitConnectorCreator = async (config) => {
200
427
  silent
201
428
  }) => {
202
429
  const normalizedStart = round(
203
- start || getTimestamp(PRELOAD_FALLBACK_DAYS),
430
+ start || getTimestamp2(PRELOAD_FALLBACK_DAYS2),
204
431
  0
205
432
  );
206
433
  const normalizedEnd = round(end || Date.now());
@@ -222,7 +449,7 @@ var ByBitConnectorCreator = async (config) => {
222
449
  if (isKlineRateLimited(kline)) {
223
450
  if (attempt < KLINE_RATE_LIMIT_MAX_ATTEMPTS) {
224
451
  const waitMs = resolveKlineRetryDelayMs(kline, attempt);
225
- logger2.log(
452
+ logger3.log(
226
453
  "warn",
227
454
  "kline rate limited for %s %s: attempt=%s/%s waitMs=%s",
228
455
  symbol,
@@ -237,7 +464,7 @@ var ByBitConnectorCreator = async (config) => {
237
464
  }
238
465
  if (!kline?.result?.list) {
239
466
  const responseError = typeof kline?.retMsg === "string" && kline.retMsg !== "OK" ? `${kline.retMsg}${typeof kline?.retCode === "number" ? ` (retCode: ${kline.retCode})` : ""}` : typeof kline?.retCode === "number" && kline.retCode !== 0 ? `retCode: ${kline.retCode}` : "";
240
- logger2.log(
467
+ logger3.log(
241
468
  "error",
242
469
  "empty kline.list for %s %s%s",
243
470
  symbol,
@@ -246,8 +473,8 @@ var ByBitConnectorCreator = async (config) => {
246
473
  );
247
474
  return [];
248
475
  }
249
- if (!silent) {
250
- logger2.log(
476
+ if (!silent && (process.stdout.isTTY || process.env.TRADEJS_LOG_KLINE_REQUESTS === "1")) {
477
+ logger3.log(
251
478
  "info",
252
479
  "%s %s %s %s",
253
480
  chalk.yellow(formatUnix2(normalizedEnd)),
@@ -260,7 +487,7 @@ var ByBitConnectorCreator = async (config) => {
260
487
  }
261
488
  return [];
262
489
  } catch (error) {
263
- logger2.log(
490
+ logger3.log(
264
491
  "error",
265
492
  "request kline: %s %s %s",
266
493
  normalizedStart,
@@ -270,86 +497,9 @@ var ByBitConnectorCreator = async (config) => {
270
497
  return [];
271
498
  }
272
499
  };
273
- const loadData = async (direction, pointer, limitBoundary, requestParams, intervalMs) => {
274
- if (pointer === void 0) return [];
275
- let accumulated = [];
276
- let fulfilled = false;
277
- while (!fulfilled) {
278
- const currentPointer = pointer;
279
- const params = {
280
- symbol: requestParams.symbol,
281
- interval: requestParams.interval,
282
- silent: requestParams.silent
283
- };
284
- if (direction === "older") {
285
- params.end = pointer;
286
- if (limitBoundary !== void 0) params.start = limitBoundary;
287
- } else {
288
- params.start = pointer;
289
- if (limitBoundary !== void 0) params.end = limitBoundary;
290
- }
291
- const partData = await request(params);
292
- if (_.isEmpty(partData)) {
293
- fulfilled = true;
294
- break;
295
- }
296
- accumulated = direction === "older" ? mergeData(partData, accumulated) : mergeData(accumulated, partData);
297
- const boundaryReached = limitBoundary !== void 0 && (direction === "older" && currentPointer <= limitBoundary || direction === "newer" && currentPointer >= limitBoundary);
298
- if (partData.length < LIMIT || boundaryReached) {
299
- fulfilled = true;
300
- break;
301
- }
302
- const nextPointer = direction === "older" ? getItemTimestamp(partData[0]) - intervalMs : getItemTimestamp(partData[partData.length - 1]) + intervalMs;
303
- if (!Number.isFinite(nextPointer) || nextPointer === currentPointer) {
304
- fulfilled = true;
305
- break;
306
- }
307
- pointer = nextPointer;
308
- }
309
- return accumulated;
310
- };
311
- const intervalMsOf = (interval) => interval * 6e4;
312
- const intervalToMinutes = (interval) => {
500
+ const intervalToMinutes3 = (interval) => {
313
501
  return INTERVAL_TO_MINUTES[String(interval)] ?? null;
314
502
  };
315
- const clampToClosedCandle = (value, intervalMs) => Math.floor(value / intervalMs) * intervalMs;
316
- const normalizeRangeToClosed = (intervalMs, start, end) => {
317
- const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
318
- const normStart = start !== void 0 ? clampToClosedCandle(start, intervalMs) : 0;
319
- const cappedEnd = Math.min(end ?? Date.now(), lastClosed);
320
- const normEnd = clampToClosedCandle(cappedEnd, intervalMs);
321
- return { normStart, normEnd, lastClosed };
322
- };
323
- const rowsToKline = (rows) => rows.map(({ ts, ...data }) => ({
324
- timestamp: new Date(ts).getTime(),
325
- ...data
326
- }));
327
- const refreshTail = async ({
328
- symbol,
329
- interval,
330
- silent,
331
- tailCount = 2
332
- }) => {
333
- const intMinutes = intervalToMinutes(interval);
334
- if (!intMinutes) {
335
- logger2.log("error", "refreshTail: invalid interval %s", interval);
336
- return;
337
- }
338
- const intervalMs = intervalMsOf(intMinutes);
339
- const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
340
- const tailEnd = lastClosed + intervalMs;
341
- const tailStart = tailEnd - tailCount * intervalMs;
342
- const part = await request({
343
- symbol,
344
- interval,
345
- start: tailStart,
346
- end: tailEnd,
347
- silent
348
- });
349
- if (part.length) {
350
- await upsertCandles(toRows(symbol, intMinutes, part));
351
- }
352
- };
353
503
  const getPositionSnapshot = async (symbol) => {
354
504
  const client = await getPrivateClient();
355
505
  if (!client) {
@@ -360,7 +510,7 @@ var ByBitConnectorCreator = async (config) => {
360
510
  category: MARKET_CATEGORY2
361
511
  });
362
512
  if (positionRes.retCode !== 0) {
363
- logger2.log(
513
+ logger3.log(
364
514
  getLogLevel(positionRes),
365
515
  "position retCode: %s, %s",
366
516
  symbol,
@@ -373,7 +523,7 @@ var ByBitConnectorCreator = async (config) => {
373
523
  return null;
374
524
  }
375
525
  const position = positions[0];
376
- logger2.log(
526
+ logger3.log(
377
527
  "debug",
378
528
  "position: %s %s qty=%s price=%s",
379
529
  position.symbol,
@@ -402,7 +552,7 @@ var ByBitConnectorCreator = async (config) => {
402
552
  const meta = await getSymbolMeta(marketDataClient, symbol);
403
553
  const positionQty = qty ?? (await getPositionSnapshot(symbol))?.qty ?? 0;
404
554
  if (!Number.isFinite(positionQty) || positionQty <= 0) {
405
- logger2.log(
555
+ logger3.log(
406
556
  "warn",
407
557
  "setTakeProfits: missing position qty: %s",
408
558
  toJson({ symbol, qty, positionQty, takeProfits }, true)
@@ -417,7 +567,7 @@ var ByBitConnectorCreator = async (config) => {
417
567
  meta
418
568
  );
419
569
  if (!tpSizeNum || tpSizeNum < meta.minOrderQty) {
420
- logger2.log(
570
+ logger3.log(
421
571
  "warn",
422
572
  "tp skipped: size too small %s",
423
573
  toJson(
@@ -444,7 +594,7 @@ var ByBitConnectorCreator = async (config) => {
444
594
  positionIdx: 0
445
595
  });
446
596
  if (isTradingStopNotModified(tpRes)) {
447
- logger2.log(
597
+ logger3.log(
448
598
  "debug",
449
599
  "tp unchanged: %s %s price=%s rate=%s",
450
600
  symbol,
@@ -453,7 +603,7 @@ var ByBitConnectorCreator = async (config) => {
453
603
  tp.rate
454
604
  );
455
605
  } else if (tpRes.retCode === 0) {
456
- logger2.log(
606
+ logger3.log(
457
607
  "info",
458
608
  "tp updated: %s %s price=%s rate=%s",
459
609
  symbol,
@@ -462,7 +612,7 @@ var ByBitConnectorCreator = async (config) => {
462
612
  tp.rate
463
613
  );
464
614
  } else {
465
- logger2.log(
615
+ logger3.log(
466
616
  "error",
467
617
  "tp failed: %s %s price=%s rate=%s %s",
468
618
  symbol,
@@ -507,7 +657,7 @@ var ByBitConnectorCreator = async (config) => {
507
657
  positionIdx: 0
508
658
  });
509
659
  if (isTradingStopNotModified(slRes)) {
510
- logger2.log(
660
+ logger3.log(
511
661
  "debug",
512
662
  "sl unchanged: %s %s stopLoss=%s",
513
663
  symbol,
@@ -515,7 +665,7 @@ var ByBitConnectorCreator = async (config) => {
515
665
  slNormalized.priceStr
516
666
  );
517
667
  } else if (slRes.retCode === 0) {
518
- logger2.log(
668
+ logger3.log(
519
669
  "info",
520
670
  "sl updated: %s %s stopLoss=%s",
521
671
  symbol,
@@ -523,7 +673,7 @@ var ByBitConnectorCreator = async (config) => {
523
673
  slNormalized.priceStr
524
674
  );
525
675
  } else {
526
- logger2.log(
676
+ logger3.log(
527
677
  "error",
528
678
  "sl failed: %s %s stopLoss=%s %s",
529
679
  symbol,
@@ -539,136 +689,12 @@ var ByBitConnectorCreator = async (config) => {
539
689
  setState: async (newState) => {
540
690
  state = { ...state, ...newState };
541
691
  },
542
- kline: async ({
543
- symbol,
544
- interval,
545
- start: defaultStart,
546
- end: defaultEnd,
547
- silent = false,
548
- cacheOnly = false,
549
- warmOnly = false
550
- }) => {
551
- const intMinutes = intervalToMinutes(interval);
552
- if (!intMinutes) {
553
- logger2.log("error", "kline: invalid interval %s", interval);
554
- return [];
555
- }
556
- if (defaultStart !== void 0 && defaultEnd !== void 0 && defaultEnd <= defaultStart) {
557
- return [];
558
- }
559
- const intervalMs = intervalMsOf(intMinutes);
560
- try {
561
- const edges = await getDataEdges(symbol, intMinutes);
562
- let dataStart = edges.min;
563
- let dataEnd = edges.max;
564
- const { normStart, normEnd } = normalizeRangeToClosed(
565
- intervalMs,
566
- defaultStart,
567
- defaultEnd
568
- );
569
- if (cacheOnly) {
570
- const base = edges.max ?? Date.now();
571
- const s = Math.max(
572
- defaultStart ?? base - CACHE_FALLBACK_WINDOW * intervalMs,
573
- 0
574
- );
575
- const e = defaultEnd ?? base;
576
- const dbData2 = await getCandlesRange(symbol, intMinutes, s, e);
577
- return rowsToKline(dbData2);
578
- }
579
- const needOlderData = defaultStart !== void 0 && (dataStart === void 0 || normStart < dataStart);
580
- const needNewerData = defaultEnd !== void 0 && (dataEnd === void 0 || normEnd > dataEnd);
581
- if (needOlderData) {
582
- const pointerForOlder = dataStart ?? normEnd ?? Date.now();
583
- const olderData = await loadData(
584
- "older",
585
- pointerForOlder,
586
- normStart,
587
- {
588
- symbol,
589
- interval,
590
- silent,
591
- start: normStart,
592
- end: pointerForOlder
593
- },
594
- intervalMs
595
- );
596
- if (olderData.length) {
597
- await upsertCandles(toRows(symbol, intMinutes, olderData));
598
- dataStart = normStart;
599
- }
600
- }
601
- if (needNewerData) {
602
- const pointerForNewer = dataEnd ?? normStart ?? 0;
603
- const newerData = await loadData(
604
- "newer",
605
- pointerForNewer,
606
- normEnd,
607
- {
608
- symbol,
609
- interval,
610
- silent,
611
- start: pointerForNewer,
612
- end: normEnd
613
- },
614
- intervalMs
615
- );
616
- if (newerData.length) {
617
- await upsertCandles(toRows(symbol, intMinutes, newerData));
618
- dataEnd = normEnd;
619
- }
620
- }
621
- const isRightEdgeQuery = defaultEnd === void 0 || defaultEnd && defaultEnd >= Date.now() - intervalMs;
622
- if (!cacheOnly && isRightEdgeQuery) {
623
- await refreshTail({ symbol, interval, silent });
624
- }
625
- if (warmOnly) {
626
- if (isTimescaleFallbackMode) {
627
- isTimescaleFallbackMode = false;
628
- logger2.log(
629
- "info",
630
- "TimescaleDB connection restored for kline cache"
631
- );
632
- }
633
- return [];
634
- }
635
- const rangeStart = defaultStart ?? dataStart ?? 0;
636
- const rangeEnd = defaultEnd ?? dataEnd ?? Date.now();
637
- const { normStart: finalStart, normEnd: finalEnd } = normalizeRangeToClosed(intervalMs, rangeStart, rangeEnd);
638
- const dbData = await getCandlesRange(
639
- symbol,
640
- intMinutes,
641
- finalStart,
642
- finalEnd
643
- );
644
- if (isTimescaleFallbackMode) {
645
- isTimescaleFallbackMode = false;
646
- logger2.log("info", "TimescaleDB connection restored for kline cache");
647
- }
648
- return rowsToKline(dbData);
649
- } catch (error) {
650
- if (!isTimescaleFallbackMode) {
651
- isTimescaleFallbackMode = true;
652
- logger2.log(
653
- "warn",
654
- "TimescaleDB unavailable for %s %s: %s. Falling back to exchange API.",
655
- symbol,
656
- interval,
657
- String(error)
658
- );
659
- }
660
- if (cacheOnly || warmOnly) {
661
- return [];
662
- }
663
- return request({
664
- symbol,
665
- interval,
666
- start: defaultStart,
667
- end: defaultEnd,
668
- silent
669
- });
670
- }
671
- },
692
+ kline: createTimescaleCachedKline({
693
+ provider: "bybit",
694
+ request,
695
+ intervalToMinutes: intervalToMinutes3,
696
+ limit: LIMIT
697
+ }),
672
698
  getPosition: async (symbol) => getPositionSnapshot(symbol),
673
699
  getPositions: async () => {
674
700
  const client = await getPrivateClient();
@@ -680,7 +706,7 @@ var ByBitConnectorCreator = async (config) => {
680
706
  settleCoin: "USDT"
681
707
  });
682
708
  if (positionRes.retCode !== 0) {
683
- logger2.log(
709
+ logger3.log(
684
710
  getLogLevel(positionRes),
685
711
  "positions retCode: %s, %s",
686
712
  positionRes.retCode
@@ -693,7 +719,94 @@ var ByBitConnectorCreator = async (config) => {
693
719
  }
694
720
  return positions;
695
721
  },
696
- placeOrder: async ({ symbol, price, qty, direction, isLimit }) => {
722
+ getOpenPositionPnl: async () => {
723
+ const client = await getPrivateClient();
724
+ if (!client) {
725
+ return [];
726
+ }
727
+ const positionRes = await client.getPositionInfo({
728
+ category: MARKET_CATEGORY2,
729
+ settleCoin: "USDT"
730
+ });
731
+ if (positionRes.retCode !== 0) {
732
+ logger3.log(
733
+ getLogLevel(positionRes),
734
+ "positions pnl retCode: %s, %s",
735
+ positionRes.retCode
736
+ );
737
+ return [];
738
+ }
739
+ return (positionRes.result?.list ?? []).map((item) => {
740
+ const qty = Number(item.size ?? Number.NaN);
741
+ const entryPrice = Number(item.avgPrice ?? Number.NaN);
742
+ const currentPrice = Number(item.markPrice ?? Number.NaN);
743
+ const unrealizedPnl = Number(item.unrealisedPnl ?? Number.NaN);
744
+ const side = String(item.side ?? "");
745
+ if (!Number.isFinite(qty) || qty <= 0 || !Number.isFinite(entryPrice) || !Number.isFinite(currentPrice) || !Number.isFinite(unrealizedPnl) || side !== "Buy" && side !== "Sell") {
746
+ return null;
747
+ }
748
+ return {
749
+ symbol: String(item.symbol ?? ""),
750
+ qty,
751
+ price: entryPrice,
752
+ currentPrice,
753
+ unrealizedPnl,
754
+ direction: side === "Buy" ? "LONG" : "SHORT"
755
+ };
756
+ }).filter(
757
+ (item) => item != null && item.symbol.length > 0
758
+ );
759
+ },
760
+ getClosedPnl: async ({
761
+ startTime,
762
+ endTime,
763
+ symbol,
764
+ limit = 100
765
+ }) => {
766
+ const client = await getPrivateClient();
767
+ if (!client) {
768
+ return [];
769
+ }
770
+ const response = await client.getClosedPnL({
771
+ category: MARKET_CATEGORY2,
772
+ startTime,
773
+ endTime,
774
+ symbol,
775
+ limit: Math.min(Math.max(1, Math.trunc(limit)), 100)
776
+ });
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) => {
787
+ const qty = Number(item.qty ?? item.closedSize ?? Number.NaN);
788
+ const entryPrice = Number(item.avgEntryPrice ?? Number.NaN);
789
+ const exitPrice = Number(item.avgExitPrice ?? Number.NaN);
790
+ const closedPnl = Number(item.closedPnl ?? Number.NaN);
791
+ const closedAt = Number(
792
+ item.updatedTime ?? item.createdTime ?? Number.NaN
793
+ );
794
+ const orderId = typeof item.orderId === "string" && item.orderId.trim() ? item.orderId : null;
795
+ if (!String(item.symbol ?? "").trim() || !Number.isFinite(qty) || !Number.isFinite(closedPnl) || !Number.isFinite(closedAt)) {
796
+ return null;
797
+ }
798
+ return {
799
+ symbol: String(item.symbol),
800
+ qty,
801
+ entryPrice: Number.isFinite(entryPrice) ? entryPrice : null,
802
+ exitPrice: Number.isFinite(exitPrice) ? exitPrice : null,
803
+ closedPnl,
804
+ closedAt,
805
+ ...orderId ? { orderId } : {}
806
+ };
807
+ }).filter((item) => item != null);
808
+ },
809
+ placeOrder: async ({ symbol, price, qty, direction, isLimit, orderId }) => {
697
810
  const client = await getPrivateClient();
698
811
  const marketDataClient = await getPublicClient();
699
812
  if (!client || !marketDataClient) {
@@ -703,7 +816,7 @@ var ByBitConnectorCreator = async (config) => {
703
816
  const meta = await getSymbolMeta(marketDataClient, symbol);
704
817
  const { qtyNum: orderQty, qtyStr: orderQtyStr } = normalizeQty(qty, meta);
705
818
  if (orderQty < meta.minOrderQty) {
706
- logger2.log(
819
+ logger3.log(
707
820
  "warn",
708
821
  "placeOrder: qty too small: %s",
709
822
  toJson(
@@ -714,7 +827,7 @@ var ByBitConnectorCreator = async (config) => {
714
827
  return false;
715
828
  }
716
829
  const entryNormalized = isLimit ? normalizePrice(price, "ENTRY", meta) : void 0;
717
- logger2.log(
830
+ logger3.log(
718
831
  "info",
719
832
  "placeOrder: %s",
720
833
  toJson(
@@ -742,9 +855,10 @@ var ByBitConnectorCreator = async (config) => {
742
855
  side: isLong ? "Buy" : "Sell",
743
856
  orderType: isLimit ? "Limit" : "Market",
744
857
  qty: orderQtyStr,
745
- orderFilter: "Order"
858
+ orderFilter: "Order",
859
+ orderLinkId: typeof orderId === "string" && orderId.trim() ? orderId : void 0
746
860
  });
747
- logger2.log(
861
+ logger3.log(
748
862
  getLogLevel(orderRes),
749
863
  "placeOrder:response: %s",
750
864
  toJson(orderRes, true)
@@ -769,7 +883,7 @@ var ByBitConnectorCreator = async (config) => {
769
883
  qty: "0",
770
884
  reduceOnly: true
771
885
  });
772
- logger2.log(
886
+ logger3.log(
773
887
  getLogLevel(closeRes),
774
888
  "closePosition: %s, %s, %s",
775
889
  symbol,
@@ -830,58 +944,74 @@ var toNum = (value, fallback = 0) => {
830
944
  const num = Number(value);
831
945
  return Number.isFinite(num) ? num : fallback;
832
946
  };
947
+ var intervalToMinutes = (interval) => {
948
+ const intervalMs = INTERVAL_MS[String(interval)];
949
+ return intervalMs ? Math.floor(intervalMs / 6e4) : null;
950
+ };
833
951
  var BinanceConnectorCreator = async () => {
834
952
  let state = {};
953
+ const requestKline = async ({
954
+ symbol,
955
+ interval,
956
+ start,
957
+ end
958
+ }) => {
959
+ const intervalToken = INTERVAL_MAP[String(interval)];
960
+ if (!intervalToken) return [];
961
+ const intervalMs = INTERVAL_MS[String(interval)] ?? 9e5;
962
+ const normalizedEnd = end ?? Date.now();
963
+ const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
964
+ let cursor = start ?? Math.max(0, normalizedEnd - intervalMs * 1e3);
965
+ const rows = [];
966
+ while (cursor <= normalizedEnd) {
967
+ const url = new URL(`${baseUrl}/api/v3/klines`);
968
+ url.searchParams.set("symbol", symbol);
969
+ url.searchParams.set("interval", intervalToken);
970
+ url.searchParams.set("startTime", String(cursor));
971
+ url.searchParams.set("endTime", String(normalizedEnd));
972
+ url.searchParams.set("limit", "1000");
973
+ const response = await fetchWithRetry(url.toString(), {
974
+ headers: { "User-Agent": "tradejs/binance-connector" }
975
+ });
976
+ if (!response.ok) break;
977
+ const payload = await response.json();
978
+ if (!Array.isArray(payload) || !payload.length) break;
979
+ let lastTs = cursor;
980
+ for (const item of payload) {
981
+ if (!Array.isArray(item)) continue;
982
+ const ts = toNum(item[0], 0);
983
+ if (!ts) continue;
984
+ rows.push({
985
+ timestamp: ts,
986
+ open: toNum(item[1]),
987
+ high: toNum(item[2]),
988
+ low: toNum(item[3]),
989
+ close: toNum(item[4]),
990
+ volume: toNum(item[5]),
991
+ turnover: toNum(item[7]),
992
+ dt: new Date(ts).toISOString()
993
+ });
994
+ lastTs = ts;
995
+ }
996
+ if (payload.length < 1e3) break;
997
+ cursor = lastTs + intervalMs;
998
+ }
999
+ rows.sort((a, b) => a.timestamp - b.timestamp);
1000
+ return rows;
1001
+ };
835
1002
  return {
836
1003
  getState: async () => state,
837
1004
  setState: async (newState) => {
838
1005
  state = { ...state, ...newState };
839
1006
  },
840
- kline: async ({ symbol, interval, start, end }) => {
841
- const intervalToken = INTERVAL_MAP[String(interval)];
842
- if (!intervalToken) return [];
843
- const intervalMs = INTERVAL_MS[String(interval)] ?? 9e5;
844
- const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
845
- let cursor = start ?? Math.max(0, end - intervalMs * 1e3);
846
- const rows = [];
847
- while (cursor <= end) {
848
- const url = new URL(`${baseUrl}/api/v3/klines`);
849
- url.searchParams.set("symbol", symbol);
850
- url.searchParams.set("interval", intervalToken);
851
- url.searchParams.set("startTime", String(cursor));
852
- url.searchParams.set("endTime", String(end));
853
- url.searchParams.set("limit", "1000");
854
- const response = await fetchWithRetry(url.toString(), {
855
- headers: { "User-Agent": "tradejs/binance-connector" }
856
- });
857
- if (!response.ok) break;
858
- const payload = await response.json();
859
- if (!Array.isArray(payload) || !payload.length) break;
860
- let lastTs = cursor;
861
- for (const item of payload) {
862
- if (!Array.isArray(item)) continue;
863
- const ts = toNum(item[0], 0);
864
- if (!ts) continue;
865
- rows.push({
866
- timestamp: ts,
867
- open: toNum(item[1]),
868
- high: toNum(item[2]),
869
- low: toNum(item[3]),
870
- close: toNum(item[4]),
871
- volume: toNum(item[5]),
872
- turnover: toNum(item[7]),
873
- dt: new Date(ts).toISOString()
874
- });
875
- lastTs = ts;
876
- }
877
- if (payload.length < 1e3) break;
878
- cursor = lastTs + intervalMs;
879
- }
880
- rows.sort((a, b) => a.timestamp - b.timestamp);
881
- return rows;
882
- },
1007
+ kline: createTimescaleCachedKline({
1008
+ provider: "binance",
1009
+ request: requestKline,
1010
+ intervalToMinutes
1011
+ }),
883
1012
  getPosition: async () => null,
884
1013
  getPositions: async () => [],
1014
+ getOpenPositionPnl: async () => [],
885
1015
  placeOrder: async () => false,
886
1016
  setTakeProfits: async () => false,
887
1017
  setStopLoss: async () => false,
@@ -965,6 +1095,10 @@ var toNum2 = (value, fallback = 0) => {
965
1095
  const num = Number(value);
966
1096
  return Number.isFinite(num) ? num : fallback;
967
1097
  };
1098
+ var intervalToMinutes2 = (interval) => {
1099
+ const intervalMs = INTERVAL_MS2[String(interval)];
1100
+ return intervalMs ? Math.floor(intervalMs / 6e4) : null;
1101
+ };
968
1102
  var toCoinbaseProduct = (symbol) => {
969
1103
  const upper = symbol.toUpperCase();
970
1104
  const quoteSuffixes = ["USDT", "USDC", "BUSD", "USD"];
@@ -991,58 +1125,70 @@ var MAJOR_PRODUCTS = [
991
1125
  ];
992
1126
  var CoinbaseConnectorCreator = async () => {
993
1127
  let state = {};
1128
+ const requestKline = async ({
1129
+ symbol,
1130
+ interval,
1131
+ start,
1132
+ end
1133
+ }) => {
1134
+ const granularity = GRANULARITY_MAP[String(interval)];
1135
+ if (!granularity) return [];
1136
+ const product = toCoinbaseProduct(symbol);
1137
+ if (!product) return [];
1138
+ const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
1139
+ const normalizedEnd = end ?? Date.now();
1140
+ const stepMs = granularity * 1e3 * 300;
1141
+ const fromMs = start ?? Math.max(0, normalizedEnd - INTERVAL_MS2[String(interval)] * 1e3);
1142
+ let cursorEnd = normalizedEnd;
1143
+ const rows = [];
1144
+ while (cursorEnd >= fromMs) {
1145
+ const chunkStart = Math.max(fromMs, cursorEnd - stepMs);
1146
+ const url = new URL(`${baseUrl}/products/${product}/candles`);
1147
+ url.searchParams.set("granularity", String(granularity));
1148
+ url.searchParams.set("start", new Date(chunkStart).toISOString());
1149
+ url.searchParams.set("end", new Date(cursorEnd).toISOString());
1150
+ const response = await fetchWithRetry2(url.toString(), {
1151
+ headers: { "User-Agent": "tradejs/coinbase-connector" }
1152
+ });
1153
+ if (!response.ok) break;
1154
+ const payload = await response.json();
1155
+ if (Array.isArray(payload)) {
1156
+ for (const item of payload) {
1157
+ if (!Array.isArray(item)) continue;
1158
+ const ts = toNum2(item[0]) * 1e3;
1159
+ rows.push({
1160
+ timestamp: ts,
1161
+ low: toNum2(item[1]),
1162
+ high: toNum2(item[2]),
1163
+ open: toNum2(item[3]),
1164
+ close: toNum2(item[4]),
1165
+ volume: toNum2(item[5]),
1166
+ turnover: 0,
1167
+ dt: new Date(ts).toISOString()
1168
+ });
1169
+ }
1170
+ }
1171
+ cursorEnd = chunkStart - 1;
1172
+ }
1173
+ const dedup = /* @__PURE__ */ new Map();
1174
+ for (const row of rows) {
1175
+ dedup.set(row.timestamp, row);
1176
+ }
1177
+ return [...dedup.values()].sort((a, b) => a.timestamp - b.timestamp);
1178
+ };
994
1179
  return {
995
1180
  getState: async () => state,
996
1181
  setState: async (newState) => {
997
1182
  state = { ...state, ...newState };
998
1183
  },
999
- kline: async ({ symbol, interval, start, end }) => {
1000
- const granularity = GRANULARITY_MAP[String(interval)];
1001
- if (!granularity) return [];
1002
- const product = toCoinbaseProduct(symbol);
1003
- if (!product) return [];
1004
- const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
1005
- const stepMs = granularity * 1e3 * 300;
1006
- const fromMs = start ?? Math.max(0, end - INTERVAL_MS2[String(interval)] * 1e3);
1007
- let cursorEnd = end;
1008
- const rows = [];
1009
- while (cursorEnd >= fromMs) {
1010
- const chunkStart = Math.max(fromMs, cursorEnd - stepMs);
1011
- const url = new URL(`${baseUrl}/products/${product}/candles`);
1012
- url.searchParams.set("granularity", String(granularity));
1013
- url.searchParams.set("start", new Date(chunkStart).toISOString());
1014
- url.searchParams.set("end", new Date(cursorEnd).toISOString());
1015
- const response = await fetchWithRetry2(url.toString(), {
1016
- headers: { "User-Agent": "tradejs/coinbase-connector" }
1017
- });
1018
- if (!response.ok) break;
1019
- const payload = await response.json();
1020
- if (Array.isArray(payload)) {
1021
- for (const item of payload) {
1022
- if (!Array.isArray(item)) continue;
1023
- const ts = toNum2(item[0]) * 1e3;
1024
- rows.push({
1025
- timestamp: ts,
1026
- low: toNum2(item[1]),
1027
- high: toNum2(item[2]),
1028
- open: toNum2(item[3]),
1029
- close: toNum2(item[4]),
1030
- volume: toNum2(item[5]),
1031
- turnover: 0,
1032
- dt: new Date(ts).toISOString()
1033
- });
1034
- }
1035
- }
1036
- cursorEnd = chunkStart - 1;
1037
- }
1038
- const dedup = /* @__PURE__ */ new Map();
1039
- for (const row of rows) {
1040
- dedup.set(row.timestamp, row);
1041
- }
1042
- return [...dedup.values()].sort((a, b) => a.timestamp - b.timestamp);
1043
- },
1184
+ kline: createTimescaleCachedKline({
1185
+ provider: "coinbase",
1186
+ request: requestKline,
1187
+ intervalToMinutes: intervalToMinutes2
1188
+ }),
1044
1189
  getPosition: async () => null,
1045
1190
  getPositions: async () => [],
1191
+ getOpenPositionPnl: async () => [],
1046
1192
  placeOrder: async () => false,
1047
1193
  setTakeProfits: async () => false,
1048
1194
  setStopLoss: async () => false,