@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.mjs CHANGED
@@ -1,29 +1,29 @@
1
1
  // src/ByBit/index.ts
2
2
  import _ from "lodash";
3
3
  import chalk from "chalk";
4
+ import { delay } from "@tradejs/core/async";
4
5
  import {
5
6
  MARKET_CATEGORY as MARKET_CATEGORY2,
6
- PRELOAD_FALLBACK_DAYS
7
+ PRELOAD_FALLBACK_DAYS as PRELOAD_FALLBACK_DAYS2
7
8
  } from "@tradejs/core/constants";
8
- import { mergeData } from "@tradejs/core/data";
9
9
  import { toJson } from "@tradejs/core/data";
10
10
  import { round } from "@tradejs/core/math";
11
11
  import { normalizeTickerData } from "@tradejs/core/tickers";
12
- import { formatUnix as formatUnix2, getItemTimestamp, getTimestamp } from "@tradejs/core/time";
13
- import { logger as logger2 } from "@tradejs/infra/logger";
14
- import {
15
- getCandlesRange,
16
- getDataEdges,
17
- upsertCandles,
18
- toRows
19
- } 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";
20
14
 
21
15
  // src/ByBit/client.ts
22
16
  import { RestClientV5 } from "bybit-api";
23
17
  import { logger } from "@tradejs/infra/logger";
24
18
  import { getData, redisKeys } from "@tradejs/infra/redis";
25
19
  var useTestnet = false;
