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