backtest-kit 1.11.7 → 1.11.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/build/index.cjs CHANGED
@@ -414,6 +414,14 @@ 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,
417
425
  };
418
426
  const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
419
427
 
@@ -880,8 +888,51 @@ class ClientExchange {
880
888
  });
881
889
  return await this.params.formatPrice(symbol, price);
882
890
  }
891
+ /**
892
+ * Fetches order book for a trading pair.
893
+ *
894
+ * @param symbol - Trading pair symbol
895
+ * @returns Promise resolving to order book data
896
+ * @throws Error if getOrderBook is not implemented
897
+ */
898
+ async getOrderBook(symbol) {
899
+ this.params.logger.debug("ClientExchange getOrderBook", {
900
+ symbol,
901
+ });
902
+ const to = new Date(this.params.execution.context.when.getTime());
903
+ const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * 60 * 1000);
904
+ return await this.params.getOrderBook(symbol, from, to);
905
+ }
883
906
  }
884
907
 
908
+ /**
909
+ * Default implementation for getCandles.
910
+ * Throws an error indicating the method is not implemented.
911
+ */
912
+ const DEFAULT_GET_CANDLES_FN$1 = async (_symbol, _interval, _since, _limit) => {
913
+ throw new Error(`getCandles is not implemented for this exchange`);
914
+ };
915
+ /**
916
+ * Default implementation for formatQuantity.
917
+ * Returns Bitcoin precision on Binance (8 decimal places).
918
+ */
919
+ const DEFAULT_FORMAT_QUANTITY_FN$1 = async (_symbol, quantity) => {
920
+ return quantity.toFixed(8);
921
+ };
922
+ /**
923
+ * Default implementation for formatPrice.
924
+ * Returns Bitcoin precision on Binance (2 decimal places).
925
+ */
926
+ const DEFAULT_FORMAT_PRICE_FN$1 = async (_symbol, price) => {
927
+ return price.toFixed(2);
928
+ };
929
+ /**
930
+ * Default implementation for getOrderBook.
931
+ * Throws an error indicating the method is not implemented.
932
+ */
933
+ const DEFAULT_GET_ORDER_BOOK_FN$1 = async (_symbol, _from, _to) => {
934
+ throw new Error(`getOrderBook is not implemented for this exchange`);
935
+ };
885
936
  /**
886
937
  * Connection service routing exchange operations to correct ClientExchange instance.
887
938
  *
@@ -920,7 +971,7 @@ class ExchangeConnectionService {
920
971
  * @returns Configured ClientExchange instance
921
972
  */
922
973
  this.getExchange = functoolsKit.memoize(([exchangeName]) => `${exchangeName}`, (exchangeName) => {
923
- const { getCandles, formatPrice, formatQuantity, callbacks } = this.exchangeSchemaService.get(exchangeName);
974
+ 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
975
  return new ClientExchange({
925
976
  execution: this.executionContextService,
926
977
  logger: this.loggerService,
@@ -928,6 +979,7 @@ class ExchangeConnectionService {
928
979
  getCandles,
929
980
  formatPrice,
930
981
  formatQuantity,
982
+ getOrderBook,
931
983
  callbacks,
932
984
  });
933
985
  });
@@ -1015,6 +1067,20 @@ class ExchangeConnectionService {
1015
1067
  });
1016
1068
  return await this.getExchange(this.methodContextService.context.exchangeName).formatQuantity(symbol, quantity);
1017
1069
  };
1070
+ /**
1071
+ * Fetches order book for a trading pair using configured exchange.
1072
+ *
1073
+ * Routes to exchange determined by methodContextService.context.exchangeName.
1074
+ *
1075
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
1076
+ * @returns Promise resolving to order book data
1077
+ */
1078
+ this.getOrderBook = async (symbol) => {
1079
+ this.loggerService.log("exchangeConnectionService getOrderBook", {
1080
+ symbol,
1081
+ });
1082
+ return await this.getExchange(this.methodContextService.context.exchangeName).getOrderBook(symbol);
1083
+ };
1018
1084
  }
1019
1085
  }
1020
1086
 
@@ -1284,7 +1350,7 @@ async function writeFileAtomic(file, data, options = {}) {
1284
1350
  }
1285
1351
  }
1286
1352
 
