backtest-kit 1.11.7 → 1.11.9
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/build/index.cjs +391 -279
- package/build/index.mjs +391 -280
- package/package.json +1 -1
- package/types.d.ts +179 -67
package/build/index.cjs
CHANGED
|
@@ -414,6 +414,21 @@ const GLOBAL_CONFIG = {
|
|
|
414
414
|
* Default: 0.2% (additional buffer above costs to ensure no loss when moving to breakeven)
|
|
415
415
|
*/
|
|
416
416
|
CC_BREAKEVEN_THRESHOLD: 0.2,
|
|
417
|
+
/**
|
|
418
|
+
* Time offset in minutes for order book fetching.
|
|
419
|
+
* Subtracts this amount from the current time when fetching order book data.
|
|
420
|
+
* This helps get a more stable snapshot of the order book by avoiding real-time volatility.
|
|
421
|
+
*
|
|
422
|
+
* Default: 10 minutes
|
|
423
|
+
*/
|
|
424
|
+
CC_ORDER_BOOK_TIME_OFFSET_MINUTES: 10,
|
|
425
|
+
/**
|
|
426
|
+
* Maximum depth levels for order book fetching.
|
|
427
|
+
* Specifies how many price levels to fetch from both bids and asks.
|
|
428
|
+
*
|
|
429
|
+
* Default: 20 levels
|
|
430
|
+
*/
|
|
431
|
+
CC_ORDER_BOOK_MAX_DEPTH_LEVELS: 1000,
|
|
417
432
|
};
|
|
418
433
|
const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
|
|
419
434
|
|
|
@@ -880,8 +895,62 @@ class ClientExchange {
|
|
|
880
895
|
});
|
|
881
896
|
return await this.params.formatPrice(symbol, price);
|
|
882
897
|
}
|
|
898
|
+
/**
|
|
899
|
+
* Fetches order book for a trading pair.
|
|
900
|
+
*
|
|
901
|
+
* Calculates time range based on execution context time (when) and
|
|
902
|
+
* CC_ORDER_BOOK_TIME_OFFSET_MINUTES, then delegates to the exchange
|
|
903
|
+
* schema implementation which may use or ignore the time range.
|
|
904
|
+
*
|
|
905
|
+
* @param symbol - Trading pair symbol
|
|
906
|
+
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
907
|
+
* @returns Promise resolving to order book data
|
|
908
|
+
* @throws Error if getOrderBook is not implemented
|
|
909
|
+
*/
|
|
910
|
+
async getOrderBook(symbol, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) {
|
|
911
|
+
this.params.logger.debug("ClientExchange getOrderBook", {
|
|
912
|
+
symbol,
|
|
913
|
+
depth,
|
|
914
|
+
});
|
|
915
|
+
const to = new Date(this.params.execution.context.when.getTime());
|
|
916
|
+
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * 60 * 1000);
|
|
917
|
+
return await this.params.getOrderBook(symbol, depth, from, to);
|
|
918
|
+
}
|
|
883
919
|
}
|
|
884
920
|
|
|
921
|
+
/**
|
|
922
|
+
* Default implementation for getCandles.
|
|
923
|
+
* Throws an error indicating the method is not implemented.
|
|
924
|
+
*/
|
|
925
|
+
const DEFAULT_GET_CANDLES_FN$1 = async (_symbol, _interval, _since, _limit) => {
|
|
926
|
+
throw new Error(`getCandles is not implemented for this exchange`);
|
|
927
|
+
};
|
|
928
|
+
/**
|
|
929
|
+
* Default implementation for formatQuantity.
|
|
930
|
+
* Returns Bitcoin precision on Binance (8 decimal places).
|
|
931
|
+
*/
|
|
932
|
+
const DEFAULT_FORMAT_QUANTITY_FN$1 = async (_symbol, quantity) => {
|
|
933
|
+
return quantity.toFixed(8);
|
|
934
|
+
};
|
|
935
|
+
/**
|
|
936
|
+
* Default implementation for formatPrice.
|
|
937
|
+
* Returns Bitcoin precision on Binance (2 decimal places).
|
|
938
|
+
*/
|
|
939
|
+
const DEFAULT_FORMAT_PRICE_FN$1 = async (_symbol, price) => {
|
|
940
|
+
return price.toFixed(2);
|
|
941
|
+
};
|
|
942
|
+
/**
|
|
943
|
+
* Default implementation for getOrderBook.
|
|
944
|
+
* Throws an error indicating the method is not implemented.
|
|
945
|
+
*
|
|
946
|
+
* @param _symbol - Trading pair symbol (unused)
|
|
947
|
+
* @param _depth - Maximum depth levels (unused)
|
|
948
|
+
* @param _from - Start of time range (unused - can be ignored in live implementations)
|
|
949
|
+
* @param _to - End of time range (unused - can be ignored in live implementations)
|
|
950
|
+
*/
|
|
951
|
+
const DEFAULT_GET_ORDER_BOOK_FN$1 = async (_symbol, _depth, _from, _to) => {
|
|
952
|
+
throw new Error(`getOrderBook is not implemented for this exchange`);
|
|
953
|
+
};
|
|
885
954
|
/**
|
|
886
955
|
* Connection service routing exchange operations to correct ClientExchange instance.
|
|
887
956
|
*
|
|
@@ -920,7 +989,7 @@ class ExchangeConnectionService {
|
|
|
920
989
|
* @returns Configured ClientExchange instance
|
|
921
990
|
*/
|
|
922
991
|
this.getExchange = functoolsKit.memoize(([exchangeName]) => `${exchangeName}`, (exchangeName) => {
|
|
923
|
-
const { getCandles, formatPrice, formatQuantity, callbacks } = this.exchangeSchemaService.get(exchangeName);
|
|
992
|
+
const { getCandles = DEFAULT_GET_CANDLES_FN$1, formatPrice = DEFAULT_FORMAT_PRICE_FN$1, formatQuantity = DEFAULT_FORMAT_QUANTITY_FN$1, getOrderBook = DEFAULT_GET_ORDER_BOOK_FN$1, callbacks } = this.exchangeSchemaService.get(exchangeName);
|
|
924
993
|
return new ClientExchange({
|
|
925
994
|
execution: this.executionContextService,
|
|
926
995
|
logger: this.loggerService,
|
|
@@ -928,6 +997,7 @@ class ExchangeConnectionService {
|
|
|
928
997
|
getCandles,
|
|
929
998
|
formatPrice,
|
|
930
999
|
formatQuantity,
|
|
1000
|
+
getOrderBook,
|
|
931
1001
|
callbacks,
|
|
932
1002
|
});
|
|
933
1003
|
});
|
|
@@ -1015,6 +1085,24 @@ class ExchangeConnectionService {
|
|
|
1015
1085
|
});
|
|
1016
1086
|
return await this.getExchange(this.methodContextService.context.exchangeName).formatQuantity(symbol, quantity);
|
|
1017
1087
|
};
|
|
1088
|
+
/**
|
|
1089
|
+
* Fetches order book for a trading pair using configured exchange.
|
|
1090
|
+
*
|
|
1091
|
+
* Routes to exchange determined by methodContextService.context.exchangeName.
|
|
1092
|
+
* The ClientExchange will calculate time range and pass it to the schema
|
|
1093
|
+
* implementation, which may use (backtest) or ignore (live) the parameters.
|
|
1094
|
+
*
|
|
1095
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
1096
|
+
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
1097
|
+
* @returns Promise resolving to order book data
|
|
1098
|
+
*/
|
|
1099
|
+
this.getOrderBook = async (symbol, depth) => {
|
|
1100
|
+
this.loggerService.log("exchangeConnectionService getOrderBook", {
|
|
1101
|
+
symbol,
|
|
1102
|
+
depth,
|
|
1103
|
+
});
|
|
1104
|
+
return await this.getExchange(this.methodContextService.context.exchangeName).getOrderBook(symbol, depth);
|
|
1105
|
+
};
|
|
1018
1106
|
}
|
|
1019
1107
|
}
|
|
1020
1108
|
|
|
@@ -1284,7 +1372,7 @@ async function writeFileAtomic(file, data, options = {}) {
|
|
|
1284
1372
|
}
|
|
1285
1373
|
}
|
|
1286
1374
|
|
|
1287
|
-
var _a$2;
|
|
1375
|
+
var _a$2, _b$2;
|
|
1288
1376
|
const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
|
|
1289
1377
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
|
|
1290
1378
|
const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
|
|
@@ -1316,9 +1404,6 @@ const PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT = "PersistBase.waitForInit";
|
|
|
1316
1404
|
const PERSIST_BASE_METHOD_NAME_READ_VALUE = "PersistBase.readValue";
|
|
1317
1405
|
const PERSIST_BASE_METHOD_NAME_WRITE_VALUE = "PersistBase.writeValue";
|
|
1318
1406
|
const PERSIST_BASE_METHOD_NAME_HAS_VALUE = "PersistBase.hasValue";
|
|
1319
|
-
const PERSIST_BASE_METHOD_NAME_REMOVE_VALUE = "PersistBase.removeValue";
|
|
1320
|
-
const PERSIST_BASE_METHOD_NAME_REMOVE_ALL = "PersistBase.removeAll";
|
|
1321
|
-
const PERSIST_BASE_METHOD_NAME_VALUES = "PersistBase.values";
|
|
1322
1407
|
const PERSIST_BASE_METHOD_NAME_KEYS = "PersistBase.keys";
|
|
1323
1408
|
const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
|
|
1324
1409
|
const BASE_UNLINK_RETRY_COUNT = 5;
|
|
@@ -1371,254 +1456,119 @@ const BASE_WAIT_FOR_INIT_UNLINK_FN = async (filePath) => functoolsKit.trycatch(f
|
|
|
1371
1456
|
* const value = await persist.readValue("key1");
|
|
1372
1457
|
* ```
|
|
1373
1458
|
*/
|
|
1374
|
-
const PersistBase = functoolsKit.makeExtendable(class {
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
}
|
|
1391
|
-
/**
|
|
1392
|
-
* Computes file path for entity ID.
|
|
1393
|
-
*
|
|
1394
|
-
* @param entityId - Entity identifier
|
|
1395
|
-
* @returns Full file path to entity JSON file
|
|
1396
|
-
*/
|
|
1397
|
-
_getFilePath(entityId) {
|
|
1398
|
-
return path.join(this.baseDir, this.entityName, `${entityId}.json`);
|
|
1399
|
-
}
|
|
1400
|
-
async waitForInit(initial) {
|
|
1401
|
-
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
|
|
1402
|
-
entityName: this.entityName,
|
|
1403
|
-
initial,
|
|
1404
|
-
});
|
|
1405
|
-
await this[BASE_WAIT_FOR_INIT_SYMBOL]();
|
|
1406
|
-
}
|
|
1407
|
-
/**
|
|
1408
|
-
* Returns count of persisted entities.
|
|
1409
|
-
*
|
|
1410
|
-
* @returns Promise resolving to number of .json files in directory
|
|
1411
|
-
*/
|
|
1412
|
-
async getCount() {
|
|
1413
|
-
const files = await fs.readdir(this._directory);
|
|
1414
|
-
const { length } = files.filter((file) => file.endsWith(".json"));
|
|
1415
|
-
return length;
|
|
1416
|
-
}
|
|
1417
|
-
async readValue(entityId) {
|
|
1418
|
-
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
|
|
1419
|
-
entityName: this.entityName,
|
|
1420
|
-
entityId,
|
|
1421
|
-
});
|
|
1422
|
-
try {
|
|
1423
|
-
const filePath = this._getFilePath(entityId);
|
|
1424
|
-
const fileContent = await fs.readFile(filePath, "utf-8");
|
|
1425
|
-
return JSON.parse(fileContent);
|
|
1459
|
+
const PersistBase = functoolsKit.makeExtendable((_b$2 = class {
|
|
1460
|
+
/**
|
|
1461
|
+
* Creates new persistence instance.
|
|
1462
|
+
*
|
|
1463
|
+
* @param entityName - Unique entity type identifier
|
|
1464
|
+
* @param baseDir - Base directory for all entities (default: ./dump/data)
|
|
1465
|
+
*/
|
|
1466
|
+
constructor(entityName, baseDir = path.join(process.cwd(), "logs/data")) {
|
|
1467
|
+
this.entityName = entityName;
|
|
1468
|
+
this.baseDir = baseDir;
|
|
1469
|
+
this[_a$2] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
|
|
1470
|
+
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
|
|
1471
|
+
entityName: this.entityName,
|
|
1472
|
+
baseDir,
|
|
1473
|
+
});
|
|
1474
|
+
this._directory = path.join(this.baseDir, this.entityName);
|
|
1426
1475
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1476
|
+
/**
|
|
1477
|
+
* Computes file path for entity ID.
|
|
1478
|
+
*
|
|
1479
|
+
* @param entityId - Entity identifier
|
|
1480
|
+
* @returns Full file path to entity JSON file
|
|
1481
|
+
*/
|
|
1482
|
+
_getFilePath(entityId) {
|
|
1483
|
+
return path.join(this.baseDir, this.entityName, `${entityId}.json`);
|
|
1432
1484
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
try {
|
|
1440
|
-
const filePath = this._getFilePath(entityId);
|
|
1441
|
-
await fs.access(filePath);
|
|
1442
|
-
return true;
|
|
1485
|
+
async waitForInit(initial) {
|
|
1486
|
+
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
|
|
1487
|
+
entityName: this.entityName,
|
|
1488
|
+
initial,
|
|
1489
|
+
});
|
|
1490
|
+
await this[BASE_WAIT_FOR_INIT_SYMBOL]();
|
|
1443
1491
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1492
|
+
async readValue(entityId) {
|
|
1493
|
+
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
|
|
1494
|
+
entityName: this.entityName,
|
|
1495
|
+
entityId,
|
|
1496
|
+
});
|
|
1497
|
+
try {
|
|
1498
|
+
const filePath = this._getFilePath(entityId);
|
|
1499
|
+
const fileContent = await fs.readFile(filePath, "utf-8");
|
|
1500
|
+
return JSON.parse(fileContent);
|
|
1447
1501
|
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
entityName: this.entityName,
|
|
1454
|
-
entityId,
|
|
1455
|
-
});
|
|
1456
|
-
try {
|
|
1457
|
-
const filePath = this._getFilePath(entityId);
|
|
1458
|
-
const serializedData = JSON.stringify(entity);
|
|
1459
|
-
await writeFileAtomic(filePath, serializedData, "utf-8");
|
|
1460
|
-
}
|
|
1461
|
-
catch (error) {
|
|
1462
|
-
throw new Error(`Failed to write entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
/**
|
|
1466
|
-
* Removes entity from storage.
|
|
1467
|
-
*
|
|
1468
|
-
* @param entityId - Entity identifier to remove
|
|
1469
|
-
* @returns Promise that resolves when entity is deleted
|
|
1470
|
-
* @throws Error if entity not found or deletion fails
|
|
1471
|
-
*/
|
|
1472
|
-
async removeValue(entityId) {
|
|
1473
|
-
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_REMOVE_VALUE, {
|
|
1474
|
-
entityName: this.entityName,
|
|
1475
|
-
entityId,
|
|
1476
|
-
});
|
|
1477
|
-
try {
|
|
1478
|
-
const filePath = this._getFilePath(entityId);
|
|
1479
|
-
await fs.unlink(filePath);
|
|
1480
|
-
}
|
|
1481
|
-
catch (error) {
|
|
1482
|
-
if (error?.code === "ENOENT") {
|
|
1483
|
-
throw new Error(`Entity ${this.entityName}:${entityId} not found for deletion`);
|
|
1502
|
+
catch (error) {
|
|
1503
|
+
if (error?.code === "ENOENT") {
|
|
1504
|
+
throw new Error(`Entity ${this.entityName}:${entityId} not found`);
|
|
1505
|
+
}
|
|
1506
|
+
throw new Error(`Failed to read entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
|
|
1484
1507
|
}
|
|
1485
|
-
throw new Error(`Failed to remove entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
|
|
1486
1508
|
}
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
entityName: this.entityName,
|
|
1497
|
-
});
|
|
1498
|
-
try {
|
|
1499
|
-
const files = await fs.readdir(this._directory);
|
|
1500
|
-
const entityFiles = files.filter((file) => file.endsWith(".json"));
|
|
1501
|
-
for (const file of entityFiles) {
|
|
1502
|
-
await fs.unlink(path.join(this._directory, file));
|
|
1509
|
+
async hasValue(entityId) {
|
|
1510
|
+
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
|
|
1511
|
+
entityName: this.entityName,
|
|
1512
|
+
entityId,
|
|
1513
|
+
});
|
|
1514
|
+
try {
|
|
1515
|
+
const filePath = this._getFilePath(entityId);
|
|
1516
|
+
await fs.access(filePath);
|
|
1517
|
+
return true;
|
|
1503
1518
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
/**
|
|
1510
|
-
* Async generator yielding all entity values.
|
|
1511
|
-
* Sorted alphanumerically by entity ID.
|
|
1512
|
-
*
|
|
1513
|
-
* @returns AsyncGenerator yielding entities
|
|
1514
|
-
* @throws Error if reading fails
|
|
1515
|
-
*/
|
|
1516
|
-
async *values() {
|
|
1517
|
-
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_VALUES, {
|
|
1518
|
-
entityName: this.entityName,
|
|
1519
|
-
});
|
|
1520
|
-
try {
|
|
1521
|
-
const files = await fs.readdir(this._directory);
|
|
1522
|
-
const entityIds = files
|
|
1523
|
-
.filter((file) => file.endsWith(".json"))
|
|
1524
|
-
.map((file) => file.slice(0, -5))
|
|
1525
|
-
.sort((a, b) => a.localeCompare(b, undefined, {
|
|
1526
|
-
numeric: true,
|
|
1527
|
-
sensitivity: "base",
|
|
1528
|
-
}));
|
|
1529
|
-
for (const entityId of entityIds) {
|
|
1530
|
-
const entity = await this.readValue(entityId);
|
|
1531
|
-
yield entity;
|
|
1519
|
+
catch (error) {
|
|
1520
|
+
if (error?.code === "ENOENT") {
|
|
1521
|
+
return false;
|
|
1522
|
+
}
|
|
1523
|
+
throw new Error(`Failed to check existence of entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
|
|
1532
1524
|
}
|
|
1533
1525
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
* @throws Error if reading fails
|
|
1544
|
-
*/
|
|
1545
|
-
async *keys() {
|
|
1546
|
-
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
|
|
1547
|
-
entityName: this.entityName,
|
|
1548
|
-
});
|
|
1549
|
-
try {
|
|
1550
|
-
const files = await fs.readdir(this._directory);
|
|
1551
|
-
const entityIds = files
|
|
1552
|
-
.filter((file) => file.endsWith(".json"))
|
|
1553
|
-
.map((file) => file.slice(0, -5))
|
|
1554
|
-
.sort((a, b) => a.localeCompare(b, undefined, {
|
|
1555
|
-
numeric: true,
|
|
1556
|
-
sensitivity: "base",
|
|
1557
|
-
}));
|
|
1558
|
-
for (const entityId of entityIds) {
|
|
1559
|
-
yield entityId;
|
|
1526
|
+
async writeValue(entityId, entity) {
|
|
1527
|
+
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
|
|
1528
|
+
entityName: this.entityName,
|
|
1529
|
+
entityId,
|
|
1530
|
+
});
|
|
1531
|
+
try {
|
|
1532
|
+
const filePath = this._getFilePath(entityId);
|
|
1533
|
+
const serializedData = JSON.stringify(entity);
|
|
1534
|
+
await writeFileAtomic(filePath, serializedData, "utf-8");
|
|
1560
1535
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
throw new Error(`Failed to read keys for ${this.entityName}: ${functoolsKit.getErrorMessage(error)}`);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
/**
|
|
1567
|
-
* Async iterator implementation.
|
|
1568
|
-
* Delegates to values() generator.
|
|
1569
|
-
*
|
|
1570
|
-
* @returns AsyncIterableIterator yielding entities
|
|
1571
|
-
*/
|
|
1572
|
-
async *[(_a$2 = BASE_WAIT_FOR_INIT_SYMBOL, Symbol.asyncIterator)]() {
|
|
1573
|
-
for await (const entity of this.values()) {
|
|
1574
|
-
yield entity;
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
/**
|
|
1578
|
-
* Filters entities by predicate function.
|
|
1579
|
-
*
|
|
1580
|
-
* @param predicate - Filter function
|
|
1581
|
-
* @returns AsyncGenerator yielding filtered entities
|
|
1582
|
-
*/
|
|
1583
|
-
async *filter(predicate) {
|
|
1584
|
-
for await (const entity of this.values()) {
|
|
1585
|
-
if (predicate(entity)) {
|
|
1586
|
-
yield entity;
|
|
1536
|
+
catch (error) {
|
|
1537
|
+
throw new Error(`Failed to write entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
|
|
1587
1538
|
}
|
|
1588
1539
|
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1540
|
+
/**
|
|
1541
|
+
* Async generator yielding all entity IDs.
|
|
1542
|
+
* Sorted alphanumerically.
|
|
1543
|
+
* Used internally by waitForInit for validation.
|
|
1544
|
+
*
|
|
1545
|
+
* @returns AsyncGenerator yielding entity IDs
|
|
1546
|
+
* @throws Error if reading fails
|
|
1547
|
+
*/
|
|
1548
|
+
async *keys() {
|
|
1549
|
+
bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
|
|
1550
|
+
entityName: this.entityName,
|
|
1551
|
+
});
|
|
1552
|
+
try {
|
|
1553
|
+
const files = await fs.readdir(this._directory);
|
|
1554
|
+
const entityIds = files
|
|
1555
|
+
.filter((file) => file.endsWith(".json"))
|
|
1556
|
+
.map((file) => file.slice(0, -5))
|
|
1557
|
+
.sort((a, b) => a.localeCompare(b, undefined, {
|
|
1558
|
+
numeric: true,
|
|
1559
|
+
sensitivity: "base",
|
|
1560
|
+
}));
|
|
1561
|
+
for (const entityId of entityIds) {
|
|
1562
|
+
yield entityId;
|
|
1608
1563
|
}
|
|
1609
1564
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
for await (const entity of this.values()) {
|
|
1613
|
-
count += 1;
|
|
1614
|
-
yield entity;
|
|
1615
|
-
if (count >= total) {
|
|
1616
|
-
break;
|
|
1617
|
-
}
|
|
1565
|
+
catch (error) {
|
|
1566
|
+
throw new Error(`Failed to read keys for ${this.entityName}: ${functoolsKit.getErrorMessage(error)}`);
|
|
1618
1567
|
}
|
|
1619
1568
|
}
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1569
|
+
},
|
|
1570
|
+
_a$2 = BASE_WAIT_FOR_INIT_SYMBOL,
|
|
1571
|
+
_b$2));
|
|
1622
1572
|
/**
|
|
1623
1573
|
* Dummy persist adapter that discards all writes.
|
|
1624
1574
|
* Used for disabling persistence.
|
|
@@ -1650,12 +1600,6 @@ class PersistDummy {
|
|
|
1650
1600
|
*/
|
|
1651
1601
|
async writeValue() {
|
|
1652
1602
|
}
|
|
1653
|
-
/**
|
|
1654
|
-
* No-op keys generator.
|
|
1655
|
-
* @returns Empty async generator
|
|
1656
|
-
*/
|
|
1657
|
-
async *keys() {
|
|
1658
|
-
}
|
|
1659
1603
|
}
|
|
1660
1604
|
/**
|
|
1661
1605
|
* Utility class for managing signal persistence.
|
|
@@ -7541,6 +7485,38 @@ class ExchangeCoreService {
|
|
|
7541
7485
|
backtest,
|
|
7542
7486
|
});
|
|
7543
7487
|
};
|
|
7488
|
+
/**
|
|
7489
|
+
* Fetches order book with execution context.
|
|
7490
|
+
*
|
|
7491
|
+
* Sets up execution context with the provided when/backtest parameters.
|
|
7492
|
+
* The exchange implementation will receive time range parameters but may
|
|
7493
|
+
* choose to use them (backtest) or ignore them (live).
|
|
7494
|
+
*
|
|
7495
|
+
* @param symbol - Trading pair symbol
|
|
7496
|
+
* @param when - Timestamp for context
|
|
7497
|
+
* @param backtest - Whether running in backtest mode
|
|
7498
|
+
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
7499
|
+
* @returns Promise resolving to order book data
|
|
7500
|
+
*/
|
|
7501
|
+
this.getOrderBook = async (symbol, when, backtest, depth) => {
|
|
7502
|
+
this.loggerService.log("exchangeCoreService getOrderBook", {
|
|
7503
|
+
symbol,
|
|
7504
|
+
when,
|
|
7505
|
+
backtest,
|
|
7506
|
+
depth,
|
|
7507
|
+
});
|
|
7508
|
+
if (!MethodContextService.hasContext()) {
|
|
7509
|
+
throw new Error("exchangeCoreService getOrderBook requires a method context");
|
|
7510
|
+
}
|
|
7511
|
+
await this.validate(this.methodContextService.context.exchangeName);
|
|
7512
|
+
return await ExecutionContextService.runInContext(async () => {
|
|
7513
|
+
return await this.exchangeConnectionService.getOrderBook(symbol, depth);
|
|
7514
|
+
}, {
|
|
7515
|
+
symbol,
|
|
7516
|
+
when,
|
|
7517
|
+
backtest,
|
|
7518
|
+
});
|
|
7519
|
+
};
|
|
7544
7520
|
}
|
|
7545
7521
|
}
|
|
7546
7522
|
|
|
@@ -8187,12 +8163,6 @@ class ExchangeSchemaService {
|
|
|
8187
8163
|
if (typeof exchangeSchema.getCandles !== "function") {
|
|
8188
8164
|
throw new Error(`exchange schema validation failed: missing getCandles for exchangeName=${exchangeSchema.exchangeName}`);
|
|
8189
8165
|
}
|
|
8190
|
-
if (typeof exchangeSchema.formatPrice !== "function") {
|
|
8191
|
-
throw new Error(`exchange schema validation failed: missing formatPrice for exchangeName=${exchangeSchema.exchangeName}`);
|
|
8192
|
-
}
|
|
8193
|
-
if (typeof exchangeSchema.formatQuantity !== "function") {
|
|
8194
|
-
throw new Error(`exchange schema validation failed: missing formatQuantity for exchangeName=${exchangeSchema.exchangeName}`);
|
|
8195
|
-
}
|
|
8196
8166
|
};
|
|
8197
8167
|
/**
|
|
8198
8168
|
* Overrides an existing exchange schema with partial updates.
|
|
@@ -10062,21 +10032,21 @@ const live_columns = [
|
|
|
10062
10032
|
{
|
|
10063
10033
|
key: "openPrice",
|
|
10064
10034
|
label: "Open Price",
|
|
10065
|
-
format: (data) => data.
|
|
10035
|
+
format: (data) => data.priceOpen !== undefined ? `${data.priceOpen.toFixed(8)} USD` : "N/A",
|
|
10066
10036
|
isVisible: () => true,
|
|
10067
10037
|
},
|
|
10068
10038
|
{
|
|
10069
10039
|
key: "takeProfit",
|
|
10070
10040
|
label: "Take Profit",
|
|
10071
|
-
format: (data) => data.
|
|
10072
|
-
? `${data.
|
|
10041
|
+
format: (data) => data.priceTakeProfit !== undefined
|
|
10042
|
+
? `${data.priceTakeProfit.toFixed(8)} USD`
|
|
10073
10043
|
: "N/A",
|
|
10074
10044
|
isVisible: () => true,
|
|
10075
10045
|
},
|
|
10076
10046
|
{
|
|
10077
10047
|
key: "stopLoss",
|
|
10078
10048
|
label: "Stop Loss",
|
|
10079
|
-
format: (data) => data.
|
|
10049
|
+
format: (data) => data.priceStopLoss !== undefined ? `${data.priceStopLoss.toFixed(8)} USD` : "N/A",
|
|
10080
10050
|
isVisible: () => true,
|
|
10081
10051
|
},
|
|
10082
10052
|
{
|
|
@@ -10760,13 +10730,13 @@ const schedule_columns = [
|
|
|
10760
10730
|
{
|
|
10761
10731
|
key: "takeProfit",
|
|
10762
10732
|
label: "Take Profit",
|
|
10763
|
-
format: (data) => `${data.
|
|
10733
|
+
format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
|
|
10764
10734
|
isVisible: () => true,
|
|
10765
10735
|
},
|
|
10766
10736
|
{
|
|
10767
10737
|
key: "stopLoss",
|
|
10768
10738
|
label: "Stop Loss",
|
|
10769
|
-
format: (data) => `${data.
|
|
10739
|
+
format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
|
|
10770
10740
|
isVisible: () => true,
|
|
10771
10741
|
},
|
|
10772
10742
|
{
|
|
@@ -12065,9 +12035,9 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
12065
12035
|
position: data.signal.position,
|
|
12066
12036
|
note: data.signal.note,
|
|
12067
12037
|
currentPrice: data.signal.priceOpen,
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12038
|
+
priceOpen: data.signal.priceOpen,
|
|
12039
|
+
priceTakeProfit: data.signal.priceTakeProfit,
|
|
12040
|
+
priceStopLoss: data.signal.priceStopLoss,
|
|
12071
12041
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
12072
12042
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
12073
12043
|
totalExecuted: data.signal.totalExecuted,
|
|
@@ -12092,9 +12062,9 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
12092
12062
|
position: data.signal.position,
|
|
12093
12063
|
note: data.signal.note,
|
|
12094
12064
|
currentPrice: data.currentPrice,
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12065
|
+
priceOpen: data.signal.priceOpen,
|
|
12066
|
+
priceTakeProfit: data.signal.priceTakeProfit,
|
|
12067
|
+
priceStopLoss: data.signal.priceStopLoss,
|
|
12098
12068
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
12099
12069
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
12100
12070
|
totalExecuted: data.signal.totalExecuted,
|
|
@@ -12132,9 +12102,9 @@ let ReportStorage$5 = class ReportStorage {
|
|
|
12132
12102
|
position: data.signal.position,
|
|
12133
12103
|
note: data.signal.note,
|
|
12134
12104
|
currentPrice: data.currentPrice,
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
12105
|
+
priceOpen: data.signal.priceOpen,
|
|
12106
|
+
priceTakeProfit: data.signal.priceTakeProfit,
|
|
12107
|
+
priceStopLoss: data.signal.priceStopLoss,
|
|
12138
12108
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
12139
12109
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
12140
12110
|
totalExecuted: data.signal.totalExecuted,
|
|
@@ -12619,8 +12589,8 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
12619
12589
|
note: data.signal.note,
|
|
12620
12590
|
currentPrice: data.currentPrice,
|
|
12621
12591
|
priceOpen: data.signal.priceOpen,
|
|
12622
|
-
|
|
12623
|
-
|
|
12592
|
+
priceTakeProfit: data.signal.priceTakeProfit,
|
|
12593
|
+
priceStopLoss: data.signal.priceStopLoss,
|
|
12624
12594
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
12625
12595
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
12626
12596
|
totalExecuted: data.signal.totalExecuted,
|
|
@@ -12647,8 +12617,8 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
12647
12617
|
note: data.signal.note,
|
|
12648
12618
|
currentPrice: data.currentPrice,
|
|
12649
12619
|
priceOpen: data.signal.priceOpen,
|
|
12650
|
-
|
|
12651
|
-
|
|
12620
|
+
priceTakeProfit: data.signal.priceTakeProfit,
|
|
12621
|
+
priceStopLoss: data.signal.priceStopLoss,
|
|
12652
12622
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
12653
12623
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
12654
12624
|
totalExecuted: data.signal.totalExecuted,
|
|
@@ -12677,8 +12647,8 @@ let ReportStorage$4 = class ReportStorage {
|
|
|
12677
12647
|
note: data.signal.note,
|
|
12678
12648
|
currentPrice: data.currentPrice,
|
|
12679
12649
|
priceOpen: data.signal.priceOpen,
|
|
12680
|
-
|
|
12681
|
-
|
|
12650
|
+
priceTakeProfit: data.signal.priceTakeProfit,
|
|
12651
|
+
priceStopLoss: data.signal.priceStopLoss,
|
|
12682
12652
|
originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
|
|
12683
12653
|
originalPriceStopLoss: data.signal.originalPriceStopLoss,
|
|
12684
12654
|
totalExecuted: data.signal.totalExecuted,
|
|
@@ -13716,11 +13686,11 @@ let ReportStorage$3 = class ReportStorage {
|
|
|
13716
13686
|
await Markdown.writeData("walker", markdown, {
|
|
13717
13687
|
path,
|
|
13718
13688
|
file: filename,
|
|
13719
|
-
symbol:
|
|
13689
|
+
symbol: symbol,
|
|
13720
13690
|
signalId: "",
|
|
13721
13691
|
strategyName: "",
|
|
13722
|
-
exchangeName:
|
|
13723
|
-
frameName:
|
|
13692
|
+
exchangeName: context.exchangeName,
|
|
13693
|
+
frameName: context.frameName
|
|
13724
13694
|
});
|
|
13725
13695
|
}
|
|
13726
13696
|
};
|
|
@@ -18815,6 +18785,9 @@ class ConfigValidationService {
|
|
|
18815
18785
|
if (!Number.isInteger(GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) || GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST <= 0) {
|
|
18816
18786
|
errors.push(`CC_MAX_CANDLES_PER_REQUEST must be a positive integer, got ${GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST}`);
|
|
18817
18787
|
}
|
|
18788
|
+
if (!Number.isInteger(GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES) || GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES < 0) {
|
|
18789
|
+
errors.push(`CC_ORDER_BOOK_TIME_OFFSET_MINUTES must be a non-negative integer, got ${GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES}`);
|
|
18790
|
+
}
|
|
18818
18791
|
// Throw aggregated errors if any
|
|
18819
18792
|
if (errors.length > 0) {
|
|
18820
18793
|
const errorMessage = `GLOBAL_CONFIG validation failed:\n${errors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`;
|
|
@@ -21406,6 +21379,7 @@ const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
|
|
|
21406
21379
|
const GET_DATE_METHOD_NAME = "exchange.getDate";
|
|
21407
21380
|
const GET_MODE_METHOD_NAME = "exchange.getMode";
|
|
21408
21381
|
const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
|
|
21382
|
+
const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
|
|
21409
21383
|
/**
|
|
21410
21384
|
* Checks if trade context is active (execution and method contexts).
|
|
21411
21385
|
*
|
|
@@ -21586,6 +21560,41 @@ async function getMode() {
|
|
|
21586
21560
|
const { backtest: bt$1 } = bt.executionContextService.context;
|
|
21587
21561
|
return bt$1 ? "backtest" : "live";
|
|
21588
21562
|
}
|
|
21563
|
+
/**
|
|
21564
|
+
* Fetches order book for a trading pair from the registered exchange.
|
|
21565
|
+
*
|
|
21566
|
+
* Uses current execution context to determine timing. The underlying exchange
|
|
21567
|
+
* implementation receives time range parameters but may use them (backtest)
|
|
21568
|
+
* or ignore them (live trading).
|
|
21569
|
+
*
|
|
21570
|
+
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
21571
|
+
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
21572
|
+
* @returns Promise resolving to order book data
|
|
21573
|
+
* @throws Error if execution or method context is missing
|
|
21574
|
+
*
|
|
21575
|
+
* @example
|
|
21576
|
+
* ```typescript
|
|
21577
|
+
* const orderBook = await getOrderBook("BTCUSDT");
|
|
21578
|
+
* console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
|
|
21579
|
+
* console.log(orderBook.asks); // [{ price: "50001.00", quantity: "0.3" }, ...]
|
|
21580
|
+
*
|
|
21581
|
+
* // Fetch deeper order book
|
|
21582
|
+
* const deepBook = await getOrderBook("BTCUSDT", 100);
|
|
21583
|
+
* ```
|
|
21584
|
+
*/
|
|
21585
|
+
async function getOrderBook(symbol, depth) {
|
|
21586
|
+
bt.loggerService.info(GET_ORDER_BOOK_METHOD_NAME, {
|
|
21587
|
+
symbol,
|
|
21588
|
+
depth,
|
|
21589
|
+
});
|
|
21590
|
+
if (!ExecutionContextService.hasContext()) {
|
|
21591
|
+
throw new Error("getOrderBook requires an execution context");
|
|
21592
|
+
}
|
|
21593
|
+
if (!MethodContextService.hasContext()) {
|
|
21594
|
+
throw new Error("getOrderBook requires a method context");
|
|
21595
|
+
}
|
|
21596
|
+
return await bt.exchangeConnectionService.getOrderBook(symbol, depth);
|
|
21597
|
+
}
|
|
21589
21598
|
|
|
21590
21599
|
const STOP_METHOD_NAME = "strategy.stop";
|
|
21591
21600
|
const CANCEL_METHOD_NAME = "strategy.cancel";
|
|
@@ -27161,6 +27170,40 @@ const EXCHANGE_METHOD_NAME_GET_CANDLES = "ExchangeUtils.getCandles";
|
|
|
27161
27170
|
const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
|
|
27162
27171
|
const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
|
|
27163
27172
|
const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
|
|
27173
|
+
const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
|
|
27174
|
+
/**
|
|
27175
|
+
* Default implementation for getCandles.
|
|
27176
|
+
* Throws an error indicating the method is not implemented.
|
|
27177
|
+
*/
|
|
27178
|
+
const DEFAULT_GET_CANDLES_FN = async (_symbol, _interval, _since, _limit) => {
|
|
27179
|
+
throw new Error(`getCandles is not implemented for this exchange`);
|
|
27180
|
+
};
|
|
27181
|
+
/**
|
|
27182
|
+
* Default implementation for formatQuantity.
|
|
27183
|
+
* Returns Bitcoin precision on Binance (8 decimal places).
|
|
27184
|
+
*/
|
|
27185
|
+
const DEFAULT_FORMAT_QUANTITY_FN = async (_symbol, quantity) => {
|
|
27186
|
+
return quantity.toFixed(8);
|
|
27187
|
+
};
|
|
27188
|
+
/**
|
|
27189
|
+
* Default implementation for formatPrice.
|
|
27190
|
+
* Returns Bitcoin precision on Binance (2 decimal places).
|
|
27191
|
+
*/
|
|
27192
|
+
const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price) => {
|
|
27193
|
+
return price.toFixed(2);
|
|
27194
|
+
};
|
|
27195
|
+
/**
|
|
27196
|
+
* Default implementation for getOrderBook.
|
|
27197
|
+
* Throws an error indicating the method is not implemented.
|
|
27198
|
+
*
|
|
27199
|
+
* @param _symbol - Trading pair symbol (unused)
|
|
27200
|
+
* @param _depth - Maximum depth levels (unused)
|
|
27201
|
+
* @param _from - Start of time range (unused - can be ignored in live implementations)
|
|
27202
|
+
* @param _to - End of time range (unused - can be ignored in live implementations)
|
|
27203
|
+
*/
|
|
27204
|
+
const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to) => {
|
|
27205
|
+
throw new Error(`getOrderBook is not implemented for this exchange`);
|
|
27206
|
+
};
|
|
27164
27207
|
const INTERVAL_MINUTES$1 = {
|
|
27165
27208
|
"1m": 1,
|
|
27166
27209
|
"3m": 3,
|
|
@@ -27173,6 +27216,25 @@ const INTERVAL_MINUTES$1 = {
|
|
|
27173
27216
|
"6h": 360,
|
|
27174
27217
|
"8h": 480,
|
|
27175
27218
|
};
|
|
27219
|
+
/**
|
|
27220
|
+
* Creates exchange instance with methods resolved once during construction.
|
|
27221
|
+
* Applies default implementations where schema methods are not provided.
|
|
27222
|
+
*
|
|
27223
|
+
* @param schema - Exchange schema from registry
|
|
27224
|
+
* @returns Object with resolved exchange methods
|
|
27225
|
+
*/
|
|
27226
|
+
const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
|
|
27227
|
+
const getCandles = schema.getCandles ?? DEFAULT_GET_CANDLES_FN;
|
|
27228
|
+
const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
|
|
27229
|
+
const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
|
|
27230
|
+
const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
|
|
27231
|
+
return {
|
|
27232
|
+
getCandles,
|
|
27233
|
+
formatQuantity,
|
|
27234
|
+
formatPrice,
|
|
27235
|
+
getOrderBook,
|
|
27236
|
+
};
|
|
27237
|
+
};
|
|
27176
27238
|
/**
|
|
27177
27239
|
* Instance class for exchange operations on a specific exchange.
|
|
27178
27240
|
*
|
|
@@ -27222,6 +27284,7 @@ class ExchangeInstance {
|
|
|
27222
27284
|
interval,
|
|
27223
27285
|
limit,
|
|
27224
27286
|
});
|
|
27287
|
+
const getCandles = this._methods.getCandles;
|
|
27225
27288
|
const step = INTERVAL_MINUTES$1[interval];
|
|
27226
27289
|
const adjust = step * limit - step;
|
|
27227
27290
|
if (!adjust) {
|
|
@@ -27236,7 +27299,7 @@ class ExchangeInstance {
|
|
|
27236
27299
|
let currentSince = new Date(since.getTime());
|
|
27237
27300
|
while (remaining > 0) {
|
|
27238
27301
|
const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
|
|
27239
|
-
const chunkData = await
|
|
27302
|
+
const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit);
|
|
27240
27303
|
allData.push(...chunkData);
|
|
27241
27304
|
remaining -= chunkLimit;
|
|
27242
27305
|
if (remaining > 0) {
|
|
@@ -27246,7 +27309,7 @@ class ExchangeInstance {
|
|
|
27246
27309
|
}
|
|
27247
27310
|
}
|
|
27248
27311
|
else {
|
|
27249
|
-
allData = await
|
|
27312
|
+
allData = await getCandles(symbol, interval, since, limit);
|
|
27250
27313
|
}
|
|
27251
27314
|
// Filter candles to strictly match the requested range
|
|
27252
27315
|
const whenTimestamp = when.getTime();
|
|
@@ -27313,7 +27376,7 @@ class ExchangeInstance {
|
|
|
27313
27376
|
* ```typescript
|
|
27314
27377
|
* const instance = new ExchangeInstance("binance");
|
|
27315
27378
|
* const formatted = await instance.formatQuantity("BTCUSDT", 0.001);
|
|
27316
|
-
* console.log(formatted); // "0.
|
|
27379
|
+
* console.log(formatted); // "0.00100000"
|
|
27317
27380
|
* ```
|
|
27318
27381
|
*/
|
|
27319
27382
|
this.formatQuantity = async (symbol, quantity) => {
|
|
@@ -27322,7 +27385,7 @@ class ExchangeInstance {
|
|
|
27322
27385
|
symbol,
|
|
27323
27386
|
quantity,
|
|
27324
27387
|
});
|
|
27325
|
-
return await this.
|
|
27388
|
+
return await this._methods.formatQuantity(symbol, quantity);
|
|
27326
27389
|
};
|
|
27327
27390
|
/**
|
|
27328
27391
|
* Format price according to exchange precision rules.
|
|
@@ -27344,9 +27407,40 @@ class ExchangeInstance {
|
|
|
27344
27407
|
symbol,
|
|
27345
27408
|
price,
|
|
27346
27409
|
});
|
|
27347
|
-
return await this.
|
|
27410
|
+
return await this._methods.formatPrice(symbol, price);
|
|
27348
27411
|
};
|
|
27349
|
-
|
|
27412
|
+
/**
|
|
27413
|
+
* Fetch order book for a trading pair.
|
|
27414
|
+
*
|
|
27415
|
+
* Calculates time range using CC_ORDER_BOOK_TIME_OFFSET_MINUTES (default 10 minutes)
|
|
27416
|
+
* and passes it to the exchange schema implementation. The implementation may use
|
|
27417
|
+
* the time range (backtest) or ignore it (live trading).
|
|
27418
|
+
*
|
|
27419
|
+
* @param symbol - Trading pair symbol
|
|
27420
|
+
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
27421
|
+
* @returns Promise resolving to order book data
|
|
27422
|
+
* @throws Error if getOrderBook is not implemented
|
|
27423
|
+
*
|
|
27424
|
+
* @example
|
|
27425
|
+
* ```typescript
|
|
27426
|
+
* const instance = new ExchangeInstance("binance");
|
|
27427
|
+
* const orderBook = await instance.getOrderBook("BTCUSDT");
|
|
27428
|
+
* console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
|
|
27429
|
+
* const deepOrderBook = await instance.getOrderBook("BTCUSDT", 100);
|
|
27430
|
+
* ```
|
|
27431
|
+
*/
|
|
27432
|
+
this.getOrderBook = async (symbol, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) => {
|
|
27433
|
+
bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_ORDER_BOOK, {
|
|
27434
|
+
exchangeName: this.exchangeName,
|
|
27435
|
+
symbol,
|
|
27436
|
+
depth,
|
|
27437
|
+
});
|
|
27438
|
+
const to = new Date(Date.now());
|
|
27439
|
+
const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * 60 * 1000);
|
|
27440
|
+
return await this._methods.getOrderBook(symbol, depth, from, to);
|
|
27441
|
+
};
|
|
27442
|
+
const schema = bt.exchangeSchemaService.get(this.exchangeName);
|
|
27443
|
+
this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
|
|
27350
27444
|
}
|
|
27351
27445
|
}
|
|
27352
27446
|
/**
|
|
@@ -27432,6 +27526,23 @@ class ExchangeUtils {
|
|
|
27432
27526
|
const instance = this._getInstance(context.exchangeName);
|
|
27433
27527
|
return await instance.formatPrice(symbol, price);
|
|
27434
27528
|
};
|
|
27529
|
+
/**
|
|
27530
|
+
* Fetch order book for a trading pair.
|
|
27531
|
+
*
|
|
27532
|
+
* Delegates to ExchangeInstance which calculates time range and passes it
|
|
27533
|
+
* to the exchange schema implementation. The from/to parameters may be used
|
|
27534
|
+
* (backtest) or ignored (live) depending on the implementation.
|
|
27535
|
+
*
|
|
27536
|
+
* @param symbol - Trading pair symbol
|
|
27537
|
+
* @param context - Execution context with exchange name
|
|
27538
|
+
* @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
|
|
27539
|
+
* @returns Promise resolving to order book data
|
|
27540
|
+
*/
|
|
27541
|
+
this.getOrderBook = async (symbol, context, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) => {
|
|
27542
|
+
bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_ORDER_BOOK);
|
|
27543
|
+
const instance = this._getInstance(context.exchangeName);
|
|
27544
|
+
return await instance.getOrderBook(symbol, depth);
|
|
27545
|
+
};
|
|
27435
27546
|
}
|
|
27436
27547
|
}
|
|
27437
27548
|
/**
|
|
@@ -28425,6 +28536,7 @@ exports.getDate = getDate;
|
|
|
28425
28536
|
exports.getDefaultColumns = getDefaultColumns;
|
|
28426
28537
|
exports.getDefaultConfig = getDefaultConfig;
|
|
28427
28538
|
exports.getMode = getMode;
|
|
28539
|
+
exports.getOrderBook = getOrderBook;
|
|
28428
28540
|
exports.hasTradeContext = hasTradeContext;
|
|
28429
28541
|
exports.lib = backtest;
|
|
28430
28542
|
exports.listExchanges = listExchanges;
|