@tradejs/connectors 1.0.0
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/README.md +39 -0
- package/dist/index.d.mts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +1324 -0
- package/dist/index.mjs +1293 -0
- package/package.json +37 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1293 @@
|
|
|
1
|
+
// src/ByBit/index.ts
|
|
2
|
+
import _ from "lodash";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import {
|
|
5
|
+
MARKET_CATEGORY as MARKET_CATEGORY2,
|
|
6
|
+
PRELOAD_FALLBACK_DAYS
|
|
7
|
+
} from "@tradejs/core/constants";
|
|
8
|
+
import { mergeData } from "@tradejs/core/data";
|
|
9
|
+
import { toJson } from "@tradejs/core/json";
|
|
10
|
+
import { round } from "@tradejs/core/math";
|
|
11
|
+
import { normalizeTickerData } from "@tradejs/core/tickers";
|
|
12
|
+
import { formatUnix as formatUnix2, getItemTimestamp, getTimestamp } from "@tradejs/core/time";
|
|
13
|
+
import { logger as logger2 } from "@tradejs/infra/logger";
|
|
14
|
+
import {
|
|
15
|
+
getCandlesRange,
|
|
16
|
+
getDataEdges,
|
|
17
|
+
upsertCandles,
|
|
18
|
+
toRows
|
|
19
|
+
} from "@tradejs/infra/timescale";
|
|
20
|
+
|
|
21
|
+
// src/ByBit/client.ts
|
|
22
|
+
import { RestClientV5 } from "bybit-api";
|
|
23
|
+
import { logger } from "@tradejs/infra/logger";
|
|
24
|
+
import { getData, redisKeys } from "@tradejs/infra/redis";
|
|
25
|
+
var useTestnet = false;
|
|
26
|
+
var getClient = async ({ userName }) => {
|
|
27
|
+
const user = await getData(redisKeys.user(userName));
|
|
28
|
+
if (!user) {
|
|
29
|
+
logger.log("error", "connection config not found: %s", userName);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const client = new RestClientV5({
|
|
33
|
+
key: user.BYBIT_API_KEY,
|
|
34
|
+
secret: user.BYBIT_API_SECRET,
|
|
35
|
+
testnet: useTestnet
|
|
36
|
+
});
|
|
37
|
+
return client;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/ByBit/utils.ts
|
|
41
|
+
import { MARKET_CATEGORY } from "@tradejs/core/constants";
|
|
42
|
+
import { formatUnix } from "@tradejs/core/time";
|
|
43
|
+
var parseKlineItem = (item) => ({
|
|
44
|
+
dt: formatUnix(parseInt(item[0])),
|
|
45
|
+
timestamp: parseInt(item[0]),
|
|
46
|
+
open: parseFloat(item[1]),
|
|
47
|
+
high: parseFloat(item[2]),
|
|
48
|
+
low: parseFloat(item[3]),
|
|
49
|
+
close: parseFloat(item[4]),
|
|
50
|
+
volume: parseFloat(item[5]),
|
|
51
|
+
turnover: parseFloat(item[6])
|
|
52
|
+
});
|
|
53
|
+
var mapKlineToChartData = (data) => data.map(parseKlineItem);
|
|
54
|
+
var symbolMetaCache = /* @__PURE__ */ new Map();
|
|
55
|
+
var roundByStep = (value, step, mode = "floor") => {
|
|
56
|
+
if (!step || !Number.isFinite(value)) return value;
|
|
57
|
+
const ratio = value / step;
|
|
58
|
+
const fn = mode === "ceil" ? Math.ceil : mode === "round" ? Math.round : Math.floor;
|
|
59
|
+
const res = fn(ratio) * step;
|
|
60
|
+
return Number(res.toFixed(12));
|
|
61
|
+
};
|
|
62
|
+
var stepToPrecision = (raw) => {
|
|
63
|
+
const [, decimals = ""] = raw.split(".");
|
|
64
|
+
return decimals.length;
|
|
65
|
+
};
|
|
66
|
+
var getSymbolMeta = async (client, symbol) => {
|
|
67
|
+
const cached = symbolMetaCache.get(symbol);
|
|
68
|
+
if (cached) return cached;
|
|
69
|
+
const res = await client.getInstrumentsInfo({
|
|
70
|
+
category: MARKET_CATEGORY,
|
|
71
|
+
symbol
|
|
72
|
+
});
|
|
73
|
+
const item = res?.result?.list?.[0];
|
|
74
|
+
if (!item) {
|
|
75
|
+
throw new Error(`No instrument info for symbol ${symbol}`);
|
|
76
|
+
}
|
|
77
|
+
const tickSizeStr = item.priceFilter?.tickSize ?? "0.01";
|
|
78
|
+
const qtyStepStr = item.lotSizeFilter?.qtyStep ?? "0.001";
|
|
79
|
+
const minOrderQtyStr = item.lotSizeFilter?.minOrderQty ?? "0.001";
|
|
80
|
+
const meta = {
|
|
81
|
+
tickSize: Number(tickSizeStr),
|
|
82
|
+
qtyStep: Number(qtyStepStr),
|
|
83
|
+
minOrderQty: Number(minOrderQtyStr),
|
|
84
|
+
pricePrecision: stepToPrecision(tickSizeStr),
|
|
85
|
+
qtyPrecision: stepToPrecision(qtyStepStr)
|
|
86
|
+
};
|
|
87
|
+
symbolMetaCache.set(symbol, meta);
|
|
88
|
+
return meta;
|
|
89
|
+
};
|
|
90
|
+
var normalizeQty = (rawQty, meta) => {
|
|
91
|
+
const qtyNum = roundByStep(rawQty, meta.qtyStep, "floor");
|
|
92
|
+
const qtyStr = qtyNum.toFixed(meta.qtyPrecision);
|
|
93
|
+
return { qtyNum, qtyStr };
|
|
94
|
+
};
|
|
95
|
+
var normalizePrice = (rawPrice, role, meta) => {
|
|
96
|
+
let mode = "floor";
|
|
97
|
+
switch (role) {
|
|
98
|
+
case "TP_LONG":
|
|
99
|
+
mode = "ceil";
|
|
100
|
+
break;
|
|
101
|
+
case "TP_SHORT":
|
|
102
|
+
mode = "floor";
|
|
103
|
+
break;
|
|
104
|
+
case "SL_LONG":
|
|
105
|
+
mode = "floor";
|
|
106
|
+
break;
|
|
107
|
+
case "SL_SHORT":
|
|
108
|
+
mode = "ceil";
|
|
109
|
+
break;
|
|
110
|
+
case "ENTRY":
|
|
111
|
+
default:
|
|
112
|
+
mode = "floor";
|
|
113
|
+
}
|
|
114
|
+
const priceNum = roundByStep(rawPrice, meta.tickSize, mode);
|
|
115
|
+
const priceStr = priceNum.toFixed(meta.pricePrecision);
|
|
116
|
+
return { priceNum, priceStr };
|
|
117
|
+
};
|
|
118
|
+
var mapPositionData = (data) => {
|
|
119
|
+
if (!data) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
return data.filter((item) => parseFloat(item.size) > 0).map((item) => ({
|
|
123
|
+
symbol: item.symbol,
|
|
124
|
+
price: parseFloat(item.avgPrice),
|
|
125
|
+
slPrice: parseFloat(item.stopLoss || ""),
|
|
126
|
+
qty: parseFloat(item.size),
|
|
127
|
+
direction: item.side === "Buy" ? "LONG" : "SHORT"
|
|
128
|
+
}));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// src/ByBit/index.ts
|
|
132
|
+
var LIMIT = 1e3;
|
|
133
|
+
var CACHE_FALLBACK_WINDOW = 1e3;
|
|
134
|
+
var INTERVAL_TO_MINUTES = {
|
|
135
|
+
"1": 1,
|
|
136
|
+
"3": 3,
|
|
137
|
+
"5": 5,
|
|
138
|
+
"15": 15,
|
|
139
|
+
"30": 30,
|
|
140
|
+
"60": 60,
|
|
141
|
+
"120": 120,
|
|
142
|
+
"240": 240,
|
|
143
|
+
"360": 360,
|
|
144
|
+
"720": 720,
|
|
145
|
+
D: 1440,
|
|
146
|
+
W: 10080,
|
|
147
|
+
M: 43200
|
|
148
|
+
};
|
|
149
|
+
var getLogLevel = (res) => res.retCode === 0 ? "info" : "error";
|
|
150
|
+
var ByBitConnectorCreator = async (config) => {
|
|
151
|
+
let state = {};
|
|
152
|
+
let isTimescaleFallbackMode = false;
|
|
153
|
+
const request = async ({
|
|
154
|
+
symbol,
|
|
155
|
+
interval,
|
|
156
|
+
start,
|
|
157
|
+
end,
|
|
158
|
+
silent
|
|
159
|
+
}) => {
|
|
160
|
+
const normalizedStart = round(
|
|
161
|
+
start || getTimestamp(PRELOAD_FALLBACK_DAYS),
|
|
162
|
+
0
|
|
163
|
+
);
|
|
164
|
+
const normalizedEnd = round(end || Date.now());
|
|
165
|
+
try {
|
|
166
|
+
const client = await getClient(config);
|
|
167
|
+
if (!client) return [];
|
|
168
|
+
if (normalizedEnd <= normalizedStart) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
const kline = await client.getKline({
|
|
172
|
+
category: MARKET_CATEGORY2,
|
|
173
|
+
symbol,
|
|
174
|
+
interval,
|
|
175
|
+
start: normalizedStart,
|
|
176
|
+
end: normalizedEnd,
|
|
177
|
+
limit: LIMIT
|
|
178
|
+
});
|
|
179
|
+
if (!kline?.result?.list) {
|
|
180
|
+
logger2.log("error", "empty kline.list for %s %s", symbol, interval);
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
if (!silent) {
|
|
184
|
+
logger2.log(
|
|
185
|
+
"info",
|
|
186
|
+
"%s %s %s %s",
|
|
187
|
+
chalk.yellow(formatUnix2(normalizedEnd)),
|
|
188
|
+
chalk.cyan(symbol),
|
|
189
|
+
chalk.cyan(interval),
|
|
190
|
+
chalk.yellow(kline.result.list.length)
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return mapKlineToChartData(kline.result.list.reverse());
|
|
194
|
+
} catch (error) {
|
|
195
|
+
logger2.log(
|
|
196
|
+
"error",
|
|
197
|
+
"request kline: %s %s %s",
|
|
198
|
+
normalizedStart,
|
|
199
|
+
normalizedEnd,
|
|
200
|
+
error
|
|
201
|
+
);
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const loadData = async (direction, pointer, limitBoundary, requestParams, intervalMs) => {
|
|
206
|
+
if (pointer === void 0) return [];
|
|
207
|
+
let accumulated = [];
|
|
208
|
+
let fulfilled = false;
|
|
209
|
+
while (!fulfilled) {
|
|
210
|
+
const currentPointer = pointer;
|
|
211
|
+
const params = {
|
|
212
|
+
symbol: requestParams.symbol,
|
|
213
|
+
interval: requestParams.interval,
|
|
214
|
+
silent: requestParams.silent
|
|
215
|
+
};
|
|
216
|
+
if (direction === "older") {
|
|
217
|
+
params.end = pointer;
|
|
218
|
+
if (limitBoundary !== void 0) params.start = limitBoundary;
|
|
219
|
+
} else {
|
|
220
|
+
params.start = pointer;
|
|
221
|
+
if (limitBoundary !== void 0) params.end = limitBoundary;
|
|
222
|
+
}
|
|
223
|
+
const partData = await request(params);
|
|
224
|
+
if (_.isEmpty(partData)) {
|
|
225
|
+
fulfilled = true;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
accumulated = direction === "older" ? mergeData(partData, accumulated) : mergeData(accumulated, partData);
|
|
229
|
+
const boundaryReached = limitBoundary !== void 0 && (direction === "older" && currentPointer <= limitBoundary || direction === "newer" && currentPointer >= limitBoundary);
|
|
230
|
+
if (partData.length < LIMIT || boundaryReached) {
|
|
231
|
+
fulfilled = true;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
const nextPointer = direction === "older" ? getItemTimestamp(partData[0]) - intervalMs : getItemTimestamp(partData[partData.length - 1]) + intervalMs;
|
|
235
|
+
if (!Number.isFinite(nextPointer) || nextPointer === currentPointer) {
|
|
236
|
+
fulfilled = true;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
pointer = nextPointer;
|
|
240
|
+
}
|
|
241
|
+
return accumulated;
|
|
242
|
+
};
|
|
243
|
+
const intervalMsOf = (interval) => interval * 6e4;
|
|
244
|
+
const intervalToMinutes = (interval) => {
|
|
245
|
+
return INTERVAL_TO_MINUTES[String(interval)] ?? null;
|
|
246
|
+
};
|
|
247
|
+
const clampToClosedCandle = (value, intervalMs) => Math.floor(value / intervalMs) * intervalMs;
|
|
248
|
+
const normalizeRangeToClosed = (intervalMs, start, end) => {
|
|
249
|
+
const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
|
|
250
|
+
const normStart = start !== void 0 ? clampToClosedCandle(start, intervalMs) : 0;
|
|
251
|
+
const cappedEnd = Math.min(end ?? Date.now(), lastClosed);
|
|
252
|
+
const normEnd = clampToClosedCandle(cappedEnd, intervalMs);
|
|
253
|
+
return { normStart, normEnd, lastClosed };
|
|
254
|
+
};
|
|
255
|
+
const rowsToKline = (rows) => rows.map(({ ts, ...data }) => ({
|
|
256
|
+
timestamp: new Date(ts).getTime(),
|
|
257
|
+
...data
|
|
258
|
+
}));
|
|
259
|
+
const refreshTail = async ({
|
|
260
|
+
symbol,
|
|
261
|
+
interval,
|
|
262
|
+
silent,
|
|
263
|
+
tailCount = 2
|
|
264
|
+
}) => {
|
|
265
|
+
const intMinutes = intervalToMinutes(interval);
|
|
266
|
+
if (!intMinutes) {
|
|
267
|
+
logger2.log("error", "refreshTail: invalid interval %s", interval);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const intervalMs = intervalMsOf(intMinutes);
|
|
271
|
+
const lastClosed = Math.floor(Date.now() / intervalMs) * intervalMs;
|
|
272
|
+
const tailEnd = lastClosed + intervalMs;
|
|
273
|
+
const tailStart = tailEnd - tailCount * intervalMs;
|
|
274
|
+
const part = await request({
|
|
275
|
+
symbol,
|
|
276
|
+
interval,
|
|
277
|
+
start: tailStart,
|
|
278
|
+
end: tailEnd,
|
|
279
|
+
silent
|
|
280
|
+
});
|
|
281
|
+
if (part.length) {
|
|
282
|
+
await upsertCandles(toRows(symbol, intMinutes, part));
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
getState: async () => state,
|
|
287
|
+
setState: async (newState) => {
|
|
288
|
+
state = { ...state, ...newState };
|
|
289
|
+
},
|
|
290
|
+
kline: async ({
|
|
291
|
+
symbol,
|
|
292
|
+
interval,
|
|
293
|
+
start: defaultStart,
|
|
294
|
+
end: defaultEnd,
|
|
295
|
+
silent = false,
|
|
296
|
+
cacheOnly = false
|
|
297
|
+
}) => {
|
|
298
|
+
const intMinutes = intervalToMinutes(interval);
|
|
299
|
+
if (!intMinutes) {
|
|
300
|
+
logger2.log("error", "kline: invalid interval %s", interval);
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
if (defaultStart !== void 0 && defaultEnd !== void 0 && defaultEnd <= defaultStart) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
const intervalMs = intervalMsOf(intMinutes);
|
|
307
|
+
try {
|
|
308
|
+
const edges = await getDataEdges(symbol, intMinutes);
|
|
309
|
+
let dataStart = edges.min;
|
|
310
|
+
let dataEnd = edges.max;
|
|
311
|
+
const { normStart, normEnd } = normalizeRangeToClosed(
|
|
312
|
+
intervalMs,
|
|
313
|
+
defaultStart,
|
|
314
|
+
defaultEnd
|
|
315
|
+
);
|
|
316
|
+
if (cacheOnly) {
|
|
317
|
+
const base = edges.max ?? Date.now();
|
|
318
|
+
const s = Math.max(
|
|
319
|
+
defaultStart ?? base - CACHE_FALLBACK_WINDOW * intervalMs,
|
|
320
|
+
0
|
|
321
|
+
);
|
|
322
|
+
const e = defaultEnd ?? base;
|
|
323
|
+
const dbData2 = await getCandlesRange(symbol, intMinutes, s, e);
|
|
324
|
+
return rowsToKline(dbData2);
|
|
325
|
+
}
|
|
326
|
+
const needOlderData = defaultStart !== void 0 && (dataStart === void 0 || normStart < dataStart);
|
|
327
|
+
const needNewerData = defaultEnd !== void 0 && (dataEnd === void 0 || normEnd > dataEnd);
|
|
328
|
+
if (needOlderData) {
|
|
329
|
+
const pointerForOlder = dataStart ?? normEnd ?? Date.now();
|
|
330
|
+
const olderData = await loadData(
|
|
331
|
+
"older",
|
|
332
|
+
pointerForOlder,
|
|
333
|
+
normStart,
|
|
334
|
+
{
|
|
335
|
+
symbol,
|
|
336
|
+
interval,
|
|
337
|
+
silent,
|
|
338
|
+
start: normStart,
|
|
339
|
+
end: pointerForOlder
|
|
340
|
+
},
|
|
341
|
+
intervalMs
|
|
342
|
+
);
|
|
343
|
+
if (olderData.length) {
|
|
344
|
+
await upsertCandles(toRows(symbol, intMinutes, olderData));
|
|
345
|
+
dataStart = normStart;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (needNewerData) {
|
|
349
|
+
const pointerForNewer = dataEnd ?? normStart ?? 0;
|
|
350
|
+
const newerData = await loadData(
|
|
351
|
+
"newer",
|
|
352
|
+
pointerForNewer,
|
|
353
|
+
normEnd,
|
|
354
|
+
{
|
|
355
|
+
symbol,
|
|
356
|
+
interval,
|
|
357
|
+
silent,
|
|
358
|
+
start: pointerForNewer,
|
|
359
|
+
end: normEnd
|
|
360
|
+
},
|
|
361
|
+
intervalMs
|
|
362
|
+
);
|
|
363
|
+
if (newerData.length) {
|
|
364
|
+
await upsertCandles(toRows(symbol, intMinutes, newerData));
|
|
365
|
+
dataEnd = normEnd;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const isRightEdgeQuery = defaultEnd === void 0 || defaultEnd && defaultEnd >= Date.now() - intervalMs;
|
|
369
|
+
if (!cacheOnly && isRightEdgeQuery) {
|
|
370
|
+
await refreshTail({ symbol, interval, silent });
|
|
371
|
+
}
|
|
372
|
+
const rangeStart = defaultStart ?? dataStart ?? 0;
|
|
373
|
+
const rangeEnd = defaultEnd ?? dataEnd ?? Date.now();
|
|
374
|
+
const { normStart: finalStart, normEnd: finalEnd } = normalizeRangeToClosed(intervalMs, rangeStart, rangeEnd);
|
|
375
|
+
const dbData = await getCandlesRange(
|
|
376
|
+
symbol,
|
|
377
|
+
intMinutes,
|
|
378
|
+
finalStart,
|
|
379
|
+
finalEnd
|
|
380
|
+
);
|
|
381
|
+
if (isTimescaleFallbackMode) {
|
|
382
|
+
isTimescaleFallbackMode = false;
|
|
383
|
+
logger2.log("info", "TimescaleDB connection restored for kline cache");
|
|
384
|
+
}
|
|
385
|
+
return rowsToKline(dbData);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (!isTimescaleFallbackMode) {
|
|
388
|
+
isTimescaleFallbackMode = true;
|
|
389
|
+
logger2.log(
|
|
390
|
+
"warn",
|
|
391
|
+
"TimescaleDB unavailable for %s %s: %s. Falling back to exchange API.",
|
|
392
|
+
symbol,
|
|
393
|
+
interval,
|
|
394
|
+
String(error)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (cacheOnly) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
return request({
|
|
401
|
+
symbol,
|
|
402
|
+
interval,
|
|
403
|
+
start: defaultStart,
|
|
404
|
+
end: defaultEnd,
|
|
405
|
+
silent
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
getPosition: async (symbol) => {
|
|
410
|
+
const client = await getClient(config);
|
|
411
|
+
if (!client) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
const positionRes = await client.getPositionInfo({
|
|
415
|
+
symbol,
|
|
416
|
+
category: MARKET_CATEGORY2
|
|
417
|
+
});
|
|
418
|
+
logger2.log(
|
|
419
|
+
getLogLevel(positionRes),
|
|
420
|
+
"position retCode: %s, %s",
|
|
421
|
+
symbol,
|
|
422
|
+
positionRes.retCode
|
|
423
|
+
);
|
|
424
|
+
if (positionRes.retCode !== 0) {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
const positions = mapPositionData(positionRes.result.list);
|
|
428
|
+
if (!positions || _.isEmpty(positions)) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const position = positions[0];
|
|
432
|
+
logger2.log(
|
|
433
|
+
getLogLevel(positionRes),
|
|
434
|
+
"position: %s, %s",
|
|
435
|
+
symbol,
|
|
436
|
+
toJson(positionRes, true)
|
|
437
|
+
);
|
|
438
|
+
return {
|
|
439
|
+
...position
|
|
440
|
+
};
|
|
441
|
+
},
|
|
442
|
+
getPositions: async () => {
|
|
443
|
+
const client = await getClient(config);
|
|
444
|
+
if (!client) {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
const positionRes = await client.getPositionInfo({
|
|
448
|
+
category: MARKET_CATEGORY2,
|
|
449
|
+
settleCoin: "USDT"
|
|
450
|
+
});
|
|
451
|
+
logger2.log(
|
|
452
|
+
getLogLevel(positionRes),
|
|
453
|
+
"positions retCode: %s, %s",
|
|
454
|
+
positionRes.retCode
|
|
455
|
+
);
|
|
456
|
+
if (positionRes.retCode !== 0) {
|
|
457
|
+
return [];
|
|
458
|
+
}
|
|
459
|
+
const positions = mapPositionData(positionRes.result.list);
|
|
460
|
+
if (!positions || _.isEmpty(positions)) {
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
return positions;
|
|
464
|
+
},
|
|
465
|
+
placeOrder: async ({ symbol, price, qty, direction, isLimit }, TP = [], slPrice) => {
|
|
466
|
+
const client = await getClient(config);
|
|
467
|
+
if (!client) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const isLong = direction === "LONG";
|
|
471
|
+
const meta = await getSymbolMeta(client, symbol);
|
|
472
|
+
const { qtyNum: orderQty, qtyStr: orderQtyStr } = normalizeQty(qty, meta);
|
|
473
|
+
if (orderQty < meta.minOrderQty) {
|
|
474
|
+
logger2.log(
|
|
475
|
+
"warn",
|
|
476
|
+
"placeOrder: qty too small: %s",
|
|
477
|
+
toJson(
|
|
478
|
+
{ symbol, qty, orderQty, minOrderQty: meta.minOrderQty },
|
|
479
|
+
true
|
|
480
|
+
)
|
|
481
|
+
);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const entryNormalized = isLimit ? normalizePrice(price, "ENTRY", meta) : void 0;
|
|
485
|
+
const slNormalized = slPrice ? normalizePrice(slPrice, isLong ? "SL_LONG" : "SL_SHORT", meta) : void 0;
|
|
486
|
+
const firstTP = TP?.[0];
|
|
487
|
+
const tpNormalized = firstTP && firstTP.rate === 1 ? normalizePrice(firstTP.price, isLong ? "TP_LONG" : "TP_SHORT", meta) : void 0;
|
|
488
|
+
logger2.log(
|
|
489
|
+
"info",
|
|
490
|
+
"placeOrder: %s",
|
|
491
|
+
toJson(
|
|
492
|
+
{
|
|
493
|
+
symbol,
|
|
494
|
+
price,
|
|
495
|
+
qty,
|
|
496
|
+
direction,
|
|
497
|
+
orderQty,
|
|
498
|
+
orderQtyStr,
|
|
499
|
+
slPrice,
|
|
500
|
+
slPriceNorm: slNormalized?.priceStr,
|
|
501
|
+
TP
|
|
502
|
+
},
|
|
503
|
+
true
|
|
504
|
+
)
|
|
505
|
+
);
|
|
506
|
+
await client.setLeverage({
|
|
507
|
+
category: MARKET_CATEGORY2,
|
|
508
|
+
symbol,
|
|
509
|
+
buyLeverage: "10",
|
|
510
|
+
sellLeverage: "10"
|
|
511
|
+
});
|
|
512
|
+
const orderRes = await client.submitOrder({
|
|
513
|
+
category: MARKET_CATEGORY2,
|
|
514
|
+
symbol,
|
|
515
|
+
price: entryNormalized?.priceStr || void 0,
|
|
516
|
+
takeProfit: tpNormalized?.priceStr || void 0,
|
|
517
|
+
tpTriggerBy: "MarkPrice",
|
|
518
|
+
stopLoss: slNormalized?.priceStr || void 0,
|
|
519
|
+
slTriggerBy: "LastPrice",
|
|
520
|
+
side: isLong ? "Buy" : "Sell",
|
|
521
|
+
orderType: isLimit ? "Limit" : "Market",
|
|
522
|
+
qty: orderQtyStr,
|
|
523
|
+
orderFilter: "Order"
|
|
524
|
+
});
|
|
525
|
+
logger2.log(
|
|
526
|
+
getLogLevel(orderRes),
|
|
527
|
+
"placeOrder:response: %s",
|
|
528
|
+
toJson(orderRes, true)
|
|
529
|
+
);
|
|
530
|
+
if (orderRes.retCode !== 0) {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
if (!isLimit) {
|
|
534
|
+
for (const tp of TP) {
|
|
535
|
+
const tpSizeRaw = orderQty * tp.rate;
|
|
536
|
+
const { qtyNum: tpSizeNum, qtyStr: tpSizeStr } = normalizeQty(
|
|
537
|
+
tpSizeRaw,
|
|
538
|
+
meta
|
|
539
|
+
);
|
|
540
|
+
if (!tpSizeNum || tpSizeNum < meta.minOrderQty) {
|
|
541
|
+
logger2.log(
|
|
542
|
+
"warn",
|
|
543
|
+
"tp skipped: size too small %s",
|
|
544
|
+
toJson(
|
|
545
|
+
{ symbol, tp, tpSizeNum, minOrderQty: meta.minOrderQty },
|
|
546
|
+
true
|
|
547
|
+
)
|
|
548
|
+
);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const tpPriceNorm = normalizePrice(
|
|
552
|
+
tp.price,
|
|
553
|
+
isLong ? "TP_LONG" : "TP_SHORT",
|
|
554
|
+
meta
|
|
555
|
+
);
|
|
556
|
+
const isFullMode = TP.length === 1 && tp.rate === 1;
|
|
557
|
+
const tpRes = await client.setTradingStop({
|
|
558
|
+
category: MARKET_CATEGORY2,
|
|
559
|
+
symbol,
|
|
560
|
+
tpSize: isFullMode ? void 0 : tpSizeStr,
|
|
561
|
+
tpslMode: isFullMode ? "Full" : "Partial",
|
|
562
|
+
takeProfit: tpPriceNorm.priceStr,
|
|
563
|
+
stopLoss: isFullMode && slNormalized ? slNormalized.priceStr : void 0,
|
|
564
|
+
slTriggerBy: "LastPrice",
|
|
565
|
+
tpOrderType: "Market",
|
|
566
|
+
positionIdx: 0
|
|
567
|
+
});
|
|
568
|
+
logger2.log(
|
|
569
|
+
getLogLevel(tpRes),
|
|
570
|
+
"tp: %s %s",
|
|
571
|
+
toJson(tp, true),
|
|
572
|
+
toJson(tpRes, true)
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return true;
|
|
577
|
+
},
|
|
578
|
+
closePosition: async ({ symbol, direction }) => {
|
|
579
|
+
const client = await getClient(config);
|
|
580
|
+
if (!client) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
const closeRes = await client.submitOrder({
|
|
584
|
+
category: MARKET_CATEGORY2,
|
|
585
|
+
symbol,
|
|
586
|
+
side: direction === "LONG" ? "Sell" : "Buy",
|
|
587
|
+
orderType: "Market",
|
|
588
|
+
qty: "0",
|
|
589
|
+
reduceOnly: true
|
|
590
|
+
});
|
|
591
|
+
logger2.log(
|
|
592
|
+
getLogLevel(closeRes),
|
|
593
|
+
"closePosition: %s, %s, %s",
|
|
594
|
+
symbol,
|
|
595
|
+
direction,
|
|
596
|
+
toJson(closeRes, true)
|
|
597
|
+
);
|
|
598
|
+
if (closeRes.retCode !== 0) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
},
|
|
603
|
+
getTickers: async () => {
|
|
604
|
+
const client = await getClient(config);
|
|
605
|
+
if (!client) {
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
const data = await client.getTickers({
|
|
609
|
+
category: MARKET_CATEGORY2
|
|
610
|
+
});
|
|
611
|
+
return data.result.list.map((item) => normalizeTickerData(item));
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// src/Binance/index.ts
|
|
617
|
+
import { fetchWithRetry } from "@tradejs/infra/http";
|
|
618
|
+
var INTERVAL_MAP = {
|
|
619
|
+
"1": "1m",
|
|
620
|
+
"3": "3m",
|
|
621
|
+
"5": "5m",
|
|
622
|
+
"15": "15m",
|
|
623
|
+
"30": "30m",
|
|
624
|
+
"60": "1h",
|
|
625
|
+
"120": "2h",
|
|
626
|
+
"240": "4h",
|
|
627
|
+
"360": "6h",
|
|
628
|
+
"720": "12h",
|
|
629
|
+
D: "1d",
|
|
630
|
+
W: "1w",
|
|
631
|
+
M: "1M"
|
|
632
|
+
};
|
|
633
|
+
var INTERVAL_MS = {
|
|
634
|
+
"1": 6e4,
|
|
635
|
+
"3": 18e4,
|
|
636
|
+
"5": 3e5,
|
|
637
|
+
"15": 9e5,
|
|
638
|
+
"30": 18e5,
|
|
639
|
+
"60": 36e5,
|
|
640
|
+
"120": 72e5,
|
|
641
|
+
"240": 144e5,
|
|
642
|
+
"360": 216e5,
|
|
643
|
+
"720": 432e5,
|
|
644
|
+
D: 864e5,
|
|
645
|
+
W: 6048e5,
|
|
646
|
+
M: 2592e6
|
|
647
|
+
};
|
|
648
|
+
var toNum = (value, fallback = 0) => {
|
|
649
|
+
const num = Number(value);
|
|
650
|
+
return Number.isFinite(num) ? num : fallback;
|
|
651
|
+
};
|
|
652
|
+
var BinanceConnectorCreator = async () => {
|
|
653
|
+
let state = {};
|
|
654
|
+
return {
|
|
655
|
+
getState: async () => state,
|
|
656
|
+
setState: async (newState) => {
|
|
657
|
+
state = { ...state, ...newState };
|
|
658
|
+
},
|
|
659
|
+
kline: async ({ symbol, interval, start, end }) => {
|
|
660
|
+
const intervalToken = INTERVAL_MAP[String(interval)];
|
|
661
|
+
if (!intervalToken) return [];
|
|
662
|
+
const intervalMs = INTERVAL_MS[String(interval)] ?? 9e5;
|
|
663
|
+
const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
|
|
664
|
+
let cursor = start ?? Math.max(0, end - intervalMs * 1e3);
|
|
665
|
+
const rows = [];
|
|
666
|
+
while (cursor <= end) {
|
|
667
|
+
const url = new URL(`${baseUrl}/api/v3/klines`);
|
|
668
|
+
url.searchParams.set("symbol", symbol);
|
|
669
|
+
url.searchParams.set("interval", intervalToken);
|
|
670
|
+
url.searchParams.set("startTime", String(cursor));
|
|
671
|
+
url.searchParams.set("endTime", String(end));
|
|
672
|
+
url.searchParams.set("limit", "1000");
|
|
673
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
674
|
+
headers: { "User-Agent": "tradejs/binance-connector" }
|
|
675
|
+
});
|
|
676
|
+
if (!response.ok) break;
|
|
677
|
+
const payload = await response.json();
|
|
678
|
+
if (!Array.isArray(payload) || !payload.length) break;
|
|
679
|
+
let lastTs = cursor;
|
|
680
|
+
for (const item of payload) {
|
|
681
|
+
if (!Array.isArray(item)) continue;
|
|
682
|
+
const ts = toNum(item[0], 0);
|
|
683
|
+
if (!ts) continue;
|
|
684
|
+
rows.push({
|
|
685
|
+
timestamp: ts,
|
|
686
|
+
open: toNum(item[1]),
|
|
687
|
+
high: toNum(item[2]),
|
|
688
|
+
low: toNum(item[3]),
|
|
689
|
+
close: toNum(item[4]),
|
|
690
|
+
volume: toNum(item[5]),
|
|
691
|
+
turnover: toNum(item[7]),
|
|
692
|
+
dt: new Date(ts).toISOString()
|
|
693
|
+
});
|
|
694
|
+
lastTs = ts;
|
|
695
|
+
}
|
|
696
|
+
if (payload.length < 1e3) break;
|
|
697
|
+
cursor = lastTs + intervalMs;
|
|
698
|
+
}
|
|
699
|
+
rows.sort((a, b) => a.timestamp - b.timestamp);
|
|
700
|
+
return rows;
|
|
701
|
+
},
|
|
702
|
+
getPosition: async () => null,
|
|
703
|
+
getPositions: async () => [],
|
|
704
|
+
placeOrder: async () => false,
|
|
705
|
+
closePosition: async () => false,
|
|
706
|
+
getTickers: async () => {
|
|
707
|
+
const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
|
|
708
|
+
const response = await fetchWithRetry(`${baseUrl}/api/v3/ticker/24hr`, {
|
|
709
|
+
headers: { "User-Agent": "tradejs/binance-connector" }
|
|
710
|
+
});
|
|
711
|
+
if (!response.ok) return [];
|
|
712
|
+
const payload = await response.json();
|
|
713
|
+
if (!Array.isArray(payload)) return [];
|
|
714
|
+
return payload.map((row) => {
|
|
715
|
+
const symbol = String(row.symbol ?? "");
|
|
716
|
+
return {
|
|
717
|
+
symbol,
|
|
718
|
+
lastPrice: toNum(row.lastPrice),
|
|
719
|
+
indexPrice: toNum(row.lastPrice),
|
|
720
|
+
markPrice: toNum(row.lastPrice),
|
|
721
|
+
prevPrice24h: toNum(row.openPrice),
|
|
722
|
+
price24hPcnt: toNum(row.priceChangePercent) / 100,
|
|
723
|
+
highPrice24h: toNum(row.highPrice),
|
|
724
|
+
lowPrice24h: toNum(row.lowPrice),
|
|
725
|
+
prevPrice1h: 0,
|
|
726
|
+
openInterest: 0,
|
|
727
|
+
openInterestValue: 0,
|
|
728
|
+
turnover24h: toNum(row.quoteVolume),
|
|
729
|
+
volume24h: toNum(row.volume),
|
|
730
|
+
fundingRate: 0,
|
|
731
|
+
nextFundingTime: 0,
|
|
732
|
+
predictedDeliveryPrice: "",
|
|
733
|
+
basisRate: "",
|
|
734
|
+
deliveryFeeRate: "",
|
|
735
|
+
deliveryTime: 0,
|
|
736
|
+
ask1Size: toNum(row.askQty),
|
|
737
|
+
bid1Price: toNum(row.bidPrice),
|
|
738
|
+
ask1Price: toNum(row.askPrice),
|
|
739
|
+
bid1Size: toNum(row.bidQty),
|
|
740
|
+
basis: "",
|
|
741
|
+
preOpenPrice: "",
|
|
742
|
+
preQty: ""
|
|
743
|
+
};
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// src/Coinbase/index.ts
|
|
750
|
+
import { fetchWithRetry as fetchWithRetry2 } from "@tradejs/infra/http";
|
|
751
|
+
var INTERVAL_MS2 = {
|
|
752
|
+
"1": 6e4,
|
|
753
|
+
"3": 18e4,
|
|
754
|
+
"5": 3e5,
|
|
755
|
+
"15": 9e5,
|
|
756
|
+
"30": 18e5,
|
|
757
|
+
"60": 36e5,
|
|
758
|
+
"120": 72e5,
|
|
759
|
+
"240": 144e5,
|
|
760
|
+
"360": 216e5,
|
|
761
|
+
"720": 432e5,
|
|
762
|
+
D: 864e5,
|
|
763
|
+
W: 6048e5,
|
|
764
|
+
M: 2592e6
|
|
765
|
+
};
|
|
766
|
+
var GRANULARITY_MAP = {
|
|
767
|
+
"1": 60,
|
|
768
|
+
"3": 300,
|
|
769
|
+
"5": 300,
|
|
770
|
+
"15": 900,
|
|
771
|
+
"30": 1800,
|
|
772
|
+
"60": 3600,
|
|
773
|
+
"120": 21600,
|
|
774
|
+
"240": 21600,
|
|
775
|
+
"360": 21600,
|
|
776
|
+
"720": 21600,
|
|
777
|
+
D: 86400,
|
|
778
|
+
W: 86400,
|
|
779
|
+
M: 86400
|
|
780
|
+
};
|
|
781
|
+
var toNum2 = (value, fallback = 0) => {
|
|
782
|
+
const num = Number(value);
|
|
783
|
+
return Number.isFinite(num) ? num : fallback;
|
|
784
|
+
};
|
|
785
|
+
var toCoinbaseProduct = (symbol) => {
|
|
786
|
+
const upper = symbol.toUpperCase();
|
|
787
|
+
const quoteSuffixes = ["USDT", "USDC", "BUSD", "USD"];
|
|
788
|
+
for (const quote of quoteSuffixes) {
|
|
789
|
+
if (upper.endsWith(quote)) {
|
|
790
|
+
const base = upper.slice(0, -quote.length);
|
|
791
|
+
if (!base) return null;
|
|
792
|
+
return `${base}-USD`;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return null;
|
|
796
|
+
};
|
|
797
|
+
var MAJOR_PRODUCTS = [
|
|
798
|
+
"BTC-USD",
|
|
799
|
+
"ETH-USD",
|
|
800
|
+
"SOL-USD",
|
|
801
|
+
"XRP-USD",
|
|
802
|
+
"ADA-USD",
|
|
803
|
+
"DOGE-USD",
|
|
804
|
+
"LTC-USD",
|
|
805
|
+
"BCH-USD",
|
|
806
|
+
"LINK-USD",
|
|
807
|
+
"AVAX-USD"
|
|
808
|
+
];
|
|
809
|
+
var CoinbaseConnectorCreator = async () => {
|
|
810
|
+
let state = {};
|
|
811
|
+
return {
|
|
812
|
+
getState: async () => state,
|
|
813
|
+
setState: async (newState) => {
|
|
814
|
+
state = { ...state, ...newState };
|
|
815
|
+
},
|
|
816
|
+
kline: async ({ symbol, interval, start, end }) => {
|
|
817
|
+
const granularity = GRANULARITY_MAP[String(interval)];
|
|
818
|
+
if (!granularity) return [];
|
|
819
|
+
const product = toCoinbaseProduct(symbol);
|
|
820
|
+
if (!product) return [];
|
|
821
|
+
const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
|
|
822
|
+
const stepMs = granularity * 1e3 * 300;
|
|
823
|
+
const fromMs = start ?? Math.max(0, end - INTERVAL_MS2[String(interval)] * 1e3);
|
|
824
|
+
let cursorEnd = end;
|
|
825
|
+
const rows = [];
|
|
826
|
+
while (cursorEnd >= fromMs) {
|
|
827
|
+
const chunkStart = Math.max(fromMs, cursorEnd - stepMs);
|
|
828
|
+
const url = new URL(`${baseUrl}/products/${product}/candles`);
|
|
829
|
+
url.searchParams.set("granularity", String(granularity));
|
|
830
|
+
url.searchParams.set("start", new Date(chunkStart).toISOString());
|
|
831
|
+
url.searchParams.set("end", new Date(cursorEnd).toISOString());
|
|
832
|
+
const response = await fetchWithRetry2(url.toString(), {
|
|
833
|
+
headers: { "User-Agent": "tradejs/coinbase-connector" }
|
|
834
|
+
});
|
|
835
|
+
if (!response.ok) break;
|
|
836
|
+
const payload = await response.json();
|
|
837
|
+
if (Array.isArray(payload)) {
|
|
838
|
+
for (const item of payload) {
|
|
839
|
+
if (!Array.isArray(item)) continue;
|
|
840
|
+
const ts = toNum2(item[0]) * 1e3;
|
|
841
|
+
rows.push({
|
|
842
|
+
timestamp: ts,
|
|
843
|
+
low: toNum2(item[1]),
|
|
844
|
+
high: toNum2(item[2]),
|
|
845
|
+
open: toNum2(item[3]),
|
|
846
|
+
close: toNum2(item[4]),
|
|
847
|
+
volume: toNum2(item[5]),
|
|
848
|
+
turnover: 0,
|
|
849
|
+
dt: new Date(ts).toISOString()
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
cursorEnd = chunkStart - 1;
|
|
854
|
+
}
|
|
855
|
+
const dedup = /* @__PURE__ */ new Map();
|
|
856
|
+
for (const row of rows) {
|
|
857
|
+
dedup.set(row.timestamp, row);
|
|
858
|
+
}
|
|
859
|
+
return [...dedup.values()].sort((a, b) => a.timestamp - b.timestamp);
|
|
860
|
+
},
|
|
861
|
+
getPosition: async () => null,
|
|
862
|
+
getPositions: async () => [],
|
|
863
|
+
placeOrder: async () => false,
|
|
864
|
+
closePosition: async () => false,
|
|
865
|
+
getTickers: async () => {
|
|
866
|
+
const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
|
|
867
|
+
const entries = await Promise.all(
|
|
868
|
+
MAJOR_PRODUCTS.map(async (product) => {
|
|
869
|
+
const [tickerRes, statsRes] = await Promise.all([
|
|
870
|
+
fetchWithRetry2(`${baseUrl}/products/${product}/ticker`, {
|
|
871
|
+
headers: { "User-Agent": "tradejs/coinbase-connector" }
|
|
872
|
+
}),
|
|
873
|
+
fetchWithRetry2(`${baseUrl}/products/${product}/stats`, {
|
|
874
|
+
headers: { "User-Agent": "tradejs/coinbase-connector" }
|
|
875
|
+
})
|
|
876
|
+
]);
|
|
877
|
+
if (!tickerRes.ok || !statsRes.ok) return null;
|
|
878
|
+
const ticker = await tickerRes.json();
|
|
879
|
+
const stats = await statsRes.json();
|
|
880
|
+
const base = product.replace("-USD", "");
|
|
881
|
+
const symbol = `${base}USDT`;
|
|
882
|
+
const open = toNum2(stats.open);
|
|
883
|
+
const last = toNum2(ticker.price);
|
|
884
|
+
const pct = open > 0 ? (last - open) / open : 0;
|
|
885
|
+
return {
|
|
886
|
+
symbol,
|
|
887
|
+
lastPrice: last,
|
|
888
|
+
indexPrice: last,
|
|
889
|
+
markPrice: last,
|
|
890
|
+
prevPrice24h: open,
|
|
891
|
+
price24hPcnt: pct,
|
|
892
|
+
highPrice24h: toNum2(stats.high),
|
|
893
|
+
lowPrice24h: toNum2(stats.low),
|
|
894
|
+
prevPrice1h: 0,
|
|
895
|
+
openInterest: 0,
|
|
896
|
+
openInterestValue: 0,
|
|
897
|
+
turnover24h: 0,
|
|
898
|
+
volume24h: toNum2(stats.volume),
|
|
899
|
+
fundingRate: 0,
|
|
900
|
+
nextFundingTime: 0,
|
|
901
|
+
predictedDeliveryPrice: "",
|
|
902
|
+
basisRate: "",
|
|
903
|
+
deliveryFeeRate: "",
|
|
904
|
+
deliveryTime: 0,
|
|
905
|
+
ask1Size: toNum2(ticker.ask_size),
|
|
906
|
+
bid1Price: toNum2(ticker.bid),
|
|
907
|
+
ask1Price: toNum2(ticker.ask),
|
|
908
|
+
bid1Size: toNum2(ticker.bid_size),
|
|
909
|
+
basis: "",
|
|
910
|
+
preOpenPrice: "",
|
|
911
|
+
preQty: ""
|
|
912
|
+
};
|
|
913
|
+
})
|
|
914
|
+
);
|
|
915
|
+
return entries.filter((item) => item != null);
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// src/Test/index.ts
|
|
921
|
+
import { createTestConnector } from "@tradejs/node/backtest";
|
|
922
|
+
var TestConnectorCreator = (connector, context) => createTestConnector(connector, context);
|
|
923
|
+
|
|
924
|
+
// src/marketData/spotKlineProviders.ts
|
|
925
|
+
import { fetchWithRetry as fetchWithRetry3 } from "@tradejs/infra/http";
|
|
926
|
+
var toIntervalToken = (interval) => interval === "1h" ? "1h" : "15m";
|
|
927
|
+
var mapBinanceKline = (payload) => payload.map((item) => {
|
|
928
|
+
if (!Array.isArray(item)) return null;
|
|
929
|
+
const ts = Number(item[0]);
|
|
930
|
+
const open = Number(item[1]);
|
|
931
|
+
const high = Number(item[2]);
|
|
932
|
+
const low = Number(item[3]);
|
|
933
|
+
const close = Number(item[4]);
|
|
934
|
+
const volume = Number(item[5]);
|
|
935
|
+
if (![ts, open, high, low, close, volume].every(Number.isFinite))
|
|
936
|
+
return null;
|
|
937
|
+
return {
|
|
938
|
+
timestamp: ts,
|
|
939
|
+
open,
|
|
940
|
+
high,
|
|
941
|
+
low,
|
|
942
|
+
close,
|
|
943
|
+
volume,
|
|
944
|
+
turnover: Number(item[7]) || 0,
|
|
945
|
+
dt: new Date(ts).toISOString()
|
|
946
|
+
};
|
|
947
|
+
}).filter((item) => item != null).sort((a, b) => a.timestamp - b.timestamp);
|
|
948
|
+
var mapCoinbaseKline = (payload) => payload.map((item) => {
|
|
949
|
+
if (!Array.isArray(item)) return null;
|
|
950
|
+
const tsSec = Number(item[0]);
|
|
951
|
+
const low = Number(item[1]);
|
|
952
|
+
const high = Number(item[2]);
|
|
953
|
+
const open = Number(item[3]);
|
|
954
|
+
const close = Number(item[4]);
|
|
955
|
+
const volume = Number(item[5]);
|
|
956
|
+
const ts = tsSec * 1e3;
|
|
957
|
+
if (![ts, open, high, low, close, volume].every(Number.isFinite))
|
|
958
|
+
return null;
|
|
959
|
+
return {
|
|
960
|
+
timestamp: ts,
|
|
961
|
+
open,
|
|
962
|
+
high,
|
|
963
|
+
low,
|
|
964
|
+
close,
|
|
965
|
+
volume,
|
|
966
|
+
turnover: 0,
|
|
967
|
+
dt: new Date(ts).toISOString()
|
|
968
|
+
};
|
|
969
|
+
}).filter((item) => item != null).sort((a, b) => a.timestamp - b.timestamp);
|
|
970
|
+
var spotKlineProviders = {
|
|
971
|
+
binance: {
|
|
972
|
+
kline: async ({ symbol, interval, start, end }) => {
|
|
973
|
+
const token = toIntervalToken(interval);
|
|
974
|
+
const baseUrl = process.env.BINANCE_BASE_URL?.trim() || "https://api.binance.com";
|
|
975
|
+
const url = new URL(`${baseUrl}/api/v3/klines`);
|
|
976
|
+
url.searchParams.set("symbol", symbol);
|
|
977
|
+
url.searchParams.set("interval", token);
|
|
978
|
+
url.searchParams.set("startTime", String(start));
|
|
979
|
+
url.searchParams.set("endTime", String(end));
|
|
980
|
+
url.searchParams.set("limit", "1000");
|
|
981
|
+
const response = await fetchWithRetry3(url.toString(), {
|
|
982
|
+
headers: { "User-Agent": "tradejs/market-data-ingest" }
|
|
983
|
+
});
|
|
984
|
+
if (!response.ok) {
|
|
985
|
+
throw new Error(
|
|
986
|
+
`Binance kline ${response.status}: ${await response.text()}`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const payload = await response.json();
|
|
990
|
+
return Array.isArray(payload) ? mapBinanceKline(payload) : [];
|
|
991
|
+
}
|
|
992
|
+
},
|
|
993
|
+
coinbase: {
|
|
994
|
+
kline: async ({ symbol, interval, start, end }) => {
|
|
995
|
+
const baseUrl = process.env.COINBASE_BASE_URL?.trim() || "https://api.exchange.coinbase.com";
|
|
996
|
+
const granularity = interval === "1h" ? 3600 : 900;
|
|
997
|
+
const url = new URL(`${baseUrl}/products/${symbol}/candles`);
|
|
998
|
+
url.searchParams.set("granularity", String(granularity));
|
|
999
|
+
url.searchParams.set("start", new Date(start).toISOString());
|
|
1000
|
+
url.searchParams.set("end", new Date(end).toISOString());
|
|
1001
|
+
const response = await fetchWithRetry3(url.toString(), {
|
|
1002
|
+
headers: {
|
|
1003
|
+
"User-Agent": "tradejs/market-data-ingest",
|
|
1004
|
+
Accept: "application/json"
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
if (response.status === 404) return [];
|
|
1008
|
+
if (!response.ok) {
|
|
1009
|
+
throw new Error(
|
|
1010
|
+
`Coinbase kline ${response.status}: ${await response.text()}`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
const payload = await response.json();
|
|
1014
|
+
return Array.isArray(payload) ? mapCoinbaseKline(payload) : [];
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
// src/marketData/providers/binanceCoinbaseSpread.ts
|
|
1020
|
+
import {
|
|
1021
|
+
alignSpreadRows,
|
|
1022
|
+
coinbaseProductFromSymbol,
|
|
1023
|
+
intervalToMs
|
|
1024
|
+
} from "@tradejs/core/indicators";
|
|
1025
|
+
var fetchBinanceKlines = async (params) => {
|
|
1026
|
+
const { symbol, interval, fromMs, toMs } = params;
|
|
1027
|
+
const rows = await spotKlineProviders.binance.kline({
|
|
1028
|
+
symbol,
|
|
1029
|
+
interval,
|
|
1030
|
+
start: fromMs,
|
|
1031
|
+
end: toMs
|
|
1032
|
+
});
|
|
1033
|
+
return rows.map((row) => ({ ts: row.timestamp, close: row.close }));
|
|
1034
|
+
};
|
|
1035
|
+
var fetchCoinbaseCandles = async (params) => {
|
|
1036
|
+
const { symbol, interval, fromMs, toMs } = params;
|
|
1037
|
+
const product = coinbaseProductFromSymbol(symbol);
|
|
1038
|
+
if (!product) return [];
|
|
1039
|
+
const stepMs = intervalToMs(interval) * 250;
|
|
1040
|
+
const rows = [];
|
|
1041
|
+
let cursor = fromMs;
|
|
1042
|
+
while (cursor <= toMs) {
|
|
1043
|
+
const endMs = Math.min(toMs, cursor + stepMs);
|
|
1044
|
+
const klineRows = await spotKlineProviders.coinbase.kline({
|
|
1045
|
+
symbol: product,
|
|
1046
|
+
interval,
|
|
1047
|
+
start: cursor,
|
|
1048
|
+
end: endMs
|
|
1049
|
+
});
|
|
1050
|
+
for (const row of klineRows) {
|
|
1051
|
+
rows.push({ ts: row.timestamp, close: row.close });
|
|
1052
|
+
}
|
|
1053
|
+
cursor = endMs + 1;
|
|
1054
|
+
}
|
|
1055
|
+
rows.sort((a, b) => a.ts - b.ts);
|
|
1056
|
+
const dedup = /* @__PURE__ */ new Map();
|
|
1057
|
+
for (const row of rows) dedup.set(row.ts, row);
|
|
1058
|
+
return [...dedup.values()].sort((a, b) => a.ts - b.ts);
|
|
1059
|
+
};
|
|
1060
|
+
var binanceCoinbaseSpreadProvider = {
|
|
1061
|
+
name: "binance_coinbase_spread",
|
|
1062
|
+
fetchWindow: async ({ symbol, interval, fromMs, toMs }) => {
|
|
1063
|
+
const [binance, coinbase] = await Promise.all([
|
|
1064
|
+
fetchBinanceKlines({ symbol, interval, fromMs, toMs }),
|
|
1065
|
+
fetchCoinbaseCandles({ symbol, interval, fromMs, toMs })
|
|
1066
|
+
]);
|
|
1067
|
+
const spreadRows = alignSpreadRows({
|
|
1068
|
+
symbol,
|
|
1069
|
+
interval,
|
|
1070
|
+
binance,
|
|
1071
|
+
coinbase,
|
|
1072
|
+
source: "binance_coinbase_spread"
|
|
1073
|
+
});
|
|
1074
|
+
return { spreadRows };
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
// src/marketData/providers/coinalyze.ts
|
|
1079
|
+
import { delay } from "@tradejs/core/async";
|
|
1080
|
+
import {
|
|
1081
|
+
coinalyzePointsToRows,
|
|
1082
|
+
mergeCoinalyzeMetrics
|
|
1083
|
+
} from "@tradejs/core/indicators";
|
|
1084
|
+
var coinalyzeIntervalMap = {
|
|
1085
|
+
"15m": "15min",
|
|
1086
|
+
"1h": "1hour"
|
|
1087
|
+
};
|
|
1088
|
+
var coinalyzeMinRequestDelayMs = Number(
|
|
1089
|
+
process.env.COINALYZE_MIN_REQUEST_DELAY_MS ?? 300
|
|
1090
|
+
);
|
|
1091
|
+
var coinalyzeMaxRetries = Number(process.env.COINALYZE_MAX_RETRIES ?? 4);
|
|
1092
|
+
var lastRequestTs = 0;
|
|
1093
|
+
var flattenCoinalyzeHistory = (raw) => {
|
|
1094
|
+
if (!Array.isArray(raw)) return [];
|
|
1095
|
+
const out = [];
|
|
1096
|
+
for (const item of raw) {
|
|
1097
|
+
if (!item || typeof item !== "object") continue;
|
|
1098
|
+
const history = item.history;
|
|
1099
|
+
if (Array.isArray(history)) {
|
|
1100
|
+
for (const point of history) {
|
|
1101
|
+
if (point && typeof point === "object") {
|
|
1102
|
+
out.push(point);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
out.push(item);
|
|
1108
|
+
}
|
|
1109
|
+
return out;
|
|
1110
|
+
};
|
|
1111
|
+
var normalizeMetricPayload = (metric, raw) => {
|
|
1112
|
+
const points = flattenCoinalyzeHistory(raw);
|
|
1113
|
+
if (!points.length) return [];
|
|
1114
|
+
if (metric === "oi") {
|
|
1115
|
+
return points.map((point) => ({
|
|
1116
|
+
...point,
|
|
1117
|
+
open_interest: point.open_interest ?? point.openInterest ?? point.oi ?? point.c
|
|
1118
|
+
}));
|
|
1119
|
+
}
|
|
1120
|
+
if (metric === "funding") {
|
|
1121
|
+
return points.map((point) => ({
|
|
1122
|
+
...point,
|
|
1123
|
+
funding_rate: point.funding_rate ?? point.fundingRate ?? point.rate ?? point.c
|
|
1124
|
+
}));
|
|
1125
|
+
}
|
|
1126
|
+
return points.map((point) => ({
|
|
1127
|
+
...point,
|
|
1128
|
+
liq_long: point.liq_long ?? point.long_liq ?? point.long ?? point.l,
|
|
1129
|
+
liq_short: point.liq_short ?? point.short_liq ?? point.short ?? point.s
|
|
1130
|
+
}));
|
|
1131
|
+
};
|
|
1132
|
+
var fetchCoinalyzeSeries = async (params) => {
|
|
1133
|
+
const { endpoint, metric, symbol, interval, fromMs, toMs } = params;
|
|
1134
|
+
const baseUrl = process.env.COINALYZE_BASE_URL?.trim() || "https://api.coinalyze.net/v1";
|
|
1135
|
+
const apiKey = process.env.COINALYZE_API_KEY?.trim();
|
|
1136
|
+
if (!apiKey) {
|
|
1137
|
+
throw new Error("Missing COINALYZE_API_KEY");
|
|
1138
|
+
}
|
|
1139
|
+
const url = new URL(`${baseUrl}${endpoint}`);
|
|
1140
|
+
url.searchParams.set("symbols", symbol);
|
|
1141
|
+
url.searchParams.set("interval", coinalyzeIntervalMap[interval] || interval);
|
|
1142
|
+
url.searchParams.set("from", String(Math.floor(fromMs / 1e3)));
|
|
1143
|
+
url.searchParams.set("to", String(Math.floor(toMs / 1e3)));
|
|
1144
|
+
const headers = {
|
|
1145
|
+
api_key: apiKey,
|
|
1146
|
+
"x-api-key": apiKey,
|
|
1147
|
+
Authorization: `Bearer ${apiKey}`
|
|
1148
|
+
};
|
|
1149
|
+
for (let attempt = 0; attempt <= coinalyzeMaxRetries; attempt += 1) {
|
|
1150
|
+
const now = Date.now();
|
|
1151
|
+
const waitMs = Math.max(
|
|
1152
|
+
0,
|
|
1153
|
+
lastRequestTs + coinalyzeMinRequestDelayMs - now
|
|
1154
|
+
);
|
|
1155
|
+
if (waitMs > 0) {
|
|
1156
|
+
await delay(waitMs);
|
|
1157
|
+
}
|
|
1158
|
+
lastRequestTs = Date.now();
|
|
1159
|
+
const response = await fetch(url.toString(), { headers });
|
|
1160
|
+
if (response.ok) {
|
|
1161
|
+
const raw = await response.json();
|
|
1162
|
+
return normalizeMetricPayload(metric, raw);
|
|
1163
|
+
}
|
|
1164
|
+
const text = await response.text();
|
|
1165
|
+
const retryAfterRaw = Number(response.headers.get("retry-after") ?? "");
|
|
1166
|
+
const retryAfterMs = Number.isFinite(retryAfterRaw) && retryAfterRaw > 0 ? retryAfterRaw * 1e3 : null;
|
|
1167
|
+
const transient = response.status === 429 || response.status >= 500;
|
|
1168
|
+
if (attempt < coinalyzeMaxRetries && transient) {
|
|
1169
|
+
const backoffMs = Math.min(1e4, 750 * 2 ** attempt);
|
|
1170
|
+
await delay(retryAfterMs ?? backoffMs);
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
throw new Error(`Coinalyze ${endpoint} ${response.status}: ${text}`);
|
|
1174
|
+
}
|
|
1175
|
+
return [];
|
|
1176
|
+
};
|
|
1177
|
+
var coinalyzeProvider = {
|
|
1178
|
+
name: "coinalyze",
|
|
1179
|
+
fetchWindow: async ({ symbol, marketSymbol, interval, fromMs, toMs }) => {
|
|
1180
|
+
const oiPath = process.env.COINALYZE_OI_PATH?.trim() || "/open-interest-history";
|
|
1181
|
+
const fundingPath = process.env.COINALYZE_FUNDING_PATH?.trim() || "/funding-rate-history";
|
|
1182
|
+
const liqPath = process.env.COINALYZE_LIQ_PATH?.trim() || "/liquidation-history";
|
|
1183
|
+
const requestSymbol = (marketSymbol || symbol).trim().toUpperCase();
|
|
1184
|
+
const oiRaw = await fetchCoinalyzeSeries({
|
|
1185
|
+
endpoint: oiPath,
|
|
1186
|
+
metric: "oi",
|
|
1187
|
+
symbol: requestSymbol,
|
|
1188
|
+
interval,
|
|
1189
|
+
fromMs,
|
|
1190
|
+
toMs
|
|
1191
|
+
});
|
|
1192
|
+
const fundingRaw = await fetchCoinalyzeSeries({
|
|
1193
|
+
endpoint: fundingPath,
|
|
1194
|
+
metric: "funding",
|
|
1195
|
+
symbol: requestSymbol,
|
|
1196
|
+
interval,
|
|
1197
|
+
fromMs,
|
|
1198
|
+
toMs
|
|
1199
|
+
});
|
|
1200
|
+
const liqRaw = await fetchCoinalyzeSeries({
|
|
1201
|
+
endpoint: liqPath,
|
|
1202
|
+
metric: "liq",
|
|
1203
|
+
symbol: requestSymbol,
|
|
1204
|
+
interval,
|
|
1205
|
+
fromMs,
|
|
1206
|
+
toMs
|
|
1207
|
+
});
|
|
1208
|
+
const points = mergeCoinalyzeMetrics({
|
|
1209
|
+
symbol,
|
|
1210
|
+
oiRaw,
|
|
1211
|
+
fundingRaw,
|
|
1212
|
+
liqRaw
|
|
1213
|
+
});
|
|
1214
|
+
return {
|
|
1215
|
+
derivativesRows: coinalyzePointsToRows(points, interval, "coinalyze")
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
// src/marketData/providers/index.ts
|
|
1221
|
+
var marketDataProviders = {
|
|
1222
|
+
coinalyze: coinalyzeProvider,
|
|
1223
|
+
binance_coinbase_spread: binanceCoinbaseSpreadProvider
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
// src/index.ts
|
|
1227
|
+
var ConnectorNames = /* @__PURE__ */ ((ConnectorNames2) => {
|
|
1228
|
+
ConnectorNames2["ByBit"] = "ByBit";
|
|
1229
|
+
ConnectorNames2["Binance"] = "Binance";
|
|
1230
|
+
ConnectorNames2["Coinbase"] = "Coinbase";
|
|
1231
|
+
ConnectorNames2["Test"] = "Test";
|
|
1232
|
+
return ConnectorNames2;
|
|
1233
|
+
})(ConnectorNames || {});
|
|
1234
|
+
var ConnectorProviders = /* @__PURE__ */ ((ConnectorProviders2) => {
|
|
1235
|
+
ConnectorProviders2["bybit"] = "bybit";
|
|
1236
|
+
ConnectorProviders2["binance"] = "binance";
|
|
1237
|
+
ConnectorProviders2["coinbase"] = "coinbase";
|
|
1238
|
+
return ConnectorProviders2;
|
|
1239
|
+
})(ConnectorProviders || {});
|
|
1240
|
+
var providerToConnectorName = {
|
|
1241
|
+
["bybit" /* bybit */]: "ByBit" /* ByBit */,
|
|
1242
|
+
["binance" /* binance */]: "Binance" /* Binance */,
|
|
1243
|
+
["coinbase" /* coinbase */]: "Coinbase" /* Coinbase */
|
|
1244
|
+
};
|
|
1245
|
+
var getConnectorProviders = () => Object.keys(providerToConnectorName);
|
|
1246
|
+
var resolveConnectorNameByProvider = (provider) => {
|
|
1247
|
+
const normalized = String(provider || "").trim().toLowerCase();
|
|
1248
|
+
return providerToConnectorName[normalized] || null;
|
|
1249
|
+
};
|
|
1250
|
+
var connectors = {
|
|
1251
|
+
["ByBit" /* ByBit */]: ByBitConnectorCreator,
|
|
1252
|
+
["Binance" /* Binance */]: BinanceConnectorCreator,
|
|
1253
|
+
["Coinbase" /* Coinbase */]: CoinbaseConnectorCreator,
|
|
1254
|
+
["Test" /* Test */]: TestConnectorCreator
|
|
1255
|
+
};
|
|
1256
|
+
var connectorEntries = [
|
|
1257
|
+
{
|
|
1258
|
+
name: "ByBit" /* ByBit */,
|
|
1259
|
+
creator: ByBitConnectorCreator,
|
|
1260
|
+
providers: ["bybit" /* bybit */]
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
name: "Binance" /* Binance */,
|
|
1264
|
+
creator: BinanceConnectorCreator,
|
|
1265
|
+
providers: ["binance" /* binance */]
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
name: "Coinbase" /* Coinbase */,
|
|
1269
|
+
creator: CoinbaseConnectorCreator,
|
|
1270
|
+
providers: ["coinbase" /* coinbase */]
|
|
1271
|
+
}
|
|
1272
|
+
];
|
|
1273
|
+
var getConnectorCreatorByProvider = (provider) => {
|
|
1274
|
+
const connectorName = resolveConnectorNameByProvider(provider);
|
|
1275
|
+
if (!connectorName) {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
return connectors[connectorName];
|
|
1279
|
+
};
|
|
1280
|
+
var index_default = { connectorEntries };
|
|
1281
|
+
export {
|
|
1282
|
+
ConnectorNames,
|
|
1283
|
+
ConnectorProviders,
|
|
1284
|
+
connectorEntries,
|
|
1285
|
+
connectors,
|
|
1286
|
+
index_default as default,
|
|
1287
|
+
getConnectorCreatorByProvider,
|
|
1288
|
+
getConnectorProviders,
|
|
1289
|
+
marketDataProviders,
|
|
1290
|
+
providerToConnectorName,
|
|
1291
|
+
resolveConnectorNameByProvider,
|
|
1292
|
+
spotKlineProviders
|
|
1293
|
+
};
|