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