@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.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +762 -428
- package/dist/index.mjs +759 -425
- package/package.json +7 -7
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,
|
|
13
|
-
import { logger as
|
|
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
|
|
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
|
|
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 ||
|
|
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
|
|
435
|
+
const client = await getPublicClient();
|
|
167
436
|
if (!client) return [];
|
|
168
437
|
if (normalizedEnd <= normalizedStart) {
|
|
169
438
|
return [];
|
|
170
439
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
488
|
+
return [];
|
|
201
489
|
} catch (error) {
|
|
202
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
fulfilled = true;
|
|
244
|
-
break;
|
|
625
|
+
if (!isTradingStopAccepted(tpRes)) {
|
|
626
|
+
return false;
|
|
245
627
|
}
|
|
246
|
-
pointer = nextPointer;
|
|
247
628
|
}
|
|
248
|
-
return
|
|
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
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
tailCount = 2
|
|
633
|
+
direction,
|
|
634
|
+
stopLossPrice
|
|
271
635
|
}) => {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
return;
|
|
636
|
+
const client = await getPrivateClient();
|
|
637
|
+
const marketDataClient = await getPublicClient();
|
|
638
|
+
if (!client || !marketDataClient) {
|
|
639
|
+
return false;
|
|
276
640
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
const
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
654
|
+
tpslMode: "Full",
|
|
655
|
+
stopLoss: slNormalized.priceStr,
|
|
656
|
+
slTriggerBy: "LastPrice",
|
|
657
|
+
positionIdx: 0
|
|
287
658
|
});
|
|
288
|
-
if (
|
|
289
|
-
|
|
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:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
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
|
|
702
|
+
return [];
|
|
420
703
|
}
|
|
421
704
|
const positionRes = await client.getPositionInfo({
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
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
|
|
718
|
+
return [];
|
|
437
719
|
}
|
|
438
|
-
|
|
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
|
-
|
|
450
|
-
const client = await
|
|
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
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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
|
|
473
|
-
const client = await
|
|
474
|
-
|
|
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(
|
|
816
|
+
const meta = await getSymbolMeta(marketDataClient, symbol);
|
|
479
817
|
const { qtyNum: orderQty, qtyStr: orderQtyStr } = normalizeQty(qty, meta);
|
|
480
818
|
if (orderQty < meta.minOrderQty) {
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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:
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
|
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
|
|
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 ({
|
|
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
|