1287
- var _a$2;
1353
+ var _a$2, _b$2;
1288
1354
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
1289
1355
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
1290
1356
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
@@ -1316,9 +1382,6 @@ const PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT = "PersistBase.waitForInit";
1316
1382
  const PERSIST_BASE_METHOD_NAME_READ_VALUE = "PersistBase.readValue";
1317
1383
  const PERSIST_BASE_METHOD_NAME_WRITE_VALUE = "PersistBase.writeValue";
1318
1384
  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
1385
  const PERSIST_BASE_METHOD_NAME_KEYS = "PersistBase.keys";
1323
1386
  const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
1324
1387
  const BASE_UNLINK_RETRY_COUNT = 5;
@@ -1371,254 +1434,119 @@ const BASE_WAIT_FOR_INIT_UNLINK_FN = async (filePath) => functoolsKit.trycatch(f
1371
1434
  * const value = await persist.readValue("key1");
1372
1435
  * ```
1373
1436
  */
1374
- const PersistBase = functoolsKit.makeExtendable(class {
1375
- /**
1376
- * Creates new persistence instance.
1377
- *
1378
- * @param entityName - Unique entity type identifier
1379
- * @param baseDir - Base directory for all entities (default: ./dump/data)
1380
- */
1381
- constructor(entityName, baseDir = path.join(process.cwd(), "logs/data")) {
1382
- this.entityName = entityName;
1383
- this.baseDir = baseDir;
1384
- this[_a$2] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
1385
- bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
1386
- entityName: this.entityName,
1387
- baseDir,
1388
- });
1389
- this._directory = path.join(this.baseDir, this.entityName);
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);
1437
+ const PersistBase = functoolsKit.makeExtendable((_b$2 = class {
1438
+ /**
1439
+ * Creates new persistence instance.
1440
+ *
1441
+ * @param entityName - Unique entity type identifier
1442
+ * @param baseDir - Base directory for all entities (default: ./dump/data)
1443
+ */
1444
+ constructor(entityName, baseDir = path.join(process.cwd(), "logs/data")) {
1445
+ this.entityName = entityName;
1446
+ this.baseDir = baseDir;
1447
+ this[_a$2] = functoolsKit.singleshot(async () => await BASE_WAIT_FOR_INIT_FN(this));
1448
+ bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_CTOR, {
1449
+ entityName: this.entityName,
1450
+ baseDir,
1451
+ });
1452
+ this._directory = path.join(this.baseDir, this.entityName);
1426
1453
  }
1427
- catch (error) {
1428
- if (error?.code === "ENOENT") {
1429
- throw new Error(`Entity ${this.entityName}:${entityId} not found`);
1430
- }
1431
- throw new Error(`Failed to read entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
1454
+ /**
1455
+ * Computes file path for entity ID.
1456
+ *
1457
+ * @param entityId - Entity identifier
1458
+ * @returns Full file path to entity JSON file
1459
+ */
1460
+ _getFilePath(entityId) {
1461
+ return path.join(this.baseDir, this.entityName, `${entityId}.json`);
1432
1462
  }
1433
- }
1434
- async hasValue(entityId) {
1435
- bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
1436
- entityName: this.entityName,
1437
- entityId,
1438
- });
1439
- try {
1440
- const filePath = this._getFilePath(entityId);
1441
- await fs.access(filePath);
1442
- return true;
1463
+ async waitForInit(initial) {
1464
+ bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_WAIT_FOR_INIT, {
1465
+ entityName: this.entityName,
1466
+ initial,
1467
+ });
1468
+ await this[BASE_WAIT_FOR_INIT_SYMBOL]();
1443
1469
  }
