@tradejs/connectors 1.0.5 → 1.0.8

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.js CHANGED
@@ -47,21 +47,26 @@ module.exports = __toCommonJS(index_exports);
47
47
  // src/ByBit/index.ts
48
48
  var import_lodash = __toESM(require("lodash"));
49
49
  var import_chalk = __toESM(require("chalk"));
50
- var import_constants2 = require("@tradejs/core/constants");
51
- var import_data = require("@tradejs/core/data");
50
+ var import_async = require("@tradejs/core/async");
51
+ var import_constants3 = require("@tradejs/core/constants");
52
52
  var import_data2 = require("@tradejs/core/data");
53
53
  var import_math = require("@tradejs/core/math");
54
54
  var import_tickers = require("@tradejs/core/tickers");
55
- var import_time2 = require("@tradejs/core/time");
56
- var import_logger2 = require("@tradejs/infra/logger");
57
- var import_timescale = require("@tradejs/infra/timescale");
55
+ var import_time3 = require("@tradejs/core/time");
56
+ var import_logger3 = require("@tradejs/infra/logger");
58
57
 
59
58
  // src/ByBit/client.ts
60
59
  var import_bybit_api = require("bybit-api");
61
60
  var import_logger = require("@tradejs/infra/logger");
62
61
  var import_redis = require("@tradejs/infra/redis");
63
62
  var useTestnet = false;
