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