1444
- catch (error) {
1445
- if (error?.code === "ENOENT") {
1446
- return false;
1470
+ async readValue(entityId) {
1471
+ bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_READ_VALUE, {
1472
+ entityName: this.entityName,
1473
+ entityId,
1474
+ });
1475
+ try {
1476
+ const filePath = this._getFilePath(entityId);
1477
+ const fileContent = await fs.readFile(filePath, "utf-8");
1478
+ return JSON.parse(fileContent);
1447
1479
  }
1448
- throw new Error(`Failed to check existence of entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
1449
- }
1450
- }
1451
- async writeValue(entityId, entity) {
1452
- bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
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`);
1480
+ catch (error) {
1481
+ if (error?.code === "ENOENT") {
1482
+ throw new Error(`Entity ${this.entityName}:${entityId} not found`);
1483
+ }
1484
+ throw new Error(`Failed to read entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
1484
1485
  }
1485
- throw new Error(`Failed to remove entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
1486
1486
  }
1487
- }
1488
- /**
1489
- * Removes all entities from storage.
1490
- *
1491
- * @returns Promise that resolves when all entities are deleted
1492
- * @throws Error if deletion fails
1493
- */
1494
- async removeAll() {
1495
- bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_REMOVE_ALL, {
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));
1487
+ async hasValue(entityId) {
1488
+ bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_HAS_VALUE, {
1489
+ entityName: this.entityName,
1490
+ entityId,
1491
+ });
1492
+ try {
1493
+ const filePath = this._getFilePath(entityId);
1494
+ await fs.access(filePath);
1495
+ return true;
1503
1496
  }
1504
- }
1505
- catch (error) {
1506
- throw new Error(`Failed to remove values for ${this.entityName}: ${functoolsKit.getErrorMessage(error)}`);
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;
1497
+ catch (error) {
1498
+ if (error?.code === "ENOENT") {
1499
+ return false;
1500
+ }
1501
+ throw new Error(`Failed to check existence of entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
1532
1502
  }
1533
1503
  }
1534
- catch (error) {
1535
- throw new Error(`Failed to read values for ${this.entityName}: ${functoolsKit.getErrorMessage(error)}`);
1536
- }
1537
- }
1538
- /**
1539
- * Async generator yielding all entity IDs.
1540
- * Sorted alphanumerically.
1541
- *
1542
- * @returns AsyncGenerator yielding entity IDs
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;
1504
+ async writeValue(entityId, entity) {
1505
+ bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_WRITE_VALUE, {
1506
+ entityName: this.entityName,
1507
+ entityId,
1508
+ });
1509
+ try {
1510
+ const filePath = this._getFilePath(entityId);
1511
+ const serializedData = JSON.stringify(entity);
1512
+ await writeFileAtomic(filePath, serializedData, "utf-8");
1560
1513
  }
1561
- }
1562
- catch (error) {
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;
1514
+ catch (error) {
1515
+ throw new Error(`Failed to write entity ${this.entityName}:${entityId}: ${functoolsKit.getErrorMessage(error)}`);
1587
1516
  }
1588
1517
  }
1589
- }
1590
- /**
1591
- * Takes first N entities, optionally filtered.
1592
- *
1593
- * @param total - Maximum number of entities to yield
1594
- * @param predicate - Optional filter function
1595
- * @returns AsyncGenerator yielding up to total entities
1596
- */
1597
- async *take(total, predicate) {
1598
- let count = 0;
1599
- if (predicate) {
1600
- for await (const entity of this.values()) {
1601
- if (!predicate(entity)) {
1602
- continue;
1603
- }
1604
- count += 1;
1605
- yield entity;
1606
- if (count >= total) {
1607
- break;
1518
+ /**
1519
+ * Async generator yielding all entity IDs.
1520
+ * Sorted alphanumerically.
1521
+ * Used internally by waitForInit for validation.
1522
+ *
1523
+ * @returns AsyncGenerator yielding entity IDs
1524
+ * @throws Error if reading fails
1525
+ */
1526
+ async *keys() {
1527
+ bt.loggerService.debug(PERSIST_BASE_METHOD_NAME_KEYS, {
1528
+ entityName: this.entityName,
1529
+ });
1530
+ try {
1531
+ const files = await fs.readdir(this._directory);
1532
+ const entityIds = files
1533
+ .filter((file) => file.endsWith(".json"))
1534
+ .map((file) => file.slice(0, -5))
1535
+ .sort((a, b) => a.localeCompare(b, undefined, {
1536
+ numeric: true,
1537
+ sensitivity: "base",
1538
+ }));
1539
+ for (const entityId of entityIds) {
1540
+ yield entityId;
1608
1541
  }
1609
1542
  }
1610
- }
1611
- else {
1612
- for await (const entity of this.values()) {
1613
- count += 1;
1614
- yield entity;
1615
- if (count >= total) {
1616
- break;
1617
- }
1543
+ catch (error) {
1544
+ throw new Error(`Failed to read keys for ${this.entityName}: ${functoolsKit.getErrorMessage(error)}`);
1618
1545
  }
1619
1546
  }
1620
- }
1621
- });
1547
+ },
1548
+ _a$2 = BASE_WAIT_FOR_INIT_SYMBOL,
1549
+ _b$2));
1622
1550
  /**
1623
1551
  * Dummy persist adapter that discards all writes.
1624
1552
  * Used for disabling persistence.
@@ -1650,12 +1578,6 @@ class PersistDummy {
1650
1578
  */
1651
1579
  async writeValue() {
1652
1580
  }
1653
- /**
1654
- * No-op keys generator.
1655
- * @returns Empty async generator
1656
- */
1657
- async *keys() {
1658
- }
1659
1581
  }
1660
1582
  /**
1661
1583
  * Utility class for managing signal persistence.
@@ -7541,6 +7463,32 @@ class ExchangeCoreService {
7541
7463
  backtest,
7542
7464
  });
7543
7465
  };
7466
+ /**
7467
+ * Fetches order book with execution context.
7468
+ *
7469
+ * @param symbol - Trading pair symbol
7470
+ * @param when - Timestamp for context
7471
+ * @param backtest - Whether running in backtest mode
7472
+ * @returns Promise resolving to order book data
7473
+ */
7474
+ this.getOrderBook = async (symbol, when, backtest) => {
7475
+ this.loggerService.log("exchangeCoreService getOrderBook", {
7476
+ symbol,
7477
+ when,
7478
+ backtest,
7479
+ });
7480
+ if (!MethodContextService.hasContext()) {
7481
+ throw new Error("exchangeCoreService getOrderBook requires a method context");
7482
+ }
7483
+ await this.validate(this.methodContextService.context.exchangeName);
7484
+ return await ExecutionContextService.runInContext(async () => {
7485
+ return await this.exchangeConnectionService.getOrderBook(symbol);
7486
+ }, {
7487
+ symbol,
7488
+ when,
7489
+ backtest,
7490
+ });
7491
+ };
7544
7492
  }
7545
7493
  }
7546
7494
 
@@ -8187,12 +8135,6 @@ class ExchangeSchemaService {
8187
8135
  if (typeof exchangeSchema.getCandles !== "function") {
8188
8136
  throw new Error(`exchange schema validation failed: missing getCandles for exchangeName=${exchangeSchema.exchangeName}`);
8189
8137
  }
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
8138
  };
8197
8139
  /**
8198
8140
  * Overrides an existing exchange schema with partial updates.
@@ -10062,21 +10004,21 @@ const live_columns = [
10062
10004
  {
10063
10005
  key: "openPrice",
10064
10006
  label: "Open Price",
10065
- format: (data) => data.openPrice !== undefined ? `${data.openPrice.toFixed(8)} USD` : "N/A",
10007
+ format: (data) => data.priceOpen !== undefined ? `${data.priceOpen.toFixed(8)} USD` : "N/A",
10066
10008
  isVisible: () => true,
10067
10009
  },
10068
10010
  {
10069
10011
  key: "takeProfit",
10070
10012
  label: "Take Profit",
10071
- format: (data) => data.takeProfit !== undefined
10072
- ? `${data.takeProfit.toFixed(8)} USD`
10013
+ format: (data) => data.priceTakeProfit !== undefined
10014
+ ? `${data.priceTakeProfit.toFixed(8)} USD`
10073
10015
  : "N/A",
10074
10016
  isVisible: () => true,
10075
10017
  },
10076
10018
  {
10077
10019
  key: "stopLoss",
10078
10020
  label: "Stop Loss",
10079
- format: (data) => data.stopLoss !== undefined ? `${data.stopLoss.toFixed(8)} USD` : "N/A",
10021
+ format: (data) => data.priceStopLoss !== undefined ? `${data.priceStopLoss.toFixed(8)} USD` : "N/A",
10080
10022
  isVisible: () => true,
10081
10023
  },
10082
10024
  {
@@ -10760,13 +10702,13 @@ const schedule_columns = [
10760
10702
  {
10761
10703
  key: "takeProfit",
10762
10704
  label: "Take Profit",
10763
- format: (data) => `${data.takeProfit.toFixed(8)} USD`,
10705
+ format: (data) => `${data.priceTakeProfit.toFixed(8)} USD`,
10764
10706
  isVisible: () => true,
10765
10707
  },
10766
10708
  {
10767
10709
  key: "stopLoss",
10768
10710
  label: "Stop Loss",
10769
- format: (data) => `${data.stopLoss.toFixed(8)} USD`,
10711
+ format: (data) => `${data.priceStopLoss.toFixed(8)} USD`,
10770
10712
  isVisible: () => true,
10771
10713
  },
10772
10714
  {
@@ -12065,9 +12007,9 @@ let ReportStorage$5 = class ReportStorage {
12065
12007
  position: data.signal.position,
12066
12008
  note: data.signal.note,
12067
12009
  currentPrice: data.signal.priceOpen,
12068
- openPrice: data.signal.priceOpen,
12069
- takeProfit: data.signal.priceTakeProfit,
12070
- stopLoss: data.signal.priceStopLoss,
12010
+ priceOpen: data.signal.priceOpen,
12011
+ priceTakeProfit: data.signal.priceTakeProfit,
12012
+ priceStopLoss: data.signal.priceStopLoss,
12071
12013
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
12072
12014
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
12073
12015
  totalExecuted: data.signal.totalExecuted,
@@ -12092,9 +12034,9 @@ let ReportStorage$5 = class ReportStorage {
12092
12034
  position: data.signal.position,
12093
12035
  note: data.signal.note,
12094
12036
  currentPrice: data.currentPrice,
12095
- openPrice: data.signal.priceOpen,
12096
- takeProfit: data.signal.priceTakeProfit,
12097
- stopLoss: data.signal.priceStopLoss,
12037
+ priceOpen: data.signal.priceOpen,
12038
+ priceTakeProfit: data.signal.priceTakeProfit,
12039
+ priceStopLoss: data.signal.priceStopLoss,
12098
12040
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
12099
12041
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
12100
12042
  totalExecuted: data.signal.totalExecuted,
@@ -12132,9 +12074,9 @@ let ReportStorage$5 = class ReportStorage {
12132
12074
  position: data.signal.position,
12133
12075
  note: data.signal.note,
12134
12076
  currentPrice: data.currentPrice,
12135
- openPrice: data.signal.priceOpen,
12136
- takeProfit: data.signal.priceTakeProfit,
12137
- stopLoss: data.signal.priceStopLoss,
12077
+ priceOpen: data.signal.priceOpen,
12078
+ priceTakeProfit: data.signal.priceTakeProfit,
12079
+ priceStopLoss: data.signal.priceStopLoss,
12138
12080
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
12139
12081
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
12140
12082
  totalExecuted: data.signal.totalExecuted,
@@ -12619,8 +12561,8 @@ let ReportStorage$4 = class ReportStorage {
12619
12561
  note: data.signal.note,
12620
12562
  currentPrice: data.currentPrice,
12621
12563
  priceOpen: data.signal.priceOpen,
12622
- takeProfit: data.signal.priceTakeProfit,
12623
- stopLoss: data.signal.priceStopLoss,
12564
+ priceTakeProfit: data.signal.priceTakeProfit,
12565
+ priceStopLoss: data.signal.priceStopLoss,
12624
12566
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
12625
12567
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
12626
12568
  totalExecuted: data.signal.totalExecuted,
@@ -12647,8 +12589,8 @@ let ReportStorage$4 = class ReportStorage {
12647
12589
  note: data.signal.note,
12648
12590
  currentPrice: data.currentPrice,
12649
12591
  priceOpen: data.signal.priceOpen,
12650
- takeProfit: data.signal.priceTakeProfit,
12651
- stopLoss: data.signal.priceStopLoss,
12592
+ priceTakeProfit: data.signal.priceTakeProfit,
12593
+ priceStopLoss: data.signal.priceStopLoss,
12652
12594
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
12653
12595
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
12654
12596
  totalExecuted: data.signal.totalExecuted,
@@ -12677,8 +12619,8 @@ let ReportStorage$4 = class ReportStorage {
12677
12619
  note: data.signal.note,
12678
12620
  currentPrice: data.currentPrice,
12679
12621
  priceOpen: data.signal.priceOpen,
12680
- takeProfit: data.signal.priceTakeProfit,
12681
- stopLoss: data.signal.priceStopLoss,
12622
+ priceTakeProfit: data.signal.priceTakeProfit,
12623
+ priceStopLoss: data.signal.priceStopLoss,
12682
12624
  originalPriceTakeProfit: data.signal.originalPriceTakeProfit,
12683
12625
  originalPriceStopLoss: data.signal.originalPriceStopLoss,
12684
12626
  totalExecuted: data.signal.totalExecuted,
@@ -13716,11 +13658,11 @@ let ReportStorage$3 = class ReportStorage {
13716
13658
  await Markdown.writeData("walker", markdown, {
13717
13659
  path,
13718
13660
  file: filename,
13719
- symbol: "",
13661
+ symbol: symbol,
13720
13662
  signalId: "",
13721
13663
  strategyName: "",
13722
- exchangeName: "",
13723
- frameName: ""
13664
+ exchangeName: context.exchangeName,
13665
+ frameName: context.frameName
13724
13666
  });
13725
13667
  }
13726
13668
  };
@@ -18815,6 +18757,9 @@ class ConfigValidationService {
18815
18757
  if (!Number.isInteger(GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) || GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST <= 0) {
18816
18758
  errors.push(`CC_MAX_CANDLES_PER_REQUEST must be a positive integer, got ${GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST}`);
18817
18759
  }
18760
+ if (!Number.isInteger(GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES) || GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES < 0) {
18761
+ errors.push(`CC_ORDER_BOOK_TIME_OFFSET_MINUTES must be a non-negative integer, got ${GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES}`);
18762
+ }
18818
18763
  // Throw aggregated errors if any
18819
18764
  if (errors.length > 0) {
18820
18765
  const errorMessage = `GLOBAL_CONFIG validation failed:\n${errors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`;
@@ -27161,6 +27106,35 @@ const EXCHANGE_METHOD_NAME_GET_CANDLES = "ExchangeUtils.getCandles";
27161
27106
  const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
27162
27107
  const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
27163
27108
  const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
27109
+ const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
27110
+ /**
27111
+ * Default implementation for getCandles.
27112
+ * Throws an error indicating the method is not implemented.
27113
+ */
27114
+ const DEFAULT_GET_CANDLES_FN = async (_symbol, _interval, _since, _limit) => {
27115
+ throw new Error(`getCandles is not implemented for this exchange`);
27116
+ };
27117
+ /**
27118
+ * Default implementation for formatQuantity.
27119
+ * Returns Bitcoin precision on Binance (8 decimal places).
27120
+ */
27121
+ const DEFAULT_FORMAT_QUANTITY_FN = async (_symbol, quantity) => {
27122
+ return quantity.toFixed(8);
27123
+ };
27124
+ /**
27125
+ * Default implementation for formatPrice.
27126
+ * Returns Bitcoin precision on Binance (2 decimal places).
27127
+ */
27128
+ const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price) => {
27129
+ return price.toFixed(2);
27130
+ };
27131
+ /**
27132
+ * Default implementation for getOrderBook.
27133
+ * Throws an error indicating the method is not implemented.
27134
+ */
27135
+ const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _from, _to) => {
27136
+ throw new Error(`getOrderBook is not implemented for this exchange`);
27137
+ };
27164
27138
  const INTERVAL_MINUTES$1 = {
27165
27139
  "1m": 1,
27166
27140
  "3m": 3,
@@ -27173,6 +27147,25 @@ const INTERVAL_MINUTES$1 = {
27173
27147
  "6h": 360,
27174
27148
  "8h": 480,
27175
27149
  };
27150
+ /**
27151
+ * Creates exchange instance with methods resolved once during construction.
27152
+ * Applies default implementations where schema methods are not provided.
27153
+ *
27154
+ * @param schema - Exchange schema from registry
27155
+ * @returns Object with resolved exchange methods
27156
+ */
27157
+ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
27158
+ const getCandles = schema.getCandles ?? DEFAULT_GET_CANDLES_FN;
27159
+ const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
27160
+ const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
27161
+ const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
27162
+ return {
27163
+ getCandles,
27164
+ formatQuantity,
27165
+ formatPrice,
27166
+ getOrderBook,
27167
+ };
27168
+ };
27176
27169
  /**
27177
27170
  * Instance class for exchange operations on a specific exchange.
27178
27171
  *
@@ -27222,6 +27215,7 @@ class ExchangeInstance {
27222
27215
  interval,
27223
27216
  limit,
27224
27217
  });
27218
+ const getCandles = this._methods.getCandles;
27225
27219
  const step = INTERVAL_MINUTES$1[interval];
27226
27220
  const adjust = step * limit - step;
27227
27221
  if (!adjust) {
@@ -27236,7 +27230,7 @@ class ExchangeInstance {
27236
27230
  let currentSince = new Date(since.getTime());
27237
27231
  while (remaining > 0) {
27238
27232
  const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
27239
- const chunkData = await this._schema.getCandles(symbol, interval, currentSince, chunkLimit);
27233
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit);
27240
27234
  allData.push(...chunkData);
27241
27235
  remaining -= chunkLimit;
27242
27236
  if (remaining > 0) {
@@ -27246,7 +27240,7 @@ class ExchangeInstance {
27246
27240
  }
27247
27241
  }
27248
27242
  else {
27249
- allData = await this._schema.getCandles(symbol, interval, since, limit);
27243
+ allData = await getCandles(symbol, interval, since, limit);
27250
27244
  }
27251
27245
  // Filter candles to strictly match the requested range
27252
27246
  const whenTimestamp = when.getTime();
@@ -27313,7 +27307,7 @@ class ExchangeInstance {
27313
27307
  * ```typescript
27314
27308
  * const instance = new ExchangeInstance("binance");
27315
27309
  * const formatted = await instance.formatQuantity("BTCUSDT", 0.001);
27316
- * console.log(formatted); // "0.001"
27310
+ * console.log(formatted); // "0.00100000"
27317
27311
  * ```
27318
27312
  */
27319
27313
  this.formatQuantity = async (symbol, quantity) => {
@@ -27322,7 +27316,7 @@ class ExchangeInstance {
27322
27316
  symbol,
27323
27317
  quantity,
27324
27318
  });
27325
- return await this._schema.formatQuantity(symbol, quantity);
27319
+ return await this._methods.formatQuantity(symbol, quantity);
27326
27320
  };
27327
27321
  /**
27328
27322
  * Format price according to exchange precision rules.
@@ -27344,9 +27338,33 @@ class ExchangeInstance {
27344
27338
  symbol,
27345
27339
  price,
27346
27340
  });
27347
- return await this._schema.formatPrice(symbol, price);
27341
+ return await this._methods.formatPrice(symbol, price);
27348
27342
  };
27349
- this._schema = bt.exchangeSchemaService.get(this.exchangeName);
27343
+ /**
27344
+ * Fetch order book for a trading pair.
27345
+ *
27346
+ * @param symbol - Trading pair symbol
27347
+ * @returns Promise resolving to order book data
27348
+ * @throws Error if getOrderBook is not implemented
27349
+ *
27350
+ * @example
27351
+ * ```typescript
27352
+ * const instance = new ExchangeInstance("binance");
27353
+ * const orderBook = await instance.getOrderBook("BTCUSDT");
27354
+ * console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
27355
+ * ```
27356
+ */
27357
+ this.getOrderBook = async (symbol) => {
27358
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_ORDER_BOOK, {
27359
+ exchangeName: this.exchangeName,
27360
+ symbol,
27361
+ });
27362
+ const to = new Date(Date.now());
27363
+ const from = new Date(to.getTime() - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * 60 * 1000);
27364
+ return await this._methods.getOrderBook(symbol, from, to);
27365
+ };
27366
+ const schema = bt.exchangeSchemaService.get(this.exchangeName);
27367
+ this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
27350
27368
  }
27351
27369
  }
27352
27370
  /**
@@ -27432,6 +27450,18 @@ class ExchangeUtils {
27432
27450
  const instance = this._getInstance(context.exchangeName);
27433
27451
  return await instance.formatPrice(symbol, price);
27434
27452
  };
27453
+ /**
27454
+ * Fetch order book for a trading pair.
27455
+ *
27456
+ * @param symbol - Trading pair symbol
27457
+ * @param context - Execution context with exchange name
27458
+ * @returns Promise resolving to order book data
27459
+ */
27460
+ this.getOrderBook = async (symbol, context) => {
27461
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_ORDER_BOOK);
27462
+ const instance = this._getInstance(context.exchangeName);
27463
+ return await instance.getOrderBook(symbol);
27464
+ };
27435
27465
  }
27436
27466
  }
27437
27467
  /**