26
- var getClient = async ({ userName }) => {
20
+ var getClient = async ({ userName }, access = "private") => {
21
+ if (access === "public") {
22
+ return new RestClientV5({
23
+ parseAPIRateLimits: true,
24
+ testnet: useTestnet
25
+ });
26
+ }
27
27
  const user = await getData(redisKeys.user(userName));
28
28
  if (!user) {
29
29
  logger.log("error", "connection config not found: %s", userName);
@@ -32,11 +32,248 @@ var getClient = async ({ userName }) => {
32
32
  const client = new RestClientV5({
33
33
  key: user.BYBIT_API_KEY,
34
34
  secret: user.BYBIT_API_SECRET,
35
+ parseAPIRateLimits: true,
35
36
  testnet: useTestnet
36
37
  });
37
38
  return client;
38
39
  };
39
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
+
40
277
  // src/ByBit/utils.ts
41
278
  import { MARKET_CATEGORY } from "@tradejs/core/constants";
42
279
  import { formatUnix } from "@tradejs/core/time";
@@ -130,7 +367,12 @@ var mapPositionData = (data) => {
130
367
 
131
368
  // src/ByBit/index.ts
132
369
  var LIMIT = 1e3;
133
- var CACHE_FALLBACK_WINDOW = 1e3;
370
+ var BYBIT_RATE_LIMIT_RETCODE = 10006;
371
+ var BYBIT_TRADING_STOP_NOT_MODIFIED_RETCODE = 34040;
372
+ var KLINE_RATE_LIMIT_MAX_ATTEMPTS = 3;
373
+ var KLINE_RATE_LIMIT_BASE_DELAY_MS = 800;
374
+ var KLINE_RATE_LIMIT_MAX_DELAY_MS = 1e4;
375
+ var KLINE_RATE_LIMIT_RESET_BUFFER_MS = 50;
134
376
  var INTERVAL_TO_MINUTES = {
135
377
  "1": 1,
136
378
  "3": 3,
@@ -147,9 +389,36 @@ var INTERVAL_TO_MINUTES = {
147
389
  M: 43200
148
390
  };
149
391
  var getLogLevel = (res) => res.retCode === 0 ? "info" : "error";
392
+ var isTradingStopNotModified = (res) => res?.retCode === BYBIT_TRADING_STOP_NOT_MODIFIED_RETCODE;
393
+ var isTradingStopAccepted = (res) => res?.retCode === 0 || isTradingStopNotModified(res);
394
+ var isKlineRateLimited = (res) => res?.retCode === BYBIT_RATE_LIMIT_RETCODE;
395
+ var resolveKlineRetryDelayMs = (res, attempt) => {
396
+ const resetAtRaw = Number(res?.rateLimitApi?.resetAtTimestamp);
397
+ if (Number.isFinite(resetAtRaw) && resetAtRaw > 0) {
398
+ return Math.max(
399
+ KLINE_RATE_LIMIT_RESET_BUFFER_MS,
400
+ resetAtRaw - Date.now() + KLINE_RATE_LIMIT_RESET_BUFFER_MS
401
+ );
402
+ }
403
+ const jitterMs = Math.round(Math.random() * 250);
404
+ const backoffMs = Math.min(
405
+ KLINE_RATE_LIMIT_MAX_DELAY_MS,
406
+ KLINE_RATE_LIMIT_BASE_DELAY_MS * 2 ** (attempt - 1)
407
+ );
408
+ return backoffMs + jitterMs;
409
+ };
150
410
  var ByBitConnectorCreator = async (config) => {
151
411
  let state = {};
152
- let isTimescaleFallbackMode = false;
412
+ let publicClientPromise = null;
413
+ let privateClientPromise = null;
414
+ const getPublicClient = async () => {
415
+ publicClientPromise ??= getClient(config, "public");
416
+ return publicClientPromise;
417
+ };
418
+ const getPrivateClient = async () => {
419
+ privateClientPromise ??= getClient(config, "private");
420
+ return privateClientPromise;
421
+ };
153
422
  const request = async ({
154
423
  symbol,
155
424
  interval,
@@ -158,48 +427,67 @@ var ByBitConnectorCreator = async (config) => {
158
427
  silent
159
428
  }) => {
160
429
  const normalizedStart = round(
161
- start || getTimestamp(PRELOAD_FALLBACK_DAYS),
430
+ start || getTimestamp2(PRELOAD_FALLBACK_DAYS2),
162
431
  0
163
432
  );
164
433
  const normalizedEnd = round(end || Date.now());
165
434
  try {
166
- const client = await getClient(config);
435
+ const client = await getPublicClient();
167
436
  if (!client) return [];
168
437
  if (normalizedEnd <= normalizedStart) {
169
438
  return [];
170
439
  }
171
- const kline = await client.getKline({
172
- category: MARKET_CATEGORY2,
173
- symbol,
174
- interval,
175
- start: normalizedStart,
176
- end: normalizedEnd,
177
- limit: LIMIT
178
- });
179
- if (!kline?.result?.list) {
180
- 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}` : "";
181
- logger2.log(
182
- "error",
183
- "empty kline.list for %s %s%s",
440
+ for (let attempt = 1; attempt <= KLINE_RATE_LIMIT_MAX_ATTEMPTS; attempt += 1) {
441
+ const kline = await client.getKline({
442
+ category: MARKET_CATEGORY2,
184
443
  symbol,
185
444
  interval,
186
- responseError ? `: ${responseError}` : ""
187
- );
188
- return [];
189
- }
190
- if (!silent) {
191
- logger2.log(
192
- "info",
193
- "%s %s %s %s",
194
- chalk.yellow(formatUnix2(normalizedEnd)),
195
- chalk.cyan(symbol),
196
- chalk.cyan(interval),
197
- chalk.yellow(kline.result.list.length)
198
- );
445
+ start: normalizedStart,
446
+ end: normalizedEnd,
447
+ limit: LIMIT
448
+ });
449
+ if (isKlineRateLimited(kline)) {
450
+ if (attempt < KLINE_RATE_LIMIT_MAX_ATTEMPTS) {
451
+ const waitMs = resolveKlineRetryDelayMs(kline, attempt);
452
+ logger3.log(
453
+ "warn",
454
+ "kline rate limited for %s %s: attempt=%s/%s waitMs=%s",
455
+ symbol,
456
+ interval,
457
+ attempt,
458
+ KLINE_RATE_LIMIT_MAX_ATTEMPTS,
459
+ waitMs
460
+ );
461
+ await delay(waitMs);
462
+ continue;
463
+ }
464
+ }
465
+ if (!kline?.result?.list) {
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}` : "";
467
+ logger3.log(
468
+ "error",
469
+ "empty kline.list for %s %s%s",
470
+ symbol,
471
+ interval,
472
+ responseError ? `: ${responseError}` : ""
473
+ );
474
+ return [];
475
+ }
476
+ if (!silent && (process.stdout.isTTY || process.env.TRADEJS_LOG_KLINE_REQUESTS === "1")) {
477
+ logger3.log(
478
+ "info",
479
+ "%s %s %s %s",
480
+ chalk.yellow(formatUnix2(normalizedEnd)),
481
+ chalk.cyan(symbol),
482
+ chalk.cyan(interval),
483
+ chalk.yellow(kline.result.list.length)
484
+ );
485
+ }
486
+ return mapKlineToChartData(kline.result.list.reverse());
199
487
  }
200
- return mapKlineToChartData(kline.result.list.reverse());
488
+ return [];
201
489
  } catch (error) {
202
- logger2.log(
490
+ logger3.log(
203
491
  "error",
204
492
  "request kline: %s %s %s",
205
493
  normalizedStart,
@@ -209,245 +497,230 @@ var ByBitConnectorCreator = async (config) => {
209
497
  return [];
210
498
  }
211
499
  };
212
- const loadData = async (direction, pointer, limitBoundary, requestParams, intervalMs) => {
213
- if (pointer === void 0) return [];
214
- let accumulated = [];
215
- let fulfilled = false;
216
- while (!fulfilled) {
217
- const currentPointer = pointer;
218
- const params = {
219
- symbol: requestParams.symbol,
220
- interval: requestParams.interval,
221
- silent: requestParams.silent
222
- };
223
- if (direction === "older") {
224
- params.end = pointer;
225
- if (limitBoundary !== void 0) params.start = limitBoundary;
226
- } else {
227
- params.start = pointer;
228
- if (limitBoundary !== void 0) params.end = limitBoundary;
229
- }
230
- const partData = await request(params);
231
- if (_.isEmpty(partData)) {
232
- fulfilled = true;
233
- break;
500
+ const intervalToMinutes3 = (interval) => {
501
+ return INTERVAL_TO_MINUTES[String(interval)] ?? null;
502
+ };
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) {
513
+ logger3.log(
514
+ getLogLevel(positionRes),
515
+ "position retCode: %s, %s",
516
+ symbol,
517
+ positionRes.retCode
518
+ );
519
+ return null;
520
+ }
521
+ const positions = mapPositionData(positionRes.result.list);
522
+ if (!positions || _.isEmpty(positions)) {
523
+ return null;
524
+ }
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
+ };
537
+ };
538
+ const setTakeProfits = async ({
539
+ symbol,
540
+ direction,
541
+ qty,
542
+ takeProfits
543
+ }) => {
544
+ const client = await getPrivateClient();
545
+ const marketDataClient = await getPublicClient();
546
+ if (!client || !marketDataClient) {
547
+ return false;
548
+ }
549
+ if (!Array.isArray(takeProfits) || takeProfits.length === 0) {
550
+ return true;
551
+ }
552
+ const meta = await getSymbolMeta(marketDataClient, symbol);
553
+ const positionQty = qty ?? (await getPositionSnapshot(symbol))?.qty ?? 0;
554
+ if (!Number.isFinite(positionQty) || positionQty <= 0) {
555
+ logger3.log(
556
+ "warn",
557
+ "setTakeProfits: missing position qty: %s",
558
+ toJson({ symbol, qty, positionQty, takeProfits }, true)
559
+ );
560
+ return false;
561
+ }
562
+ const isLong = direction === "LONG";
563
+ for (const tp of takeProfits) {
564
+ const tpSizeRaw = positionQty * tp.rate;
565
+ const { qtyNum: tpSizeNum, qtyStr: tpSizeStr } = normalizeQty(
566
+ tpSizeRaw,
567
+ meta
568
+ );
569
+ if (!tpSizeNum || tpSizeNum < meta.minOrderQty) {
570
+ logger3.log(
571
+ "warn",
572
+ "tp skipped: size too small %s",
573
+ toJson(
574
+ { symbol, tp, tpSizeNum, minOrderQty: meta.minOrderQty },
575
+ true
576
+ )
577
+ );
578
+ continue;
234
579
  }
235
- accumulated = direction === "older" ? mergeData(partData, accumulated) : mergeData(accumulated, partData);
236
- const boundaryReached = limitBoundary !== void 0 && (direction === "older" && currentPointer <= limitBoundary || direction === "newer" && currentPointer >= limitBoundary);
237
- if (partData.length < LIMIT || boundaryReached) {
238
- fulfilled = true;
239
- break;
580
+ const tpPriceNorm = normalizePrice(
581
+ tp.price,
582
+ isLong ? "TP_LONG" : "TP_SHORT",
583
+ meta
584
+ );
585
+ const isFullMode = takeProfits.length === 1 && tp.rate === 1;
586
+ const tpRes = await client.setTradingStop({
587
+ category: MARKET_CATEGORY2,
588
+ symbol,
589
+ tpSize: isFullMode ? void 0 : tpSizeStr,
590
+ tpslMode: isFullMode ? "Full" : "Partial",
591
+ takeProfit: tpPriceNorm.priceStr,
592
+ tpTriggerBy: "MarkPrice",
593
+ tpOrderType: "Market",
594
+ positionIdx: 0
595
+ });
596
+ if (isTradingStopNotModified(tpRes)) {
597
+ logger3.log(
598
+ "debug",
599
+ "tp unchanged: %s %s price=%s rate=%s",
600
+ symbol,
601
+ direction,
602
+ tpPriceNorm.priceStr,
603
+ tp.rate
604
+ );
605
+ } else if (tpRes.retCode === 0) {
606
+ logger3.log(
607
+ "info",
608
+ "tp updated: %s %s price=%s rate=%s",
609
+ symbol,
610
+ direction,
611
+ tpPriceNorm.priceStr,
612
+ tp.rate
613
+ );
614
+ } else {
615
+ logger3.log(
616
+ "error",
617
+ "tp failed: %s %s price=%s rate=%s %s",
618
+ symbol,
619
+ direction,
620
+ tpPriceNorm.priceStr,
621
+ tp.rate,
622
+ toJson(tpRes, true)
623
+ );
240
624
  }
241
- const nextPointer = direction === "older" ? getItemTimestamp(partData[0]) - intervalMs : getItemTimestamp(partData[partData.length - 1]) + intervalMs;
242
- if (!Number.isFinite(nextPointer) || nextPointer === currentPointer) {
243
- fulfilled = true;
244
- break;
625
+ if (!isTradingStopAccepted(tpRes)) {
626
+ return false;
245
627
  }
246
- pointer = nextPointer;
247
628
  }
248
- return accumulated;
249
- };
250
- const intervalMsOf = (interval) => interval * 6e4;
251
- const intervalToMinutes = (interval) => {
252
- return INTERVAL_TO_MINUTES[String(interval)] ?? null;
629
+ return true;
253
630
  };
254
- const clampToClosedCandle = (value, intervalMs) => Math.floor(value / intervalMs) * intervalMs;
255
- const normalizeRangeToClosed = (intervalMs, start, end) => {
256
- const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
257
- const normStart = start !== void 0 ? clampToClosedCandle(start, intervalMs) : 0;
258
- const cappedEnd = Math.min(end ?? Date.now(), lastClosed);
259
- const normEnd = clampToClosedCandle(cappedEnd, intervalMs);
260
- return { normStart, normEnd, lastClosed };
261
- };
262
- const rowsToKline = (rows) => rows.map(({ ts, ...data }) => ({
263
- timestamp: new Date(ts).getTime(),
264
- ...data
265
- }));
266
- const refreshTail = async ({
631
+ const setStopLoss = async ({
267
632
  symbol,
268
- interval,
269
- silent,
270
- tailCount = 2
633
+ direction,
634
+ stopLossPrice
271
635
  }) => {
272
- const intMinutes = intervalToMinutes(interval);
273
- if (!intMinutes) {
274
- logger2.log("error", "refreshTail: invalid interval %s", interval);
275
- return;
636
+ const client = await getPrivateClient();
637
+ const marketDataClient = await getPublicClient();
638
+ if (!client || !marketDataClient) {
639
+ return false;
276
640
  }
277
- const intervalMs = intervalMsOf(intMinutes);
278
- const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
279
- const tailEnd = lastClosed + intervalMs;
280
- const tailStart = tailEnd - tailCount * intervalMs;
281
- const part = await request({
641
+ if (typeof stopLossPrice !== "number" || !Number.isFinite(stopLossPrice)) {
642
+ return true;
643
+ }
644
+ const meta = await getSymbolMeta(marketDataClient, symbol);
645
+ const isLong = direction === "LONG";
646
+ const slNormalized = normalizePrice(
647
+ stopLossPrice,
648
+ isLong ? "SL_LONG" : "SL_SHORT",
649
+ meta
650
+ );
651
+ const slRes = await client.setTradingStop({
652
+ category: MARKET_CATEGORY2,
282
653
  symbol,
283
- interval,
284
- start: tailStart,
285
- end: tailEnd,
286
- silent
654
+ tpslMode: "Full",
655
+ stopLoss: slNormalized.priceStr,
656
+ slTriggerBy: "LastPrice",
657
+ positionIdx: 0
287
658
  });
288
- if (part.length) {
289
- await upsertCandles(toRows(symbol, intMinutes, part));
659
+ if (isTradingStopNotModified(slRes)) {
660
+ logger3.log(
661
+ "debug",
662
+ "sl unchanged: %s %s stopLoss=%s",
663
+ symbol,
664
+ direction,
665
+ slNormalized.priceStr
666
+ );
667
+ } else if (slRes.retCode === 0) {
668
+ logger3.log(
669
+ "info",
670
+ "sl updated: %s %s stopLoss=%s",
671
+ symbol,
672
+ direction,
673
+ slNormalized.priceStr
674
+ );
675
+ } else {
676
+ logger3.log(
677
+ "error",
678
+ "sl failed: %s %s stopLoss=%s %s",
679
+ symbol,
680
+ direction,
681
+ slNormalized.priceStr,
682
+ toJson(slRes, true)
683
+ );
290
684
  }
685
+ return isTradingStopAccepted(slRes);
291
686
  };
292
687
  return {
293
688
  getState: async () => state,
294
689
  setState: async (newState) => {
295
690
  state = { ...state, ...newState };
296
691
  },
297
- kline: async ({
298
- symbol,
299
- interval,
300
- start: defaultStart,
301
- end: defaultEnd,
302
- silent = false,
303
- cacheOnly = false
304
- }) => {
305
- const intMinutes = intervalToMinutes(interval);
306
- if (!intMinutes) {
307
- logger2.log("error", "kline: invalid interval %s", interval);
308
- return [];
309
- }
310
- if (defaultStart !== void 0 && defaultEnd !== void 0 && defaultEnd <= defaultStart) {
311
- return [];
312
- }
313
- const intervalMs = intervalMsOf(intMinutes);
314
- try {
315
- const edges = await getDataEdges(symbol, intMinutes);
316
- let dataStart = edges.min;
317
- let dataEnd = edges.max;
318
- const { normStart, normEnd } = normalizeRangeToClosed(
319
- intervalMs,
320
- defaultStart,
321
- defaultEnd
322
- );
323
- if (cacheOnly) {
324
- const base = edges.max ?? Date.now();
325
- const s = Math.max(
326
- defaultStart ?? base - CACHE_FALLBACK_WINDOW * intervalMs,
327
- 0
328
- );
329
- const e = defaultEnd ?? base;
330
- const dbData2 = await getCandlesRange(symbol, intMinutes, s, e);
331
- return rowsToKline(dbData2);
332
- }
333
- const needOlderData = defaultStart !== void 0 && (dataStart === void 0 || normStart < dataStart);
334
- const needNewerData = defaultEnd !== void 0 && (dataEnd === void 0 || normEnd > dataEnd);
335
- if (needOlderData) {
336
- const pointerForOlder = dataStart ?? normEnd ?? Date.now();
337
- const olderData = await loadData(
338
- "older",
339
- pointerForOlder,
340
- normStart,
341
- {
342
- symbol,
343
- interval,
344
- silent,
345
- start: normStart,
346
- end: pointerForOlder
347
- },
348
- intervalMs
349
- );
350
- if (olderData.length) {
351
- await upsertCandles(toRows(symbol, intMinutes, olderData));
352
- dataStart = normStart;
353
- }
354
- }
355
- if (needNewerData) {
356
- const pointerForNewer = dataEnd ?? normStart ?? 0;
357
- const newerData = await loadData(
358
- "newer",
359
- pointerForNewer,
360
- normEnd,
361
- {
362
- symbol,
363
- interval,
364
- silent,
365
- start: pointerForNewer,
366
- end: normEnd
367
- },
368
- intervalMs
369
- );
370
- if (newerData.length) {
371
- await upsertCandles(toRows(symbol, intMinutes, newerData));
372
- dataEnd = normEnd;
373
- }
374
- }
375
- const isRightEdgeQuery = defaultEnd === void 0 || defaultEnd && defaultEnd >= Date.now() - intervalMs;
376
- if (!cacheOnly && isRightEdgeQuery) {
377
- await refreshTail({ symbol, interval, silent });
378
- }
379
- const rangeStart = defaultStart ?? dataStart ?? 0;
380
- const rangeEnd = defaultEnd ?? dataEnd ?? Date.now();
381
- const { normStart: finalStart, normEnd: finalEnd } = normalizeRangeToClosed(intervalMs, rangeStart, rangeEnd);
382
- const dbData = await getCandlesRange(
383
- symbol,
384
- intMinutes,
385
- finalStart,
386
- finalEnd
387
- );
388
- if (isTimescaleFallbackMode) {
389
- isTimescaleFallbackMode = false;
390
- logger2.log("info", "TimescaleDB connection restored for kline cache");
391
- }
392
- return rowsToKline(dbData);
393
- } catch (error) {
394
- if (!isTimescaleFallbackMode) {
395
- isTimescaleFallbackMode = true;
396
- logger2.log(
397
- "warn",
398
- "TimescaleDB unavailable for %s %s: %s. Falling back to exchange API.",
399
- symbol,
400
- interval,
401
- String(error)
402
- );
403
- }
404
- if (cacheOnly) {
405
- return [];
406
- }
407
- return request({
408
- symbol,
409
- interval,
410
- start: defaultStart,
411
- end: defaultEnd,
412
- silent
413
- });
414
- }
415
- },
416
- getPosition: async (symbol) => {
417
- const client = await getClient(config);
692
+ kline: createTimescaleCachedKline({
693
+ provider: "bybit",
694
+ request,
695
+ intervalToMinutes: intervalToMinutes3,
696
+ limit: LIMIT
697
+ }),
698
+ getPosition: async (symbol) => getPositionSnapshot(symbol),
699
+ getPositions: async () => {
700
+ const client = await getPrivateClient();
418
701
  if (!client) {
419
- return null;
702
+ return [];
420
703
  }
421
704
  const positionRes = await client.getPositionInfo({
422
- symbol,
423
- category: MARKET_CATEGORY2
705
+ category: MARKET_CATEGORY2,
706
+ settleCoin: "USDT"
424
707
  });
425
- logger2.log(
426
- getLogLevel(positionRes),
427
- "position retCode: %s, %s",
428
- symbol,
429
- positionRes.retCode
430
- );
431
708
  if (positionRes.retCode !== 0) {
432
- return null;
709
+ logger3.log(
710
+ getLogLevel(positionRes),
711
+ "positions retCode: %s, %s",
712
+ positionRes.retCode
713
+ );
714
+ return [];
433
715
  }
434
716
  const positions = mapPositionData(positionRes.result.list);
435
717
  if (!positions || _.isEmpty(positions)) {
436
- return null;
718
+ return [];
437
719
  }
438
- const position = positions[0];
439
- logger2.log(
440
- getLogLevel(positionRes),
441
- "position: %s, %s",
442
- symbol,
443
- toJson(positionRes, true)
444
- );
445
- return {
446
- ...position
447
- };
720
+ return positions;
448
721
  },
449
- getPositions: async () => {
450
- const client = await getClient(config);
722
+ getOpenPositionPnl: async () => {
723
+ const client = await getPrivateClient();
451
724
  if (!client) {
452
725
  return [];
453
726
  }
@@ -455,30 +728,95 @@ var ByBitConnectorCreator = async (config) => {
455
728
  category: MARKET_CATEGORY2,
456
729
  settleCoin: "USDT"
457
730
  });
458
- logger2.log(
459
- getLogLevel(positionRes),
460
- "positions retCode: %s, %s",
461
- positionRes.retCode
462
- );
463
731
  if (positionRes.retCode !== 0) {
732
+ logger3.log(
733
+ getLogLevel(positionRes),
734
+ "positions pnl retCode: %s, %s",
735
+ positionRes.retCode
736
+ );
464
737
  return [];
465
738
  }
466
- const positions = mapPositionData(positionRes.result.list);
467
- if (!positions || _.isEmpty(positions)) {
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) {
468
768
  return [];
469
769
  }
470
- return positions;
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);
471
808
  },
472
- placeOrder: async ({ symbol, price, qty, direction, isLimit }, TP = [], slPrice) => {
473
- const client = await getClient(config);
474
- if (!client) {
809
+ placeOrder: async ({ symbol, price, qty, direction, isLimit, orderId }) => {
810
+ const client = await getPrivateClient();
811
+ const marketDataClient = await getPublicClient();
812
+ if (!client || !marketDataClient) {
475
813
  return false;
476
814
  }
477
815
  const isLong = direction === "LONG";
478
- const meta = await getSymbolMeta(client, symbol);
816
+ const meta = await getSymbolMeta(marketDataClient, symbol);
479
817
  const { qtyNum: orderQty, qtyStr: orderQtyStr } = normalizeQty(qty, meta);
480
818
  if (orderQty < meta.minOrderQty) {
481
- logger2.log(
819
+ logger3.log(
482
820
  "warn",
483
821
  "placeOrder: qty too small: %s",
484
822
  toJson(
@@ -489,10 +827,7 @@ var ByBitConnectorCreator = async (config) => {
489
827
  return false;
490
828
  }
491
829
  const entryNormalized = isLimit ? normalizePrice(price, "ENTRY", meta) : void 0;
492
- const slNormalized = slPrice ? normalizePrice(slPrice, isLong ? "SL_LONG" : "SL_SHORT", meta) : void 0;
493
- const firstTP = TP?.[0];
494
- const tpNormalized = firstTP && firstTP.rate === 1 ? normalizePrice(firstTP.price, isLong ? "TP_LONG" : "TP_SHORT", meta) : void 0;
495
- logger2.log(
830
+ logger3.log(
496
831
  "info",
497
832
  "placeOrder: %s",
498
833
  toJson(
@@ -502,10 +837,7 @@ var ByBitConnectorCreator = async (config) => {
502
837
  qty,
503
838
  direction,
504
839
  orderQty,
505
- orderQtyStr,
506
- slPrice,
507
- slPriceNorm: slNormalized?.priceStr,
508
- TP
840
+ orderQtyStr
509
841
  },
510
842
  true
511
843
  )
@@ -520,16 +852,13 @@ var ByBitConnectorCreator = async (config) => {
520
852
  category: MARKET_CATEGORY2,
521
853
  symbol,
522
854
  price: entryNormalized?.priceStr || void 0,
523
- takeProfit: tpNormalized?.priceStr || void 0,
524
- tpTriggerBy: "MarkPrice",
525
- stopLoss: slNormalized?.priceStr || void 0,
526
- slTriggerBy: "LastPrice",
527
855
  side: isLong ? "Buy" : "Sell",
528
856
  orderType: isLimit ? "Limit" : "Market",
529
857
  qty: orderQtyStr,
530
- orderFilter: "Order"
858
+ orderFilter: "Order",
859
+ orderLinkId: typeof orderId === "string" && orderId.trim() ? orderId : void 0
531
860
  });
532
- logger2.log(
861
+ logger3.log(
533
862
  getLogLevel(orderRes),
534
863
  "placeOrder:response: %s",
535
864
  toJson(orderRes, true)
@@ -537,53 +866,12 @@ var ByBitConnectorCreator = async (config) => {
537
866
  if (orderRes.retCode !== 0) {
538
867
  return false;
539
868
  }
540
- if (!isLimit) {
541
- for (const tp of TP) {
542
- const tpSizeRaw = orderQty * tp.rate;
543
- const { qtyNum: tpSizeNum, qtyStr: tpSizeStr } = normalizeQty(
544
- tpSizeRaw,
545
- meta
546
- );
547
- if (!tpSizeNum || tpSizeNum < meta.minOrderQty) {
548
- logger2.log(
549
- "warn",
550
- "tp skipped: size too small %s",
551
- toJson(
552
- { symbol, tp, tpSizeNum, minOrderQty: meta.minOrderQty },
553
- true
554
- )
555
- );
556
- continue;
557
- }
558
- const tpPriceNorm = normalizePrice(
559
- tp.price,
560
- isLong ? "TP_LONG" : "TP_SHORT",
561
- meta
562
- );
563
- const isFullMode = TP.length === 1 && tp.rate === 1;
564
- const tpRes = await client.setTradingStop({
565
- category: MARKET_CATEGORY2,
566
- symbol,
567
- tpSize: isFullMode ? void 0 : tpSizeStr,
568
- tpslMode: isFullMode ? "Full" : "Partial",
569
- takeProfit: tpPriceNorm.priceStr,
570
- stopLoss: isFullMode && slNormalized ? slNormalized.priceStr : void 0,
571
- slTriggerBy: "LastPrice",
572
- tpOrderType: "Market",
573
- positionIdx: 0
574
- });
575
- logger2.log(
576
- getLogLevel(tpRes),
577
- "tp: %s %s",
578
- toJson(tp, true),
579
- toJson(tpRes, true)
580
- );
581
- }
582
- }
583
869
  return true;
584
870
  },
871
+ setTakeProfits,
872
+ setStopLoss,
585
873
  closePosition: async ({ symbol, direction }) => {
586
- const client = await getClient(config);
874
+ const client = await getPrivateClient();
587
875
  if (!client) {
588
876
  return false;
589
877
  }
@@ -595,7 +883,7 @@ var ByBitConnectorCreator = async (config) => {
595
883
  qty: "0",
596
884
  reduceOnly: true
597
885
  });
598
- logger2.log(
886
+ logger3.log(
599
887
  getLogLevel(closeRes),
600
888
  "closePosition: %s, %s, %s",
601
889
  symbol,
@@ -608,7 +896,7 @@ var ByBitConnectorCreator = async (config) => {
608
896
  return true;
609
897
  },
610
898
  getTickers: async () => {
611
- const client = await getClient(config);
899
+ const client = await getPublicClient();
612
900
  if (!client) {
613
901
  return [];
614
902
  }
@@ -656,59 +944,77 @@ var toNum = (value, fallback = 0) => {
656
944
  const num = Number(value);
657
945
  return Number.isFinite(num) ? num : fallback;
658
946
  };
947
+ var intervalToMinutes = (interval) => {
948
+ const intervalMs = INTERVAL_MS[String(interval)];
949
+ return intervalMs ? Math.floor(intervalMs / 6e4) : null;
950
+ };
659
951
  var BinanceConnectorCreator = async () => {
660
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
+ };
661
1002
  return {
662
1003
  getState: async () => state,
663
1004
  setState: async (newState) => {
664
1005
  state = { ...state, ...newState };
665
1006
  },
666
- kline: async ({ symbol, interval, start, end }) => {
667
- const intervalToken = INTERVAL_MAP[String(interval)];
668
- if (!intervalToken) return [];
669
- const intervalMs = INTERVAL_MS[String(interval)] ?? 9e5;
670
- const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
671
- let cursor = start ?? Math.max(0, end - intervalMs * 1e3);
672
- const rows = [];
673
- while (cursor <= end) {
674
- const url = new URL(`${baseUrl}/api/v3/klines`);
675
- url.searchParams.set("symbol", symbol);
676
- url.searchParams.set("interval", intervalToken);
677
- url.searchParams.set("startTime", String(cursor));
678
- url.searchParams.set("endTime", String(end));
679
- url.searchParams.set("limit", "1000");
680
- const response = await fetchWithRetry(url.toString(), {
681
- headers: { "User-Agent": "tradejs/binance-connector" }
682
- });
683
- if (!response.ok) break;
684
- const payload = await response.json();
685
- if (!Array.isArray(payload) || !payload.length) break;
686
- let lastTs = cursor;
687
- for (const item of payload) {
688
- if (!Array.isArray(item)) continue;
689
- const ts = toNum(item[0], 0);
690
- if (!ts) continue;
691
- rows.push({
692
- timestamp: ts,
693
- open: toNum(item[1]),
694
- high: toNum(item[2]),
695
- low: toNum(item[3]),
696
- close: toNum(item[4]),
697
- volume: toNum(item[5]),
698
- turnover: toNum(item[7]),
699
- dt: new Date(ts).toISOString()
700
- });
701
- lastTs = ts;
702
- }
703
- if (payload.length < 1e3) break;
704
- cursor = lastTs + intervalMs;
705
- }
706
- rows.sort((a, b) => a.timestamp - b.timestamp);
707
- return rows;
708
- },
1007
+ kline: createTimescaleCachedKline({
1008
+ provider: "binance",
1009
+ request: requestKline,
1010
+ intervalToMinutes
1011
+ }),
709
1012
  getPosition: async () => null,
710
1013
  getPositions: async () => [],
1014
+ getOpenPositionPnl: async () => [],
711
1015
  placeOrder: async () => false,
1016
+ setTakeProfits: async () => false,
1017
+ setStopLoss: async () => false,
712
1018
  closePosition: async () => false,
713
1019
  getTickers: async () => {
714
1020
  const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
@@ -789,6 +1095,10 @@ var toNum2 = (value, fallback = 0) => {
789
1095
  const num = Number(value);
790
1096
  return Number.isFinite(num) ? num : fallback;
791
1097
  };
1098
+ var intervalToMinutes2 = (interval) => {
1099
+ const intervalMs = INTERVAL_MS2[String(interval)];
1100
+ return intervalMs ? Math.floor(intervalMs / 6e4) : null;
1101
+ };
792
1102
  var toCoinbaseProduct = (symbol) => {
793
1103
  const upper = symbol.toUpperCase();
794
1104
  const quoteSuffixes = ["USDT", "USDC", "BUSD", "USD"];
@@ -815,59 +1125,73 @@ var MAJOR_PRODUCTS = [
815
1125
  ];
816
1126
  var CoinbaseConnectorCreator = async () => {
817
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
+ };
818
1179
  return {
819
1180
  getState: async () => state,
820
1181
  setState: async (newState) => {
821
1182
  state = { ...state, ...newState };
822
1183
  },
823
- kline: async ({ symbol, interval, start, end }) => {
824
- const granularity = GRANULARITY_MAP[String(interval)];
825
- if (!granularity) return [];
826
- const product = toCoinbaseProduct(symbol);
827
- if (!product) return [];
828
- const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
829
- const stepMs = granularity * 1e3 * 300;
830
- const fromMs = start ?? Math.max(0, end - INTERVAL_MS2[String(interval)] * 1e3);
831
- let cursorEnd = end;
832
- const rows = [];
833
- while (cursorEnd >= fromMs) {
834
- const chunkStart = Math.max(fromMs, cursorEnd - stepMs);
835
- const url = new URL(`${baseUrl}/products/${product}/candles`);
836
- url.searchParams.set("granularity", String(granularity));
837
- url.searchParams.set("start", new Date(chunkStart).toISOString());
838
- url.searchParams.set("end", new Date(cursorEnd).toISOString());
839
- const response = await fetchWithRetry2(url.toString(), {
840
- headers: { "User-Agent": "tradejs/coinbase-connector" }
841
- });
842
- if (!response.ok) break;
843
- const payload = await response.json();
844
- if (Array.isArray(payload)) {
845
- for (const item of payload) {
846
- if (!Array.isArray(item)) continue;
847
- const ts = toNum2(item[0]) * 1e3;
848
- rows.push({
849
- timestamp: ts,
850
- low: toNum2(item[1]),
851
- high: toNum2(item[2]),
852
- open: toNum2(item[3]),
853
- close: toNum2(item[4]),
854
- volume: toNum2(item[5]),
855
- turnover: 0,
856
- dt: new Date(ts).toISOString()
857
- });
858
- }
859
- }
860
- cursorEnd = chunkStart - 1;
861
- }
862
- const dedup = /* @__PURE__ */ new Map();
863
- for (const row of rows) {
864
- dedup.set(row.timestamp, row);
865
- }
866
- return [...dedup.values()].sort((a, b) => a.timestamp - b.timestamp);
867
- },
1184
+ kline: createTimescaleCachedKline({
1185
+ provider: "coinbase",
1186
+ request: requestKline,
1187
+ intervalToMinutes: intervalToMinutes2
1188
+ }),
868
1189
  getPosition: async () => null,
869
1190
  getPositions: async () => [],
1191
+ getOpenPositionPnl: async () => [],
870
1192
  placeOrder: async () => false,
1193
+ setTakeProfits: async () => false,
1194
+ setStopLoss: async () => false,
871
1195
  closePosition: async () => false,
872
1196
  getTickers: async () => {
873
1197
  const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
@@ -1083,7 +1407,7 @@ var binanceCoinbaseSpreadProvider = {
1083
1407
  };
1084
1408
 
1085
1409
  // src/marketData/providers/coinalyze.ts
1086
- import { delay } from "@tradejs/core/async";
1410
+ import { delay as delay2 } from "@tradejs/core/async";
1087
1411
  import {
1088
1412
  coinalyzePointsToRows,
1089
1413
  mergeCoinalyzeMetrics
@@ -1137,12 +1461,8 @@ var normalizeMetricPayload = (metric, raw) => {
1137
1461
  }));
1138
1462
  };
1139
1463
  var fetchCoinalyzeSeries = async (params) => {
1140
- const { endpoint, metric, symbol, interval, fromMs, toMs } = params;
1464
+ const { endpoint, metric, symbol, apiKey, interval, fromMs, toMs } = params;
1141
1465
  const baseUrl = process.env.COINALYZE_BASE_URL?.trim() || "https://api.coinalyze.net/v1";
1142
- const apiKey = process.env.COINALYZE_API_KEY?.trim();
1143
- if (!apiKey) {
1144
- throw new Error("Missing COINALYZE_API_KEY");
1145
- }
1146
1466
  const url = new URL(`${baseUrl}${endpoint}`);
1147
1467
  url.searchParams.set("symbols", symbol);
1148
1468
  url.searchParams.set("interval", coinalyzeIntervalMap[interval] || interval);
@@ -1160,7 +1480,7 @@ var fetchCoinalyzeSeries = async (params) => {
1160
1480
  lastRequestTs + coinalyzeMinRequestDelayMs - now
1161
1481
  );
1162
1482
  if (waitMs > 0) {
1163
- await delay(waitMs);
1483
+ await delay2(waitMs);
1164
1484
  }
1165
1485
  lastRequestTs = Date.now();
1166
1486
  const response = await fetch(url.toString(), { headers });
@@ -1174,7 +1494,7 @@ var fetchCoinalyzeSeries = async (params) => {
1174
1494
  const transient = response.status === 429 || response.status >= 500;
1175
1495
  if (attempt < coinalyzeMaxRetries && transient) {
1176
1496
  const backoffMs = Math.min(1e4, 750 * 2 ** attempt);
1177
- await delay(retryAfterMs ?? backoffMs);
1497
+ await delay2(retryAfterMs ?? backoffMs);
1178
1498
  continue;
1179
1499
  }
1180
1500
  throw new Error(`Coinalyze ${endpoint} ${response.status}: ${text}`);
@@ -1183,15 +1503,27 @@ var fetchCoinalyzeSeries = async (params) => {
1183
1503
  };
1184
1504
  var coinalyzeProvider = {
1185
1505
  name: "coinalyze",
1186
- fetchWindow: async ({ symbol, marketSymbol, interval, fromMs, toMs }) => {
1506
+ fetchWindow: async ({
1507
+ symbol,
1508
+ marketSymbol,
1509
+ apiKey,
1510
+ interval,
1511
+ fromMs,
1512
+ toMs
1513
+ }) => {
1514
+ if (!apiKey?.trim()) {
1515
+ throw new Error("Missing COINALYZE_API_KEY in user settings");
1516
+ }
1187
1517
  const oiPath = process.env.COINALYZE_OI_PATH?.trim() || "/open-interest-history";
1188
1518
  const fundingPath = process.env.COINALYZE_FUNDING_PATH?.trim() || "/funding-rate-history";
1189
1519
  const liqPath = process.env.COINALYZE_LIQ_PATH?.trim() || "/liquidation-history";
1190
1520
  const requestSymbol = (marketSymbol || symbol).trim().toUpperCase();
1521
+ const normalizedApiKey = apiKey.trim();
1191
1522
  const oiRaw = await fetchCoinalyzeSeries({
1192
1523
  endpoint: oiPath,
1193
1524
  metric: "oi",
1194
1525
  symbol: requestSymbol,
1526
+ apiKey: normalizedApiKey,
1195
1527
  interval,
1196
1528
  fromMs,
1197
1529
  toMs
@@ -1200,6 +1532,7 @@ var coinalyzeProvider = {
1200
1532
  endpoint: fundingPath,
1201
1533
  metric: "funding",
1202
1534
  symbol: requestSymbol,
1535
+ apiKey: normalizedApiKey,
1203
1536
  interval,
1204
1537
  fromMs,
1205
1538
  toMs
@@ -1208,6 +1541,7 @@ var coinalyzeProvider = {
1208
1541
  endpoint: liqPath,
1209
1542
  metric: "liq",
1210
1543
  symbol: requestSymbol,
1544
+ apiKey: normalizedApiKey,
1211
1545
  interval,
1212
1546
  fromMs,
1213
1547
  toMs