64
- var getClient = async ({ userName }) => {
63
+ var getClient = async ({ userName }, access = "private") => {
64
+ if (access === "public") {
65
+ return new import_bybit_api.RestClientV5({
66
+ parseAPIRateLimits: true,
67
+ testnet: useTestnet
68
+ });
69
+ }
65
70
  const user = await (0, import_redis.getData)(import_redis.redisKeys.user(userName));
66
71
  if (!user) {
67
72
  import_logger.logger.log("error", "connection config not found: %s", userName);
@@ -70,16 +75,248 @@ var getClient = async ({ userName }) => {
70
75
  const client = new import_bybit_api.RestClientV5({
71
76
  key: user.BYBIT_API_KEY,
72
77
  secret: user.BYBIT_API_SECRET,
78
+ parseAPIRateLimits: true,
73
79
  testnet: useTestnet
74
80
  });
75
81
  return client;
76
82
  };
77
83
 
78
- // src/ByBit/utils.ts
84
+ // src/shared/timescaleKlineCache.ts
79
85
  var import_constants = require("@tradejs/core/constants");
86
+ var import_data = require("@tradejs/core/data");
80
87
  var import_time = require("@tradejs/core/time");
88
+ var import_logger2 = require("@tradejs/infra/logger");
89
+ var import_timescale = require("@tradejs/infra/timescale");
90
+ var DEFAULT_LIMIT = 1e3;
91
+ var DEFAULT_CACHE_FALLBACK_WINDOW = 1e3;
92
+ var intervalMsOf = (interval) => interval * 6e4;
93
+ var clampToClosedCandle = (value, intervalMs) => Math.floor(value / intervalMs) * intervalMs;
94
+ var normalizeRangeToClosed = (intervalMs, start, end) => {
95
+ const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
96
+ const normStart = start !== void 0 ? clampToClosedCandle(start, intervalMs) : 0;
97
+ const cappedEnd = Math.min(end ?? Date.now(), lastClosed);
98
+ const normEnd = clampToClosedCandle(cappedEnd, intervalMs);
99
+ return { normStart, normEnd };
100
+ };
101
+ var rowsToKline = (rows) => rows.map(({ ts, ...data }) => ({
102
+ timestamp: new Date(ts).getTime(),
103
+ ...data
104
+ }));
105
+ var createTimescaleCachedKline = ({
106
+ provider,
107
+ request,
108
+ intervalToMinutes: intervalToMinutes3,
109
+ limit = DEFAULT_LIMIT,
110
+ cacheFallbackWindow = DEFAULT_CACHE_FALLBACK_WINDOW
111
+ }) => {
112
+ let isTimescaleFallbackMode = false;
113
+ const loadData = async (direction, pointer, limitBoundary, requestParams, intervalMs) => {
114
+ if (pointer === void 0) return [];
115
+ let accumulated = [];
116
+ let fulfilled = false;
117
+ while (!fulfilled) {
118
+ const currentPointer = pointer;
119
+ const params = {
120
+ symbol: requestParams.symbol,
121
+ interval: requestParams.interval,
122
+ silent: requestParams.silent,
123
+ end: requestParams.end
124
+ };
125
+ if (direction === "older") {
126
+ params.end = pointer;
127
+ if (limitBoundary !== void 0) params.start = limitBoundary;
128
+ } else {
129
+ params.start = pointer;
130
+ if (limitBoundary !== void 0) params.end = limitBoundary;
131
+ }
132
+ const partData = await request(params);
133
+ if (!partData.length) {
134
+ fulfilled = true;
135
+ break;
136
+ }
137
+ accumulated = direction === "older" ? (0, import_data.mergeData)(partData, accumulated) : (0, import_data.mergeData)(accumulated, partData);
138
+ const boundaryReached = limitBoundary !== void 0 && (direction === "older" && currentPointer <= limitBoundary || direction === "newer" && currentPointer >= limitBoundary);
139
+ if (partData.length < limit || boundaryReached) {
140
+ fulfilled = true;
141
+ break;
142
+ }
143
+ const nextPointer = direction === "older" ? (0, import_time.getItemTimestamp)(partData[0]) - intervalMs : (0, import_time.getItemTimestamp)(partData[partData.length - 1]) + intervalMs;
144
+ if (!Number.isFinite(nextPointer) || nextPointer === currentPointer) {
145
+ fulfilled = true;
146
+ break;
147
+ }
148
+ pointer = nextPointer;
149
+ }
150
+ return accumulated;
151
+ };
152
+ const refreshTail = async ({
153
+ symbol,
154
+ interval,
155
+ silent,
156
+ tailCount = 2
157
+ }) => {
158
+ const intMinutes = intervalToMinutes3(interval);
159
+ if (!intMinutes) {
160
+ import_logger2.logger.log("error", "refreshTail: invalid interval %s", interval);
161
+ return;
162
+ }
163
+ const intervalMs = intervalMsOf(intMinutes);
164
+ const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
165
+ const tailEnd = lastClosed + intervalMs;
166
+ const tailStart = tailEnd - tailCount * intervalMs;
167
+ const part = await request({
168
+ symbol,
169
+ interval,
170
+ start: tailStart,
171
+ end: tailEnd,
172
+ silent
173
+ });
174
+ if (part.length) {
175
+ await (0, import_timescale.upsertCandles)((0, import_timescale.toRows)(provider, symbol, intMinutes, part));
176
+ }
177
+ };
178
+ return async ({
179
+ symbol,
180
+ interval,
181
+ start: defaultStart,
182
+ end: defaultEnd,
183
+ silent = false,
184
+ cacheOnly = false,
185
+ warmOnly = false
186
+ }) => {
187
+ const intMinutes = intervalToMinutes3(interval);
188
+ if (!intMinutes) {
189
+ import_logger2.logger.log("error", "kline: invalid interval %s", interval);
190
+ return [];
191
+ }
192
+ if (defaultStart !== void 0 && defaultEnd !== void 0 && defaultEnd <= defaultStart) {
193
+ return [];
194
+ }
195
+ const intervalMs = intervalMsOf(intMinutes);
196
+ try {
197
+ const edges = await (0, import_timescale.getDataEdges)(provider, symbol, intMinutes);
198
+ let dataStart = edges.min;
199
+ let dataEnd = edges.max;
200
+ const { normStart, normEnd } = normalizeRangeToClosed(
201
+ intervalMs,
202
+ defaultStart,
203
+ defaultEnd
204
+ );
205
+ if (cacheOnly) {
206
+ const base = edges.max ?? Date.now();
207
+ const s = Math.max(
208
+ defaultStart ?? base - cacheFallbackWindow * intervalMs,
209
+ 0
210
+ );
211
+ const e = defaultEnd ?? base;
212
+ const dbData2 = await (0, import_timescale.getCandlesRange)(
213
+ provider,
214
+ symbol,
215
+ intMinutes,
216
+ s,
217
+ e
218
+ );
219
+ return rowsToKline(dbData2);
220
+ }
221
+ const needOlderData = defaultStart !== void 0 && (dataStart === void 0 || normStart < dataStart);
222
+ const needNewerData = defaultEnd !== void 0 && (dataEnd === void 0 || normEnd > dataEnd);
223
+ if (needOlderData) {
224
+ const pointerForOlder = dataStart ?? normEnd ?? Date.now();
225
+ const olderData = await loadData(
226
+ "older",
227
+ pointerForOlder,
228
+ normStart,
229
+ {
230
+ symbol,
231
+ interval,
232
+ silent,
233
+ start: normStart,
234
+ end: pointerForOlder
235
+ },
236
+ intervalMs
237
+ );
238
+ if (olderData.length) {
239
+ await (0, import_timescale.upsertCandles)((0, import_timescale.toRows)(provider, symbol, intMinutes, olderData));
240
+ dataStart = normStart;
241
+ }
242
+ }
243
+ if (needNewerData) {
244
+ const fallbackStart = (0, import_time.getTimestamp)(import_constants.PRELOAD_FALLBACK_DAYS);
245
+ const pointerForNewer = dataEnd ?? (defaultStart !== void 0 ? normStart : fallbackStart) ?? 0;
246
+ const newerData = await loadData(
247
+ "newer",
248
+ pointerForNewer,
249
+ normEnd,
250
+ {
251
+ symbol,
252
+ interval,
253
+ silent,
254
+ start: pointerForNewer,
255
+ end: normEnd
256
+ },
257
+ intervalMs
258
+ );
259
+ if (newerData.length) {
260
+ await (0, import_timescale.upsertCandles)((0, import_timescale.toRows)(provider, symbol, intMinutes, newerData));
261
+ dataEnd = normEnd;
262
+ }
263
+ }
264
+ const isRightEdgeQuery = defaultEnd === void 0 || defaultEnd && defaultEnd >= Date.now() - intervalMs;
265
+ if (!cacheOnly && isRightEdgeQuery) {
266
+ await refreshTail({ symbol, interval, silent });
267
+ }
268
+ if (warmOnly) {
269
+ if (isTimescaleFallbackMode) {
270
+ isTimescaleFallbackMode = false;
271
+ import_logger2.logger.log("info", "TimescaleDB connection restored for kline cache");
272
+ }
273
+ return [];
274
+ }
275
+ const rangeStart = defaultStart ?? dataStart ?? 0;
276
+ const rangeEnd = defaultEnd ?? dataEnd ?? Date.now();
277
+ const { normStart: finalStart, normEnd: finalEnd } = normalizeRangeToClosed(intervalMs, rangeStart, rangeEnd);
278
+ const dbData = await (0, import_timescale.getCandlesRange)(
279
+ provider,
280
+ symbol,
281
+ intMinutes,
282
+ finalStart,
283
+ finalEnd
284
+ );
285
+ if (isTimescaleFallbackMode) {
286
+ isTimescaleFallbackMode = false;
287
+ import_logger2.logger.log("info", "TimescaleDB connection restored for kline cache");
288
+ }
289
+ return rowsToKline(dbData);
290
+ } catch (error) {
291
+ if (!isTimescaleFallbackMode) {
292
+ isTimescaleFallbackMode = true;
293
+ import_logger2.logger.log(
294
+ "warn",
295
+ "TimescaleDB unavailable for %s %s: %s. Falling back to exchange API.",
296
+ symbol,
297
+ interval,
298
+ String(error)
299
+ );
300
+ }
301
+ if (cacheOnly || warmOnly) {
302
+ return [];
303
+ }
304
+ return request({
305
+ symbol,
306
+ interval,
307
+ start: defaultStart,
308
+ end: defaultEnd,
309
+ silent
310
+ });
311
+ }
312
+ };
313
+ };
314
+
315
+ // src/ByBit/utils.ts
316
+ var import_constants2 = require("@tradejs/core/constants");
317
+ var import_time2 = require("@tradejs/core/time");
81
318
  var parseKlineItem = (item) => ({
82
- dt: (0, import_time.formatUnix)(parseInt(item[0])),
319
+ dt: (0, import_time2.formatUnix)(parseInt(item[0])),
83
320
  timestamp: parseInt(item[0]),
84
321
  open: parseFloat(item[1]),
85
322
  high: parseFloat(item[2]),
@@ -105,7 +342,7 @@ var getSymbolMeta = async (client, symbol) => {
105
342
  const cached = symbolMetaCache.get(symbol);
106
343
  if (cached) return cached;
107
344
  const res = await client.getInstrumentsInfo({
108
- category: import_constants.MARKET_CATEGORY,
345
+ category: import_constants2.MARKET_CATEGORY,
109
346
  symbol
110
347
  });
111
348
  const item = res?.result?.list?.[0];
@@ -168,7 +405,12 @@ var mapPositionData = (data) => {
168
405
 
169
406
  // src/ByBit/index.ts
170
407
  var LIMIT = 1e3;
171
- var CACHE_FALLBACK_WINDOW = 1e3;
408
+ var BYBIT_RATE_LIMIT_RETCODE = 10006;
409
+ var BYBIT_TRADING_STOP_NOT_MODIFIED_RETCODE = 34040;
410
+ var KLINE_RATE_LIMIT_MAX_ATTEMPTS = 3;
411
+ var KLINE_RATE_LIMIT_BASE_DELAY_MS = 800;
412
+ var KLINE_RATE_LIMIT_MAX_DELAY_MS = 1e4;
413
+ var KLINE_RATE_LIMIT_RESET_BUFFER_MS = 50;
172
414
  var INTERVAL_TO_MINUTES = {
173
415
  "1": 1,
174
416
  "3": 3,
@@ -185,9 +427,36 @@ var INTERVAL_TO_MINUTES = {
185
427
  M: 43200
186
428
  };
187
429
  var getLogLevel = (res) => res.retCode === 0 ? "info" : "error";
430
+ var isTradingStopNotModified = (res) => res?.retCode === BYBIT_TRADING_STOP_NOT_MODIFIED_RETCODE;
431
+ var isTradingStopAccepted = (res) => res?.retCode === 0 || isTradingStopNotModified(res);
432
+ var isKlineRateLimited = (res) => res?.retCode === BYBIT_RATE_LIMIT_RETCODE;
433
+ var resolveKlineRetryDelayMs = (res, attempt) => {
434
+ const resetAtRaw = Number(res?.rateLimitApi?.resetAtTimestamp);
435
+ if (Number.isFinite(resetAtRaw) && resetAtRaw > 0) {
436
+ return Math.max(
437
+ KLINE_RATE_LIMIT_RESET_BUFFER_MS,
438
+ resetAtRaw - Date.now() + KLINE_RATE_LIMIT_RESET_BUFFER_MS
439
+ );
440
+ }
441
+ const jitterMs = Math.round(Math.random() * 250);
442
+ const backoffMs = Math.min(
443
+ KLINE_RATE_LIMIT_MAX_DELAY_MS,
444
+ KLINE_RATE_LIMIT_BASE_DELAY_MS * 2 ** (attempt - 1)
445
+ );
446
+ return backoffMs + jitterMs;
447
+ };
188
448
  var ByBitConnectorCreator = async (config) => {
189
449
  let state = {};
190
- let isTimescaleFallbackMode = false;
450
+ let publicClientPromise = null;
451
+ let privateClientPromise = null;
452
+ const getPublicClient = async () => {
453
+ publicClientPromise ??= getClient(config, "public");
454
+ return publicClientPromise;
455
+ };
456
+ const getPrivateClient = async () => {
457
+ privateClientPromise ??= getClient(config, "private");
458
+ return privateClientPromise;
459
+ };
191
460
  const request = async ({
192
461
  symbol,
193
462
  interval,
@@ -196,48 +465,67 @@ var ByBitConnectorCreator = async (config) => {
196
465
  silent
197
466
  }) => {
198
467
  const normalizedStart = (0, import_math.round)(
199
- start || (0, import_time2.getTimestamp)(import_constants2.PRELOAD_FALLBACK_DAYS),
468
+ start || (0, import_time3.getTimestamp)(import_constants3.PRELOAD_FALLBACK_DAYS),
200
469
  0
201
470
  );
202
471
  const normalizedEnd = (0, import_math.round)(end || Date.now());
203
472
  try {
204
- const client = await getClient(config);
473
+ const client = await getPublicClient();
205
474
  if (!client) return [];
206
475
  if (normalizedEnd <= normalizedStart) {
207
476
  return [];
208
477
  }
209
- const kline = await client.getKline({
210
- category: import_constants2.MARKET_CATEGORY,
211
- symbol,
212
- interval,
213
- start: normalizedStart,
214
- end: normalizedEnd,
215
- limit: LIMIT
216
- });
217
- if (!kline?.result?.list) {
218
- 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}` : "";
219
- import_logger2.logger.log(
220
- "error",
221
- "empty kline.list for %s %s%s",
478
+ for (let attempt = 1; attempt <= KLINE_RATE_LIMIT_MAX_ATTEMPTS; attempt += 1) {
479
+ const kline = await client.getKline({
480
+ category: import_constants3.MARKET_CATEGORY,
222
481
  symbol,
223
482
  interval,
224
- responseError ? `: ${responseError}` : ""
225
- );
226
- return [];
227
- }
228
- if (!silent) {
229
- import_logger2.logger.log(
230
- "info",
231
- "%s %s %s %s",
232
- import_chalk.default.yellow((0, import_time2.formatUnix)(normalizedEnd)),
233
- import_chalk.default.cyan(symbol),
234
- import_chalk.default.cyan(interval),
235
- import_chalk.default.yellow(kline.result.list.length)
236
- );
483
+ start: normalizedStart,
484
+ end: normalizedEnd,
485
+ limit: LIMIT
486
+ });
487
+ if (isKlineRateLimited(kline)) {
488
+ if (attempt < KLINE_RATE_LIMIT_MAX_ATTEMPTS) {
489
+ const waitMs = resolveKlineRetryDelayMs(kline, attempt);
490
+ import_logger3.logger.log(
491
+ "warn",
492
+ "kline rate limited for %s %s: attempt=%s/%s waitMs=%s",
493
+ symbol,
494
+ interval,
495
+ attempt,
496
+ KLINE_RATE_LIMIT_MAX_ATTEMPTS,
497
+ waitMs
498
+ );
499
+ await (0, import_async.delay)(waitMs);
500
+ continue;
501
+ }
502
+ }
503
+ if (!kline?.result?.list) {
504
+ 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}` : "";
505
+ import_logger3.logger.log(
506
+ "error",
507
+ "empty kline.list for %s %s%s",
508
+ symbol,
509
+ interval,
510
+ responseError ? `: ${responseError}` : ""
511
+ );
512
+ return [];
513
+ }
514
+ if (!silent && (process.stdout.isTTY || process.env.TRADEJS_LOG_KLINE_REQUESTS === "1")) {
515
+ import_logger3.logger.log(
516
+ "info",
517
+ "%s %s %s %s",
518
+ import_chalk.default.yellow((0, import_time3.formatUnix)(normalizedEnd)),
519
+ import_chalk.default.cyan(symbol),
520
+ import_chalk.default.cyan(interval),
521
+ import_chalk.default.yellow(kline.result.list.length)
522
+ );
523
+ }
524
+ return mapKlineToChartData(kline.result.list.reverse());
237
525
  }
238
- return mapKlineToChartData(kline.result.list.reverse());
526
+ return [];
239
527
  } catch (error) {
240
- import_logger2.logger.log(
528
+ import_logger3.logger.log(
241
529
  "error",
242
530
  "request kline: %s %s %s",
243
531
  normalizedStart,
@@ -247,276 +535,326 @@ var ByBitConnectorCreator = async (config) => {
247
535
  return [];
248
536
  }
249
537
  };
250
- const loadData = async (direction, pointer, limitBoundary, requestParams, intervalMs) => {
251
- if (pointer === void 0) return [];
252
- let accumulated = [];
253
- let fulfilled = false;
254
- while (!fulfilled) {
255
- const currentPointer = pointer;
256
- const params = {
257
- symbol: requestParams.symbol,
258
- interval: requestParams.interval,
259
- silent: requestParams.silent
260
- };
261
- if (direction === "older") {
262
- params.end = pointer;
263
- if (limitBoundary !== void 0) params.start = limitBoundary;
264
- } else {
265
- params.start = pointer;
266
- if (limitBoundary !== void 0) params.end = limitBoundary;
267
- }
268
- const partData = await request(params);
269
- if (import_lodash.default.isEmpty(partData)) {
270
- fulfilled = true;
271
- break;
538
+ const intervalToMinutes3 = (interval) => {
539
+ return INTERVAL_TO_MINUTES[String(interval)] ?? null;
540
+ };
541
+ const getPositionSnapshot = async (symbol) => {
542
+ const client = await getPrivateClient();
543
+ if (!client) {
544
+ return null;
545
+ }
546
+ const positionRes = await client.getPositionInfo({
547
+ symbol,
548
+ category: import_constants3.MARKET_CATEGORY
549
+ });
550
+ if (positionRes.retCode !== 0) {
551
+ import_logger3.logger.log(
552
+ getLogLevel(positionRes),
553
+ "position retCode: %s, %s",
554
+ symbol,
555
+ positionRes.retCode
556
+ );
557
+ return null;
558
+ }
559
+ const positions = mapPositionData(positionRes.result.list);
560
+ if (!positions || import_lodash.default.isEmpty(positions)) {
561
+ return null;
562
+ }
563
+ const position = positions[0];
564
+ import_logger3.logger.log(
565
+ "debug",
566
+ "position: %s %s qty=%s price=%s",
567
+ position.symbol,
568
+ position.direction,
569
+ position.qty,
570
+ position.price
571
+ );
572
+ return {
573
+ ...position
574
+ };
575
+ };
576
+ const setTakeProfits = async ({
577
+ symbol,
578
+ direction,
579
+ qty,
580
+ takeProfits
581
+ }) => {
582
+ const client = await getPrivateClient();
583
+ const marketDataClient = await getPublicClient();
584
+ if (!client || !marketDataClient) {
585
+ return false;
586
+ }
587
+ if (!Array.isArray(takeProfits) || takeProfits.length === 0) {
588
+ return true;
589
+ }
590
+ const meta = await getSymbolMeta(marketDataClient, symbol);
591
+ const positionQty = qty ?? (await getPositionSnapshot(symbol))?.qty ?? 0;
592
+ if (!Number.isFinite(positionQty) || positionQty <= 0) {
593
+ import_logger3.logger.log(
594
+ "warn",
595
+ "setTakeProfits: missing position qty: %s",
596
+ (0, import_data2.toJson)({ symbol, qty, positionQty, takeProfits }, true)
597
+ );
598
+ return false;
599
+ }
600
+ const isLong = direction === "LONG";
601
+ for (const tp of takeProfits) {
602
+ const tpSizeRaw = positionQty * tp.rate;
603
+ const { qtyNum: tpSizeNum, qtyStr: tpSizeStr } = normalizeQty(
604
+ tpSizeRaw,
605
+ meta
606
+ );
607
+ if (!tpSizeNum || tpSizeNum < meta.minOrderQty) {
608
+ import_logger3.logger.log(
609
+ "warn",
610
+ "tp skipped: size too small %s",
611
+ (0, import_data2.toJson)(
612
+ { symbol, tp, tpSizeNum, minOrderQty: meta.minOrderQty },
613
+ true
614
+ )
615
+ );
616
+ continue;
272
617
  }
273
- accumulated = direction === "older" ? (0, import_data.mergeData)(partData, accumulated) : (0, import_data.mergeData)(accumulated, partData);
274
- const boundaryReached = limitBoundary !== void 0 && (direction === "older" && currentPointer <= limitBoundary || direction === "newer" && currentPointer >= limitBoundary);
275
- if (partData.length < LIMIT || boundaryReached) {
276
- fulfilled = true;
277
- break;
618
+ const tpPriceNorm = normalizePrice(
619
+ tp.price,
620
+ isLong ? "TP_LONG" : "TP_SHORT",
621
+ meta
622
+ );
623
+ const isFullMode = takeProfits.length === 1 && tp.rate === 1;
624
+ const tpRes = await client.setTradingStop({
625
+ category: import_constants3.MARKET_CATEGORY,
626
+ symbol,
627
+ tpSize: isFullMode ? void 0 : tpSizeStr,
628
+ tpslMode: isFullMode ? "Full" : "Partial",
629
+ takeProfit: tpPriceNorm.priceStr,
630
+ tpTriggerBy: "MarkPrice",
631
+ tpOrderType: "Market",
632
+ positionIdx: 0
633
+ });
634
+ if (isTradingStopNotModified(tpRes)) {
635
+ import_logger3.logger.log(
636
+ "debug",
637
+ "tp unchanged: %s %s price=%s rate=%s",
638
+ symbol,
639
+ direction,
640
+ tpPriceNorm.priceStr,
641
+ tp.rate
642
+ );
643
+ } else if (tpRes.retCode === 0) {
644
+ import_logger3.logger.log(
645
+ "info",
646
+ "tp updated: %s %s price=%s rate=%s",
647
+ symbol,
648
+ direction,
649
+ tpPriceNorm.priceStr,
650
+ tp.rate
651
+ );
652
+ } else {
653
+ import_logger3.logger.log(
654
+ "error",
655
+ "tp failed: %s %s price=%s rate=%s %s",
656
+ symbol,
657
+ direction,
658
+ tpPriceNorm.priceStr,
659
+ tp.rate,
660
+ (0, import_data2.toJson)(tpRes, true)
661
+ );
278
662
  }
279
- const nextPointer = direction === "older" ? (0, import_time2.getItemTimestamp)(partData[0]) - intervalMs : (0, import_time2.getItemTimestamp)(partData[partData.length - 1]) + intervalMs;
280
- if (!Number.isFinite(nextPointer) || nextPointer === currentPointer) {
281
- fulfilled = true;
282
- break;
663
+ if (!isTradingStopAccepted(tpRes)) {
664
+ return false;
283
665
  }
284
- pointer = nextPointer;
285
666
  }
286
- return accumulated;
287
- };
288
- const intervalMsOf = (interval) => interval * 6e4;
289
- const intervalToMinutes = (interval) => {
290
- return INTERVAL_TO_MINUTES[String(interval)] ?? null;
667
+ return true;
291
668
  };
292
- const clampToClosedCandle = (value, intervalMs) => Math.floor(value / intervalMs) * intervalMs;
293
- const normalizeRangeToClosed = (intervalMs, start, end) => {
294
- const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
295
- const normStart = start !== void 0 ? clampToClosedCandle(start, intervalMs) : 0;
296
- const cappedEnd = Math.min(end ?? Date.now(), lastClosed);
297
- const normEnd = clampToClosedCandle(cappedEnd, intervalMs);
298
- return { normStart, normEnd, lastClosed };
299
- };
300
- const rowsToKline = (rows) => rows.map(({ ts, ...data }) => ({
301
- timestamp: new Date(ts).getTime(),
302
- ...data
303
- }));
304
- const refreshTail = async ({
669
+ const setStopLoss = async ({
305
670
  symbol,
306
- interval,
307
- silent,
308
- tailCount = 2
671
+ direction,
672
+ stopLossPrice
309
673
  }) => {
310
- const intMinutes = intervalToMinutes(interval);
311
- if (!intMinutes) {
312
- import_logger2.logger.log("error", "refreshTail: invalid interval %s", interval);
313
- return;
674
+ const client = await getPrivateClient();
675
+ const marketDataClient = await getPublicClient();
676
+ if (!client || !marketDataClient) {
677
+ return false;
314
678
  }
315
- const intervalMs = intervalMsOf(intMinutes);
316
- const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
317
- const tailEnd = lastClosed + intervalMs;
318
- const tailStart = tailEnd - tailCount * intervalMs;
319
- const part = await request({
679
+ if (typeof stopLossPrice !== "number" || !Number.isFinite(stopLossPrice)) {
680
+ return true;
681
+ }
682
+ const meta = await getSymbolMeta(marketDataClient, symbol);
683
+ const isLong = direction === "LONG";
684
+ const slNormalized = normalizePrice(
685
+ stopLossPrice,
686
+ isLong ? "SL_LONG" : "SL_SHORT",
687
+ meta
688
+ );
689
+ const slRes = await client.setTradingStop({
690
+ category: import_constants3.MARKET_CATEGORY,
320
691
  symbol,
321
- interval,
322
- start: tailStart,
323
- end: tailEnd,
324
- silent
692
+ tpslMode: "Full",
693
+ stopLoss: slNormalized.priceStr,
694
+ slTriggerBy: "LastPrice",
695
+ positionIdx: 0
325
696
  });
326
- if (part.length) {
327
- await (0, import_timescale.upsertCandles)((0, import_timescale.toRows)(symbol, intMinutes, part));
697
+ if (isTradingStopNotModified(slRes)) {
698
+ import_logger3.logger.log(
699
+ "debug",
700
+ "sl unchanged: %s %s stopLoss=%s",
701
+ symbol,
702
+ direction,
703
+ slNormalized.priceStr
704
+ );
705
+ } else if (slRes.retCode === 0) {
706
+ import_logger3.logger.log(
707
+ "info",
708
+ "sl updated: %s %s stopLoss=%s",
709
+ symbol,
710
+ direction,
711
+ slNormalized.priceStr
712
+ );
713
+ } else {
714
+ import_logger3.logger.log(
715
+ "error",
716
+ "sl failed: %s %s stopLoss=%s %s",
717
+ symbol,
718
+ direction,
719
+ slNormalized.priceStr,
720
+ (0, import_data2.toJson)(slRes, true)
721
+ );
328
722
  }
723
+ return isTradingStopAccepted(slRes);
329
724
  };
330
725
  return {
331
726
  getState: async () => state,
332
727
  setState: async (newState) => {
333
728
  state = { ...state, ...newState };
334
729
  },
335
- kline: async ({
336
- symbol,
337
- interval,
338
- start: defaultStart,
339
- end: defaultEnd,
340
- silent = false,
341
- cacheOnly = false
342
- }) => {
343
- const intMinutes = intervalToMinutes(interval);
344
- if (!intMinutes) {
345
- import_logger2.logger.log("error", "kline: invalid interval %s", interval);
346
- return [];
347
- }
348
- if (defaultStart !== void 0 && defaultEnd !== void 0 && defaultEnd <= defaultStart) {
349
- return [];
350
- }
351
- const intervalMs = intervalMsOf(intMinutes);
352
- try {
353
- const edges = await (0, import_timescale.getDataEdges)(symbol, intMinutes);
354
- let dataStart = edges.min;
355
- let dataEnd = edges.max;
356
- const { normStart, normEnd } = normalizeRangeToClosed(
357
- intervalMs,
358
- defaultStart,
359
- defaultEnd
360
- );
361
- if (cacheOnly) {
362
- const base = edges.max ?? Date.now();
363
- const s = Math.max(
364
- defaultStart ?? base - CACHE_FALLBACK_WINDOW * intervalMs,
365
- 0
366
- );
367
- const e = defaultEnd ?? base;
368
- const dbData2 = await (0, import_timescale.getCandlesRange)(symbol, intMinutes, s, e);
369
- return rowsToKline(dbData2);
370
- }
371
- const needOlderData = defaultStart !== void 0 && (dataStart === void 0 || normStart < dataStart);
372
- const needNewerData = defaultEnd !== void 0 && (dataEnd === void 0 || normEnd > dataEnd);
373
- if (needOlderData) {
374
- const pointerForOlder = dataStart ?? normEnd ?? Date.now();
375
- const olderData = await loadData(
376
- "older",
377
- pointerForOlder,
378
- normStart,
379
- {
380
- symbol,
381
- interval,
382
- silent,
383
- start: normStart,
384
- end: pointerForOlder
385
- },
386
- intervalMs
387
- );
388
- if (olderData.length) {
389
- await (0, import_timescale.upsertCandles)((0, import_timescale.toRows)(symbol, intMinutes, olderData));
390
- dataStart = normStart;
391
- }
392
- }
393
- if (needNewerData) {
394
- const pointerForNewer = dataEnd ?? normStart ?? 0;
395
- const newerData = await loadData(
396
- "newer",
397
- pointerForNewer,
398
- normEnd,
399
- {
400
- symbol,
401
- interval,
402
- silent,
403
- start: pointerForNewer,
404
- end: normEnd
405
- },
406
- intervalMs
407
- );
408
- if (newerData.length) {
409
- await (0, import_timescale.upsertCandles)((0, import_timescale.toRows)(symbol, intMinutes, newerData));
410
- dataEnd = normEnd;
411
- }
412
- }
413
- const isRightEdgeQuery = defaultEnd === void 0 || defaultEnd && defaultEnd >= Date.now() - intervalMs;
414
- if (!cacheOnly && isRightEdgeQuery) {
415
- await refreshTail({ symbol, interval, silent });
416
- }
417
- const rangeStart = defaultStart ?? dataStart ?? 0;
418
- const rangeEnd = defaultEnd ?? dataEnd ?? Date.now();
419
- const { normStart: finalStart, normEnd: finalEnd } = normalizeRangeToClosed(intervalMs, rangeStart, rangeEnd);
420
- const dbData = await (0, import_timescale.getCandlesRange)(
421
- symbol,
422
- intMinutes,
423
- finalStart,
424
- finalEnd
425
- );
426
- if (isTimescaleFallbackMode) {
427
- isTimescaleFallbackMode = false;
428
- import_logger2.logger.log("info", "TimescaleDB connection restored for kline cache");
429
- }
430
- return rowsToKline(dbData);
431
- } catch (error) {
432
- if (!isTimescaleFallbackMode) {
433
- isTimescaleFallbackMode = true;
434
- import_logger2.logger.log(
435
- "warn",
436
- "TimescaleDB unavailable for %s %s: %s. Falling back to exchange API.",
437
- symbol,
438
- interval,
439
- String(error)
440
- );
441
- }
442
- if (cacheOnly) {
443
- return [];
444
- }
445
- return request({
446
- symbol,
447
- interval,
448
- start: defaultStart,
449
- end: defaultEnd,
450
- silent
451
- });
452
- }
453
- },
454
- getPosition: async (symbol) => {
455
- const client = await getClient(config);
730
+ kline: createTimescaleCachedKline({
731
+ provider: "bybit",
732
+ request,
733
+ intervalToMinutes: intervalToMinutes3,
734
+ limit: LIMIT
735
+ }),
736
+ getPosition: async (symbol) => getPositionSnapshot(symbol),
737
+ getPositions: async () => {
738
+ const client = await getPrivateClient();
456
739
  if (!client) {
457
- return null;
740
+ return [];
458
741
  }
459
742
  const positionRes = await client.getPositionInfo({
460
- symbol,
461
- category: import_constants2.MARKET_CATEGORY
743
+ category: import_constants3.MARKET_CATEGORY,
744
+ settleCoin: "USDT"
462
745
  });
463
- import_logger2.logger.log(
464
- getLogLevel(positionRes),
465
- "position retCode: %s, %s",
466
- symbol,
467
- positionRes.retCode
468
- );
469
746
  if (positionRes.retCode !== 0) {
470
- return null;
747
+ import_logger3.logger.log(
748
+ getLogLevel(positionRes),
749
+ "positions retCode: %s, %s",
750
+ positionRes.retCode
751
+ );
752
+ return [];
471
753
  }
472
754
  const positions = mapPositionData(positionRes.result.list);
473
755
  if (!positions || import_lodash.default.isEmpty(positions)) {
474
- return null;
756
+ return [];
475
757
  }
476
- const position = positions[0];
477
- import_logger2.logger.log(
478
- getLogLevel(positionRes),
479
- "position: %s, %s",
480
- symbol,
481
- (0, import_data2.toJson)(positionRes, true)
482
- );
483
- return {
484
- ...position
485
- };
758
+ return positions;
486
759
  },
487
- getPositions: async () => {
488
- const client = await getClient(config);
760
+ getOpenPositionPnl: async () => {
761
+ const client = await getPrivateClient();
489
762
  if (!client) {
490
763
  return [];
491
764
  }
492
765
  const positionRes = await client.getPositionInfo({
493
- category: import_constants2.MARKET_CATEGORY,
766
+ category: import_constants3.MARKET_CATEGORY,
494
767
  settleCoin: "USDT"
495
768
  });
496
- import_logger2.logger.log(
497
- getLogLevel(positionRes),
498
- "positions retCode: %s, %s",
499
- positionRes.retCode
500
- );
501
769
  if (positionRes.retCode !== 0) {
770
+ import_logger3.logger.log(
771
+ getLogLevel(positionRes),
772
+ "positions pnl retCode: %s, %s",
773
+ positionRes.retCode
774
+ );
502
775
  return [];
503
776
  }
504
- const positions = mapPositionData(positionRes.result.list);
505
- if (!positions || import_lodash.default.isEmpty(positions)) {
777
+ return (positionRes.result?.list ?? []).map((item) => {
778
+ const qty = Number(item.size ?? Number.NaN);
779
+ const entryPrice = Number(item.avgPrice ?? Number.NaN);
780
+ const currentPrice = Number(item.markPrice ?? Number.NaN);
781
+ const unrealizedPnl = Number(item.unrealisedPnl ?? Number.NaN);
782
+ const side = String(item.side ?? "");
783
+ if (!Number.isFinite(qty) || qty <= 0 || !Number.isFinite(entryPrice) || !Number.isFinite(currentPrice) || !Number.isFinite(unrealizedPnl) || side !== "Buy" && side !== "Sell") {
784
+ return null;
785
+ }
786
+ return {
787
+ symbol: String(item.symbol ?? ""),
788
+ qty,
789
+ price: entryPrice,
790
+ currentPrice,
791
+ unrealizedPnl,
792
+ direction: side === "Buy" ? "LONG" : "SHORT"
793
+ };
794
+ }).filter(
795
+ (item) => item != null && item.symbol.length > 0
796
+ );
797
+ },
798
+ getClosedPnl: async ({
799
+ startTime,
800
+ endTime,
801
+ symbol,
802
+ limit = 100
803
+ }) => {
804
+ const client = await getPrivateClient();
805
+ if (!client) {
506
806
  return [];
507
807
  }
508
- return positions;
808
+ const response = await client.getClosedPnL({
809
+ category: import_constants3.MARKET_CATEGORY,
810
+ startTime,
811
+ endTime,
812
+ symbol,
813
+ limit: Math.min(Math.max(1, Math.trunc(limit)), 100)
814
+ });
815
+ if (response.retCode !== 0) {
816
+ import_logger3.logger.log(
817
+ "error",
818
+ "closedPnl retCode: %s, %s",
819
+ response.retCode,
820
+ response.retMsg
821
+ );
822
+ return [];
823
+ }
824
+ return (response.result?.list ?? []).map((item) => {
825
+ const qty = Number(item.qty ?? item.closedSize ?? Number.NaN);
826
+ const entryPrice = Number(item.avgEntryPrice ?? Number.NaN);
827
+ const exitPrice = Number(item.avgExitPrice ?? Number.NaN);
828
+ const closedPnl = Number(item.closedPnl ?? Number.NaN);
829
+ const closedAt = Number(
830
+ item.updatedTime ?? item.createdTime ?? Number.NaN
831
+ );
832
+ const orderId = typeof item.orderId === "string" && item.orderId.trim() ? item.orderId : null;
833
+ if (!String(item.symbol ?? "").trim() || !Number.isFinite(qty) || !Number.isFinite(closedPnl) || !Number.isFinite(closedAt)) {
834
+ return null;
835
+ }
836
+ return {
837
+ symbol: String(item.symbol),
838
+ qty,
839
+ entryPrice: Number.isFinite(entryPrice) ? entryPrice : null,
840
+ exitPrice: Number.isFinite(exitPrice) ? exitPrice : null,
841
+ closedPnl,
842
+ closedAt,
843
+ ...orderId ? { orderId } : {}
844
+ };
845
+ }).filter((item) => item != null);
509
846
  },
510
- placeOrder: async ({ symbol, price, qty, direction, isLimit }, TP = [], slPrice) => {
511
- const client = await getClient(config);
512
- if (!client) {
847
+ placeOrder: async ({ symbol, price, qty, direction, isLimit, orderId }) => {
848
+ const client = await getPrivateClient();
849
+ const marketDataClient = await getPublicClient();
850
+ if (!client || !marketDataClient) {
513
851
  return false;
514
852
  }
515
853
  const isLong = direction === "LONG";
516
- const meta = await getSymbolMeta(client, symbol);
854
+ const meta = await getSymbolMeta(marketDataClient, symbol);
517
855
  const { qtyNum: orderQty, qtyStr: orderQtyStr } = normalizeQty(qty, meta);
518
856
  if (orderQty < meta.minOrderQty) {
519
- import_logger2.logger.log(
857
+ import_logger3.logger.log(
520
858
  "warn",
521
859
  "placeOrder: qty too small: %s",
522
860
  (0, import_data2.toJson)(
@@ -527,10 +865,7 @@ var ByBitConnectorCreator = async (config) => {
527
865
  return false;
528
866
  }
529
867
  const entryNormalized = isLimit ? normalizePrice(price, "ENTRY", meta) : void 0;
530
- const slNormalized = slPrice ? normalizePrice(slPrice, isLong ? "SL_LONG" : "SL_SHORT", meta) : void 0;
531
- const firstTP = TP?.[0];
532
- const tpNormalized = firstTP && firstTP.rate === 1 ? normalizePrice(firstTP.price, isLong ? "TP_LONG" : "TP_SHORT", meta) : void 0;
533
- import_logger2.logger.log(
868
+ import_logger3.logger.log(
534
869
  "info",
535
870
  "placeOrder: %s",
536
871
  (0, import_data2.toJson)(
@@ -540,34 +875,28 @@ var ByBitConnectorCreator = async (config) => {
540
875
  qty,
541
876
  direction,
542
877
  orderQty,
543
- orderQtyStr,
544
- slPrice,
545
- slPriceNorm: slNormalized?.priceStr,
546
- TP
878
+ orderQtyStr
547
879
  },
548
880
  true
549
881
  )
550
882
  );
551
883
  await client.setLeverage({
552
- category: import_constants2.MARKET_CATEGORY,
884
+ category: import_constants3.MARKET_CATEGORY,
553
885
  symbol,
554
886
  buyLeverage: "10",
555
887
  sellLeverage: "10"
556
888
  });
557
889
  const orderRes = await client.submitOrder({
558
- category: import_constants2.MARKET_CATEGORY,
890
+ category: import_constants3.MARKET_CATEGORY,
559
891
  symbol,
560
892
  price: entryNormalized?.priceStr || void 0,
561
- takeProfit: tpNormalized?.priceStr || void 0,
562
- tpTriggerBy: "MarkPrice",
563
- stopLoss: slNormalized?.priceStr || void 0,
564
- slTriggerBy: "LastPrice",
565
893
  side: isLong ? "Buy" : "Sell",
566
894
  orderType: isLimit ? "Limit" : "Market",
567
895
  qty: orderQtyStr,
568
- orderFilter: "Order"
896
+ orderFilter: "Order",
897
+ orderLinkId: typeof orderId === "string" && orderId.trim() ? orderId : void 0
569
898
  });
570
- import_logger2.logger.log(
899
+ import_logger3.logger.log(
571
900
  getLogLevel(orderRes),
572
901
  "placeOrder:response: %s",
573
902
  (0, import_data2.toJson)(orderRes, true)
@@ -575,65 +904,24 @@ var ByBitConnectorCreator = async (config) => {
575
904
  if (orderRes.retCode !== 0) {
576
905
  return false;
577
906
  }
578
- if (!isLimit) {
579
- for (const tp of TP) {
580
- const tpSizeRaw = orderQty * tp.rate;
581
- const { qtyNum: tpSizeNum, qtyStr: tpSizeStr } = normalizeQty(
582
- tpSizeRaw,
583
- meta
584
- );
585
- if (!tpSizeNum || tpSizeNum < meta.minOrderQty) {
586
- import_logger2.logger.log(
587
- "warn",
588
- "tp skipped: size too small %s",
589
- (0, import_data2.toJson)(
590
- { symbol, tp, tpSizeNum, minOrderQty: meta.minOrderQty },
591
- true
592
- )
593
- );
594
- continue;
595
- }
596
- const tpPriceNorm = normalizePrice(
597
- tp.price,
598
- isLong ? "TP_LONG" : "TP_SHORT",
599
- meta
600
- );
601
- const isFullMode = TP.length === 1 && tp.rate === 1;
602
- const tpRes = await client.setTradingStop({
603
- category: import_constants2.MARKET_CATEGORY,
604
- symbol,
605
- tpSize: isFullMode ? void 0 : tpSizeStr,
606
- tpslMode: isFullMode ? "Full" : "Partial",
607
- takeProfit: tpPriceNorm.priceStr,
608
- stopLoss: isFullMode && slNormalized ? slNormalized.priceStr : void 0,
609
- slTriggerBy: "LastPrice",
610
- tpOrderType: "Market",
611
- positionIdx: 0
612
- });
613
- import_logger2.logger.log(
614
- getLogLevel(tpRes),
615
- "tp: %s %s",
616
- (0, import_data2.toJson)(tp, true),
617
- (0, import_data2.toJson)(tpRes, true)
618
- );
619
- }
620
- }
621
907
  return true;
622
908
  },
909
+ setTakeProfits,
910
+ setStopLoss,
623
911
  closePosition: async ({ symbol, direction }) => {
624
- const client = await getClient(config);
912
+ const client = await getPrivateClient();
625
913
  if (!client) {
626
914
  return false;
627
915
  }
628
916
  const closeRes = await client.submitOrder({
629
- category: import_constants2.MARKET_CATEGORY,
917
+ category: import_constants3.MARKET_CATEGORY,
630
918
  symbol,
631
919
  side: direction === "LONG" ? "Sell" : "Buy",
632
920
  orderType: "Market",
633
921
  qty: "0",
634
922
  reduceOnly: true
635
923
  });
636
- import_logger2.logger.log(
924
+ import_logger3.logger.log(
637
925
  getLogLevel(closeRes),
638
926
  "closePosition: %s, %s, %s",
639
927
  symbol,
@@ -646,12 +934,12 @@ var ByBitConnectorCreator = async (config) => {
646
934
  return true;
647
935
  },
648
936
  getTickers: async () => {
649
- const client = await getClient(config);
937
+ const client = await getPublicClient();
650
938
  if (!client) {
651
939
  return [];
652
940
  }
653
941
  const data = await client.getTickers({
654
- category: import_constants2.MARKET_CATEGORY
942
+ category: import_constants3.MARKET_CATEGORY
655
943
  });
656
944
  return data.result.list.map((item) => (0, import_tickers.normalizeTickerData)(item));
657
945
  }
@@ -694,59 +982,77 @@ var toNum = (value, fallback = 0) => {
694
982
  const num = Number(value);
695
983
  return Number.isFinite(num) ? num : fallback;
696
984
  };
985
+ var intervalToMinutes = (interval) => {
986
+ const intervalMs = INTERVAL_MS[String(interval)];
987
+ return intervalMs ? Math.floor(intervalMs / 6e4) : null;
988
+ };
697
989
  var BinanceConnectorCreator = async () => {
698
990
  let state = {};
991
+ const requestKline = async ({
992
+ symbol,
993
+ interval,
994
+ start,
995
+ end
996
+ }) => {
997
+ const intervalToken = INTERVAL_MAP[String(interval)];
998
+ if (!intervalToken) return [];
999
+ const intervalMs = INTERVAL_MS[String(interval)] ?? 9e5;
1000
+ const normalizedEnd = end ?? Date.now();
1001
+ const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
1002
+ let cursor = start ?? Math.max(0, normalizedEnd - intervalMs * 1e3);
1003
+ const rows = [];
1004
+ while (cursor <= normalizedEnd) {
1005
+ const url = new URL(`${baseUrl}/api/v3/klines`);
1006
+ url.searchParams.set("symbol", symbol);
1007
+ url.searchParams.set("interval", intervalToken);
1008
+ url.searchParams.set("startTime", String(cursor));
1009
+ url.searchParams.set("endTime", String(normalizedEnd));
1010
+ url.searchParams.set("limit", "1000");
1011
+ const response = await (0, import_http.fetchWithRetry)(url.toString(), {
1012
+ headers: { "User-Agent": "tradejs/binance-connector" }
1013
+ });
1014
+ if (!response.ok) break;
1015
+ const payload = await response.json();
1016
+ if (!Array.isArray(payload) || !payload.length) break;
1017
+ let lastTs = cursor;
1018
+ for (const item of payload) {
1019
+ if (!Array.isArray(item)) continue;
1020
+ const ts = toNum(item[0], 0);
1021
+ if (!ts) continue;
1022
+ rows.push({
1023
+ timestamp: ts,
1024
+ open: toNum(item[1]),
1025
+ high: toNum(item[2]),
1026
+ low: toNum(item[3]),
1027
+ close: toNum(item[4]),
1028
+ volume: toNum(item[5]),
1029
+ turnover: toNum(item[7]),
1030
+ dt: new Date(ts).toISOString()
1031
+ });
1032
+ lastTs = ts;
1033
+ }
1034
+ if (payload.length < 1e3) break;
1035
+ cursor = lastTs + intervalMs;
1036
+ }
1037
+ rows.sort((a, b) => a.timestamp - b.timestamp);
1038
+ return rows;
1039
+ };
699
1040
  return {
700
1041
  getState: async () => state,
701
1042
  setState: async (newState) => {
702
1043
  state = { ...state, ...newState };
703
1044
  },
704
- kline: async ({ symbol, interval, start, end }) => {
705
- const intervalToken = INTERVAL_MAP[String(interval)];
706
- if (!intervalToken) return [];
707
- const intervalMs = INTERVAL_MS[String(interval)] ?? 9e5;
708
- const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
709
- let cursor = start ?? Math.max(0, end - intervalMs * 1e3);
710
- const rows = [];
711
- while (cursor <= end) {
712
- const url = new URL(`${baseUrl}/api/v3/klines`);
713
- url.searchParams.set("symbol", symbol);
714
- url.searchParams.set("interval", intervalToken);
715
- url.searchParams.set("startTime", String(cursor));
716
- url.searchParams.set("endTime", String(end));
717
- url.searchParams.set("limit", "1000");
718
- const response = await (0, import_http.fetchWithRetry)(url.toString(), {
719
- headers: { "User-Agent": "tradejs/binance-connector" }
720
- });
721
- if (!response.ok) break;
722
- const payload = await response.json();
723
- if (!Array.isArray(payload) || !payload.length) break;
724
- let lastTs = cursor;
725
- for (const item of payload) {
726
- if (!Array.isArray(item)) continue;
727
- const ts = toNum(item[0], 0);
728
- if (!ts) continue;
729
- rows.push({
730
- timestamp: ts,
731
- open: toNum(item[1]),
732
- high: toNum(item[2]),
733
- low: toNum(item[3]),
734
- close: toNum(item[4]),
735
- volume: toNum(item[5]),
736
- turnover: toNum(item[7]),
737
- dt: new Date(ts).toISOString()
738
- });
739
- lastTs = ts;
740
- }
741
- if (payload.length < 1e3) break;
742
- cursor = lastTs + intervalMs;
743
- }
744
- rows.sort((a, b) => a.timestamp - b.timestamp);
745
- return rows;
746
- },
1045
+ kline: createTimescaleCachedKline({
1046
+ provider: "binance",
1047
+ request: requestKline,
1048
+ intervalToMinutes
1049
+ }),
747
1050
  getPosition: async () => null,
748
1051
  getPositions: async () => [],
1052
+ getOpenPositionPnl: async () => [],
749
1053
  placeOrder: async () => false,
1054
+ setTakeProfits: async () => false,
1055
+ setStopLoss: async () => false,
750
1056
  closePosition: async () => false,
751
1057
  getTickers: async () => {
752
1058
  const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
@@ -827,6 +1133,10 @@ var toNum2 = (value, fallback = 0) => {
827
1133
  const num = Number(value);
828
1134
  return Number.isFinite(num) ? num : fallback;
829
1135
  };
1136
+ var intervalToMinutes2 = (interval) => {
1137
+ const intervalMs = INTERVAL_MS2[String(interval)];
1138
+ return intervalMs ? Math.floor(intervalMs / 6e4) : null;
1139
+ };
830
1140
  var toCoinbaseProduct = (symbol) => {
831
1141
  const upper = symbol.toUpperCase();
832
1142
  const quoteSuffixes = ["USDT", "USDC", "BUSD", "USD"];
@@ -853,59 +1163,73 @@ var MAJOR_PRODUCTS = [
853
1163
  ];
854
1164
  var CoinbaseConnectorCreator = async () => {
855
1165
  let state = {};
1166
+ const requestKline = async ({
1167
+ symbol,
1168
+ interval,
1169
+ start,
1170
+ end
1171
+ }) => {
1172
+ const granularity = GRANULARITY_MAP[String(interval)];
1173
+ if (!granularity) return [];
1174
+ const product = toCoinbaseProduct(symbol);
1175
+ if (!product) return [];
1176
+ const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
1177
+ const normalizedEnd = end ?? Date.now();
1178
+ const stepMs = granularity * 1e3 * 300;
1179
+ const fromMs = start ?? Math.max(0, normalizedEnd - INTERVAL_MS2[String(interval)] * 1e3);
1180
+ let cursorEnd = normalizedEnd;
1181
+ const rows = [];
1182
+ while (cursorEnd >= fromMs) {
1183
+ const chunkStart = Math.max(fromMs, cursorEnd - stepMs);
1184
+ const url = new URL(`${baseUrl}/products/${product}/candles`);
1185
+ url.searchParams.set("granularity", String(granularity));
1186
+ url.searchParams.set("start", new Date(chunkStart).toISOString());
1187
+ url.searchParams.set("end", new Date(cursorEnd).toISOString());
1188
+ const response = await (0, import_http2.fetchWithRetry)(url.toString(), {
1189
+ headers: { "User-Agent": "tradejs/coinbase-connector" }
1190
+ });
1191
+ if (!response.ok) break;
1192
+ const payload = await response.json();
1193
+ if (Array.isArray(payload)) {
1194
+ for (const item of payload) {
1195
+ if (!Array.isArray(item)) continue;
1196
+ const ts = toNum2(item[0]) * 1e3;
1197
+ rows.push({
1198
+ timestamp: ts,
1199
+ low: toNum2(item[1]),
1200
+ high: toNum2(item[2]),
1201
+ open: toNum2(item[3]),
1202
+ close: toNum2(item[4]),
1203
+ volume: toNum2(item[5]),
1204
+ turnover: 0,
1205
+ dt: new Date(ts).toISOString()
1206
+ });
1207
+ }
1208
+ }
1209
+ cursorEnd = chunkStart - 1;
1210
+ }
1211
+ const dedup = /* @__PURE__ */ new Map();
1212
+ for (const row of rows) {
1213
+ dedup.set(row.timestamp, row);
1214
+ }
1215
+ return [...dedup.values()].sort((a, b) => a.timestamp - b.timestamp);
1216
+ };
856
1217
  return {
857
1218
  getState: async () => state,
858
1219
  setState: async (newState) => {
859
1220
  state = { ...state, ...newState };
860
1221
  },
861
- kline: async ({ symbol, interval, start, end }) => {
862
- const granularity = GRANULARITY_MAP[String(interval)];
863
- if (!granularity) return [];
864
- const product = toCoinbaseProduct(symbol);
865
- if (!product) return [];
866
- const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
867
- const stepMs = granularity * 1e3 * 300;
868
- const fromMs = start ?? Math.max(0, end - INTERVAL_MS2[String(interval)] * 1e3);
869
- let cursorEnd = end;
870
- const rows = [];
871
- while (cursorEnd >= fromMs) {
872
- const chunkStart = Math.max(fromMs, cursorEnd - stepMs);
873
- const url = new URL(`${baseUrl}/products/${product}/candles`);
874
- url.searchParams.set("granularity", String(granularity));
875
- url.searchParams.set("start", new Date(chunkStart).toISOString());
876
- url.searchParams.set("end", new Date(cursorEnd).toISOString());
877
- const response = await (0, import_http2.fetchWithRetry)(url.toString(), {
878
- headers: { "User-Agent": "tradejs/coinbase-connector" }
879
- });
880
- if (!response.ok) break;
881
- const payload = await response.json();
882
- if (Array.isArray(payload)) {
883
- for (const item of payload) {
884
- if (!Array.isArray(item)) continue;
885
- const ts = toNum2(item[0]) * 1e3;
886
- rows.push({
887
- timestamp: ts,
888
- low: toNum2(item[1]),
889
- high: toNum2(item[2]),
890
- open: toNum2(item[3]),
891
- close: toNum2(item[4]),
892
- volume: toNum2(item[5]),
893
- turnover: 0,
894
- dt: new Date(ts).toISOString()
895
- });
896
- }
897
- }
898
- cursorEnd = chunkStart - 1;
899
- }
900
- const dedup = /* @__PURE__ */ new Map();
901
- for (const row of rows) {
902
- dedup.set(row.timestamp, row);
903
- }
904
- return [...dedup.values()].sort((a, b) => a.timestamp - b.timestamp);
905
- },
1222
+ kline: createTimescaleCachedKline({
1223
+ provider: "coinbase",
1224
+ request: requestKline,
1225
+ intervalToMinutes: intervalToMinutes2
1226
+ }),
906
1227
  getPosition: async () => null,
907
1228
  getPositions: async () => [],
1229
+ getOpenPositionPnl: async () => [],
908
1230
  placeOrder: async () => false,
1231
+ setTakeProfits: async () => false,
1232
+ setStopLoss: async () => false,
909
1233
  closePosition: async () => false,
910
1234
  getTickers: async () => {
911
1235
  const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
@@ -1117,7 +1441,7 @@ var binanceCoinbaseSpreadProvider = {
1117
1441
  };
1118
1442
 
1119
1443
  // src/marketData/providers/coinalyze.ts
1120
- var import_async = require("@tradejs/core/async");
1444
+ var import_async2 = require("@tradejs/core/async");
1121
1445
  var import_indicators2 = require("@tradejs/core/indicators");
1122
1446
  var coinalyzeIntervalMap = {
1123
1447
  "15m": "15min",
@@ -1168,12 +1492,8 @@ var normalizeMetricPayload = (metric, raw) => {
1168
1492
  }));
1169
1493
  };
1170
1494
  var fetchCoinalyzeSeries = async (params) => {
1171
- const { endpoint, metric, symbol, interval, fromMs, toMs } = params;
1495
+ const { endpoint, metric, symbol, apiKey, interval, fromMs, toMs } = params;
1172
1496
  const baseUrl = process.env.COINALYZE_BASE_URL?.trim() || "https://api.coinalyze.net/v1";
1173
- const apiKey = process.env.COINALYZE_API_KEY?.trim();
1174
- if (!apiKey) {
1175
- throw new Error("Missing COINALYZE_API_KEY");
1176
- }
1177
1497
  const url = new URL(`${baseUrl}${endpoint}`);
1178
1498
  url.searchParams.set("symbols", symbol);
1179
1499
  url.searchParams.set("interval", coinalyzeIntervalMap[interval] || interval);
@@ -1191,7 +1511,7 @@ var fetchCoinalyzeSeries = async (params) => {
1191
1511
  lastRequestTs + coinalyzeMinRequestDelayMs - now
1192
1512
  );
1193
1513
  if (waitMs > 0) {
1194
- await (0, import_async.delay)(waitMs);
1514
+ await (0, import_async2.delay)(waitMs);
1195
1515
  }
1196
1516
  lastRequestTs = Date.now();
1197
1517
  const response = await fetch(url.toString(), { headers });
@@ -1205,7 +1525,7 @@ var fetchCoinalyzeSeries = async (params) => {
1205
1525
  const transient = response.status === 429 || response.status >= 500;
1206
1526
  if (attempt < coinalyzeMaxRetries && transient) {
1207
1527
  const backoffMs = Math.min(1e4, 750 * 2 ** attempt);
1208
- await (0, import_async.delay)(retryAfterMs ?? backoffMs);
1528
+ await (0, import_async2.delay)(retryAfterMs ?? backoffMs);
1209
1529
  continue;
1210
1530
  }
1211
1531
  throw new Error(`Coinalyze ${endpoint} ${response.status}: ${text}`);
@@ -1214,15 +1534,27 @@ var fetchCoinalyzeSeries = async (params) => {
1214
1534
  };
1215
1535
  var coinalyzeProvider = {
1216
1536
  name: "coinalyze",
1217
- fetchWindow: async ({ symbol, marketSymbol, interval, fromMs, toMs }) => {
1537
+ fetchWindow: async ({
1538
+ symbol,
1539
+ marketSymbol,
1540
+ apiKey,
1541
+ interval,
1542
+ fromMs,
1543
+ toMs
1544
+ }) => {
1545
+ if (!apiKey?.trim()) {
1546
+ throw new Error("Missing COINALYZE_API_KEY in user settings");
1547
+ }
1218
1548
  const oiPath = process.env.COINALYZE_OI_PATH?.trim() || "/open-interest-history";
1219
1549
  const fundingPath = process.env.COINALYZE_FUNDING_PATH?.trim() || "/funding-rate-history";
1220
1550
  const liqPath = process.env.COINALYZE_LIQ_PATH?.trim() || "/liquidation-history";
1221
1551
  const requestSymbol = (marketSymbol || symbol).trim().toUpperCase();
1552
+ const normalizedApiKey = apiKey.trim();
1222
1553
  const oiRaw = await fetchCoinalyzeSeries({
1223
1554
  endpoint: oiPath,
1224
1555
  metric: "oi",
1225
1556
  symbol: requestSymbol,
1557
+ apiKey: normalizedApiKey,
1226
1558
  interval,
1227
1559
  fromMs,
1228
1560
  toMs
@@ -1231,6 +1563,7 @@ var coinalyzeProvider = {
1231
1563
  endpoint: fundingPath,
1232
1564
  metric: "funding",
1233
1565
  symbol: requestSymbol,
1566
+ apiKey: normalizedApiKey,
1234
1567
  interval,
1235
1568
  fromMs,
1236
1569
  toMs
@@ -1239,6 +1572,7 @@ var coinalyzeProvider = {
1239
1572
  endpoint: liqPath,
1240
1573
  metric: "liq",
1241
1574
  symbol: requestSymbol,
1575
+ apiKey: normalizedApiKey,
1242
1576
  interval,
1243
1577
  fromMs,
1244
1578
  toMs