backtest-kit 3.4.0 → 3.4.2

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.mjs CHANGED
@@ -408,6 +408,12 @@ const GLOBAL_CONFIG = {
408
408
  * Default: 20 levels
409
409
  */
410
410
  CC_ORDER_BOOK_MAX_DEPTH_LEVELS: 1000,
411
+ /**
412
+ * Maximum minutes of aggregated trades to fetch when no limit is provided.
413
+ * If limit is not specified, the system will fetch aggregated trades for this many minutes starting from the current time minus the offset.
414
+ * Binance requirement
415
+ */
416
+ CC_AGGREGATED_TRADES_MAX_MINUTES: 60,
411
417
  /**
412
418
  * Maximum number of notifications to keep in storage.
413
419
  * Older notifications are removed when this limit is exceeded.
@@ -422,6 +428,14 @@ const GLOBAL_CONFIG = {
422
428
  * Default: 50 signals
423
429
  */
424
430
  CC_MAX_SIGNALS: 50,
431
+ /**
432
+ * Maximum number of log lines to keep in storage.
433
+ * Older log lines are removed when this limit is exceeded.
434
+ * This helps prevent unbounded log growth which can consume memory and degrade performance over time.
435
+ *
436
+ * Default: 1000 log lines
437
+ */
438
+ CC_MAX_LOG_LINES: 1000,
425
439
  /**
426
440
  * Enables mutex locking for candle fetching to prevent concurrent fetches of the same candles.
427
441
  * This can help avoid redundant API calls and ensure data consistency when multiple processes/threads attempt to fetch candles simultaneously.
@@ -756,6 +770,11 @@ const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA = "PersistNotificationUt
756
770
  const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON = "PersistNotificationUtils.useJson";
757
771
  const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY = "PersistNotificationUtils.useDummy";
758
772
  const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER = "PersistNotificationUtils.usePersistNotificationAdapter";
773
+ const PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA = "PersistLogUtils.readLogData";
774
+ const PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA = "PersistLogUtils.writeLogData";
775
+ const PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON = "PersistLogUtils.useJson";
776
+ const PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY = "PersistLogUtils.useDummy";
777
+ const PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER = "PersistLogUtils.usePersistLogAdapter";
759
778
  const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
760
779
  const BASE_UNLINK_RETRY_COUNT = 5;
761
780
  const BASE_UNLINK_RETRY_DELAY = 1000;
@@ -1926,6 +1945,106 @@ class PersistNotificationUtils {
1926
1945
  * Used by NotificationPersistLiveUtils/NotificationPersistBacktestUtils for notification persistence.
1927
1946
  */
1928
1947
  const PersistNotificationAdapter = new PersistNotificationUtils();
1948
+ /**
1949
+ * Utility class for managing log entry persistence.
1950
+ *
1951
+ * Features:
1952
+ * - Memoized storage instance
1953
+ * - Custom adapter support
1954
+ * - Atomic read/write operations for LogData
1955
+ * - Each log entry stored as separate file keyed by id
1956
+ * - Crash-safe log state management
1957
+ *
1958
+ * Used by LogPersistUtils for log entry persistence.
1959
+ */
1960
+ class PersistLogUtils {
1961
+ constructor() {
1962
+ this.PersistLogFactory = PersistBase;
1963
+ this._logStorage = null;
1964
+ /**
1965
+ * Reads persisted log entries.
1966
+ *
1967
+ * Called by LogPersistUtils.waitForInit() to restore state.
1968
+ * Uses keys() from PersistBase to iterate over all stored entries.
1969
+ * Returns empty array if no entries exist.
1970
+ *
1971
+ * @returns Promise resolving to array of log entries
1972
+ */
1973
+ this.readLogData = async () => {
1974
+ bt.loggerService.info(PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA);
1975
+ const isInitial = !this._logStorage;
1976
+ const stateStorage = this.getLogStorage();
1977
+ await stateStorage.waitForInit(isInitial);
1978
+ const entries = [];
1979
+ for await (const entryId of stateStorage.keys()) {
1980
+ const entry = await stateStorage.readValue(entryId);
1981
+ entries.push(entry);
1982
+ }
1983
+ return entries;
1984
+ };
1985
+ /**
1986
+ * Writes log entries to disk with atomic file writes.
1987
+ *
1988
+ * Called by LogPersistUtils after each log call to persist state.
1989
+ * Uses entry.id as the storage key for individual file storage.
1990
+ * Uses atomic writes to prevent corruption on crashes.
1991
+ *
1992
+ * @param logData - Log entries to persist
1993
+ * @returns Promise that resolves when write is complete
1994
+ */
1995
+ this.writeLogData = async (logData) => {
1996
+ bt.loggerService.info(PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA);
1997
+ const isInitial = !this._logStorage;
1998
+ const stateStorage = this.getLogStorage();
1999
+ await stateStorage.waitForInit(isInitial);
2000
+ for (const entry of logData) {
2001
+ if (await stateStorage.hasValue(entry.id)) {
2002
+ continue;
2003
+ }
2004
+ await stateStorage.writeValue(entry.id, entry);
2005
+ }
2006
+ };
2007
+ }
2008
+ getLogStorage() {
2009
+ if (!this._logStorage) {
2010
+ this._logStorage = Reflect.construct(this.PersistLogFactory, [
2011
+ `log`,
2012
+ `./dump/data/log/`,
2013
+ ]);
2014
+ }
2015
+ return this._logStorage;
2016
+ }
2017
+ /**
2018
+ * Registers a custom persistence adapter.
2019
+ *
2020
+ * @param Ctor - Custom PersistBase constructor
2021
+ */
2022
+ usePersistLogAdapter(Ctor) {
2023
+ bt.loggerService.info(PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER);
2024
+ this.PersistLogFactory = Ctor;
2025
+ }
2026
+ /**
2027
+ * Switches to the default JSON persist adapter.
2028
+ * All future persistence writes will use JSON storage.
2029
+ */
2030
+ useJson() {
2031
+ bt.loggerService.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON);
2032
+ this.usePersistLogAdapter(PersistBase);
2033
+ }
2034
+ /**
2035
+ * Switches to a dummy persist adapter that discards all writes.
2036
+ * All future persistence writes will be no-ops.
2037
+ */
2038
+ useDummy() {
2039
+ bt.loggerService.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY);
2040
+ this.usePersistLogAdapter(PersistDummy);
2041
+ }
2042
+ }
2043
+ /**
2044
+ * Global singleton instance of PersistLogUtils.
2045
+ * Used by LogPersistUtils for log entry persistence.
2046
+ */
2047
+ const PersistLogAdapter = new PersistLogUtils();
1929
2048
 
1930
2049
  var _a$2, _b$2;
1931
2050
  const BUSY_DELAY = 100;
@@ -2667,6 +2786,55 @@ class ClientExchange {
2667
2786
  GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$5);
2668
2787
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
2669
2788
  }
2789
+ /**
2790
+ * Fetches aggregated trades backwards from execution context time.
2791
+ *
2792
+ * Algorithm:
2793
+ * 1. Align when down to the nearest minute boundary (1-minute granularity)
2794
+ * 2. If limit is not specified: fetch one window of CC_AGGREGATED_TRADES_MAX_MINUTES
2795
+ * 3. If limit is specified: paginate backwards in CC_AGGREGATED_TRADES_MAX_MINUTES
2796
+ * chunks until at least limit trades are collected, then slice to limit
2797
+ *
2798
+ * Look-ahead bias prevention:
2799
+ * - `to` is always aligned down to the minute (never exceeds current when)
2800
+ * - Each pagination window goes strictly backwards from alignedWhen
2801
+ *
2802
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2803
+ * @param limit - Optional maximum number of trades to return. If not specified,
2804
+ * returns all trades within the last CC_AGGREGATED_TRADES_MAX_MINUTES window.
2805
+ * @returns Promise resolving to array of aggregated trade data
2806
+ */
2807
+ async getAggregatedTrades(symbol, limit) {
2808
+ this.params.logger.debug("ClientExchange getAggregatedTrades", {
2809
+ symbol,
2810
+ limit,
2811
+ });
2812
+ const whenTimestamp = this.params.execution.context.when.getTime();
2813
+ // Align to 1-minute boundary to prevent look-ahead bias
2814
+ const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, 1);
2815
+ const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$5 - MS_PER_MINUTE$5;
2816
+ // No limit: fetch a single window and return as-is
2817
+ if (limit === undefined) {
2818
+ const to = new Date(alignedTo);
2819
+ const from = new Date(alignedTo - windowMs);
2820
+ return await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest);
2821
+ }
2822
+ // With limit: paginate backwards until we have enough trades
2823
+ const result = [];
2824
+ let windowEnd = alignedTo;
2825
+ while (result.length < limit) {
2826
+ const windowStart = windowEnd - windowMs;
2827
+ const to = new Date(windowEnd);
2828
+ const from = new Date(windowStart);
2829
+ const chunk = await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest);
2830
+ // Prepend chunk (older data goes first)
2831
+ result.unshift(...chunk);
2832
+ // Move window backwards
2833
+ windowEnd = windowStart;
2834
+ }
2835
+ // Slice to requested limit (most recent trades)
2836
+ return result.slice(-limit);
2837
+ }
2670
2838
  }
2671
2839
 
2672
2840
  /**
@@ -2703,6 +2871,18 @@ const DEFAULT_FORMAT_PRICE_FN$1 = async (_symbol, price, _backtest) => {
2703
2871
  const DEFAULT_GET_ORDER_BOOK_FN$1 = async (_symbol, _depth, _from, _to, _backtest) => {
2704
2872
  throw new Error(`getOrderBook is not implemented for this exchange`);
2705
2873
  };
2874
+ /**
2875
+ * Default implementation for getAggregatedTrades.
2876
+ * Throws an error indicating the method is not implemented.
2877
+ *
2878
+ * @param _symbol - Trading pair symbol (unused)
2879
+ * @param _from - Start of time range (unused - can be ignored in live implementations)
2880
+ * @param _to - End of time range (unused - can be ignored in live implementations)
2881
+ * @param _backtest - Whether running in backtest mode (unused)
2882
+ */
2883
+ const DEFAULT_GET_AGGREGATED_TRADES_FN$1 = async (_symbol, _from, _to, _backtest) => {
2884
+ throw new Error(`getAggregatedTrades is not implemented for this exchange`);
2885
+ };
2706
2886
  /**
2707
2887
  * Connection service routing exchange operations to correct ClientExchange instance.
2708
2888
  *
@@ -2741,7 +2921,7 @@ class ExchangeConnectionService {
2741
2921
  * @returns Configured ClientExchange instance
2742
2922
  */
2743
2923
  this.getExchange = memoize(([exchangeName]) => `${exchangeName}`, (exchangeName) => {
2744
- 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);
2924
+ 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, getAggregatedTrades = DEFAULT_GET_AGGREGATED_TRADES_FN$1, callbacks } = this.exchangeSchemaService.get(exchangeName);
2745
2925
  return new ClientExchange({
2746
2926
  execution: this.executionContextService,
2747
2927
  logger: this.loggerService,
@@ -2750,6 +2930,7 @@ class ExchangeConnectionService {
2750
2930
  formatPrice,
2751
2931
  formatQuantity,
2752
2932
  getOrderBook,
2933
+ getAggregatedTrades,
2753
2934
  callbacks,
2754
2935
  });
2755
2936
  });
@@ -2855,6 +3036,22 @@ class ExchangeConnectionService {
2855
3036
  });
2856
3037
  return await this.getExchange(this.methodContextService.context.exchangeName).getOrderBook(symbol, depth);
2857
3038
  };
3039
+ /**
3040
+ * Fetches aggregated trades for a trading pair using configured exchange.
3041
+ *
3042
+ * Routes to exchange determined by methodContextService.context.exchangeName.
3043
+ *
3044
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
3045
+ * @param limit - Optional maximum number of trades to fetch. If empty returns one window of data.
3046
+ * @returns Promise resolving to array of aggregated trade data
3047
+ */
3048
+ this.getAggregatedTrades = async (symbol, limit) => {
3049
+ this.loggerService.log("exchangeConnectionService getAggregatedTrades", {
3050
+ symbol,
3051
+ limit,
3052
+ });
3053
+ return await this.getExchange(this.methodContextService.context.exchangeName).getAggregatedTrades(symbol, limit);
3054
+ };
2858
3055
  /**
2859
3056
  * Fetches raw candles with flexible date/limit parameters.
2860
3057
  *
@@ -11228,6 +11425,34 @@ class ExchangeCoreService {
11228
11425
  backtest,
11229
11426
  });
11230
11427
  };
11428
+ /**
11429
+ * Fetches aggregated trades with execution context.
11430
+ *
11431
+ * @param symbol - Trading pair symbol
11432
+ * @param when - Timestamp for context (used in backtest mode)
11433
+ * @param backtest - Whether running in backtest mode
11434
+ * @param limit - Optional maximum number of trades to fetch
11435
+ * @returns Promise resolving to array of aggregated trade data
11436
+ */
11437
+ this.getAggregatedTrades = async (symbol, when, backtest, limit) => {
11438
+ this.loggerService.log("exchangeCoreService getAggregatedTrades", {
11439
+ symbol,
11440
+ when,
11441
+ backtest,
11442
+ limit,
11443
+ });
11444
+ if (!MethodContextService.hasContext()) {
11445
+ throw new Error("exchangeCoreService getAggregatedTrades requires a method context");
11446
+ }
11447
+ await this.validate(this.methodContextService.context.exchangeName);
11448
+ return await ExecutionContextService.runInContext(async () => {
11449
+ return await this.exchangeConnectionService.getAggregatedTrades(symbol, limit);
11450
+ }, {
11451
+ symbol,
11452
+ when,
11453
+ backtest,
11454
+ });
11455
+ };
11231
11456
  /**
11232
11457
  * Fetches raw candles with flexible date/limit parameters and execution context.
11233
11458
  *
@@ -26647,6 +26872,7 @@ const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
26647
26872
  const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
26648
26873
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
26649
26874
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
26875
+ const EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES = "ExchangeUtils.getAggregatedTrades";
26650
26876
  const MS_PER_MINUTE$3 = 60000;
26651
26877
  /**
26652
26878
  * Gets current timestamp from execution context if available.
@@ -26702,6 +26928,18 @@ const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price, _backtest) => {
26702
26928
  const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest) => {
26703
26929
  throw new Error(`getOrderBook is not implemented for this exchange`);
26704
26930
  };
26931
+ /**
26932
+ * Default implementation for getAggregatedTrades.
26933
+ * Throws an error indicating the method is not implemented.
26934
+ *
26935
+ * @param _symbol - Trading pair symbol (unused)
26936
+ * @param _from - Start of time range (unused - can be ignored in live implementations)
26937
+ * @param _to - End of time range (unused - can be ignored in live implementations)
26938
+ * @param _backtest - Whether running in backtest mode (unused)
26939
+ */
26940
+ const DEFAULT_GET_AGGREGATED_TRADES_FN = async (_symbol, _from, _to, _backtest) => {
26941
+ throw new Error(`getAggregatedTrades is not implemented for this exchange`);
26942
+ };
26705
26943
  const INTERVAL_MINUTES$3 = {
26706
26944
  "1m": 1,
26707
26945
  "3m": 3,
@@ -26747,11 +26985,13 @@ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
26747
26985
  const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
26748
26986
  const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
26749
26987
  const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
26988
+ const getAggregatedTrades = schema.getAggregatedTrades ?? DEFAULT_GET_AGGREGATED_TRADES_FN;
26750
26989
  return {
26751
26990
  getCandles,
26752
26991
  formatQuantity,
26753
26992
  formatPrice,
26754
26993
  getOrderBook,
26994
+ getAggregatedTrades,
26755
26995
  };
26756
26996
  };
26757
26997
  /**
@@ -27063,6 +27303,58 @@ class ExchangeInstance {
27063
27303
  const isBacktest = await GET_BACKTEST_FN();
27064
27304
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
27065
27305
  };
27306
+ /**
27307
+ * Fetch aggregated trades for a trading pair.
27308
+ *
27309
+ * Calculates time range backwards from current timestamp (or execution context when).
27310
+ * Aligns `to` to 1-minute boundary to prevent look-ahead bias.
27311
+ * If limit is not specified, returns all trades within one CC_AGGREGATED_TRADES_MAX_MINUTES window.
27312
+ * If limit is specified, paginates backwards until at least limit trades are collected.
27313
+ *
27314
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
27315
+ * @param limit - Optional maximum number of trades to return
27316
+ * @returns Promise resolving to array of aggregated trade data
27317
+ *
27318
+ * @example
27319
+ * ```typescript
27320
+ * const instance = new ExchangeInstance("binance");
27321
+ * const trades = await instance.getAggregatedTrades("BTCUSDT");
27322
+ * const lastN = await instance.getAggregatedTrades("BTCUSDT", 500);
27323
+ * ```
27324
+ */
27325
+ this.getAggregatedTrades = async (symbol, limit) => {
27326
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES, {
27327
+ exchangeName: this.exchangeName,
27328
+ symbol,
27329
+ limit,
27330
+ });
27331
+ const when = await GET_TIMESTAMP_FN();
27332
+ // Align to 1-minute boundary to prevent look-ahead bias
27333
+ const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), 1);
27334
+ const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$3 - MS_PER_MINUTE$3;
27335
+ const isBacktest = await GET_BACKTEST_FN();
27336
+ // No limit: fetch a single window and return as-is
27337
+ if (limit === undefined) {
27338
+ const to = new Date(alignedTo);
27339
+ const from = new Date(alignedTo - windowMs);
27340
+ return await this._methods.getAggregatedTrades(symbol, from, to, isBacktest);
27341
+ }
27342
+ // With limit: paginate backwards until we have enough trades
27343
+ const result = [];
27344
+ let windowEnd = alignedTo;
27345
+ while (result.length < limit) {
27346
+ const windowStart = windowEnd - windowMs;
27347
+ const to = new Date(windowEnd);
27348
+ const from = new Date(windowStart);
27349
+ const chunk = await this._methods.getAggregatedTrades(symbol, from, to, isBacktest);
27350
+ // Prepend chunk (older data goes first)
27351
+ result.unshift(...chunk);
27352
+ // Move window backwards
27353
+ windowEnd = windowStart;
27354
+ }
27355
+ // Slice to requested limit (most recent trades)
27356
+ return result.slice(-limit);
27357
+ };
27066
27358
  /**
27067
27359
  * Fetches raw candles with flexible date/limit parameters.
27068
27360
  *
@@ -27332,6 +27624,19 @@ class ExchangeUtils {
27332
27624
  const instance = this._getInstance(context.exchangeName);
27333
27625
  return await instance.getOrderBook(symbol, depth);
27334
27626
  };
27627
+ /**
27628
+ * Fetch aggregated trades for a trading pair.
27629
+ *
27630
+ * @param symbol - Trading pair symbol
27631
+ * @param context - Execution context with exchange name
27632
+ * @param limit - Optional maximum number of trades to return
27633
+ * @returns Promise resolving to array of aggregated trade data
27634
+ */
27635
+ this.getAggregatedTrades = async (symbol, context, limit) => {
27636
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES);
27637
+ const instance = this._getInstance(context.exchangeName);
27638
+ return await instance.getAggregatedTrades(symbol, limit);
27639
+ };
27335
27640
  /**
27336
27641
  * Fetches raw candles with flexible date/limit parameters.
27337
27642
  *
@@ -27871,6 +28176,7 @@ const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
27871
28176
  const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
27872
28177
  const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
27873
28178
  const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
28179
+ const GET_AGGREGATED_TRADES_METHOD_NAME = "exchange.getAggregatedTrades";
27874
28180
  /**
27875
28181
  * Checks if trade context is active (execution and method contexts).
27876
28182
  *
@@ -28199,6 +28505,41 @@ async function getNextCandles(symbol, interval, limit) {
28199
28505
  }
28200
28506
  return await bt.exchangeConnectionService.getNextCandles(symbol, interval, limit);
28201
28507
  }
28508
+ /**
28509
+ * Fetches aggregated trades for a trading pair from the registered exchange.
28510
+ *
28511
+ * Trades are fetched backwards from the current execution context time.
28512
+ * If limit is not specified, returns all trades within one CC_AGGREGATED_TRADES_MAX_MINUTES window.
28513
+ * If limit is specified, paginates backwards until at least limit trades are collected.
28514
+ *
28515
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
28516
+ * @param limit - Optional maximum number of trades to fetch
28517
+ * @returns Promise resolving to array of aggregated trade data
28518
+ * @throws Error if execution or method context is missing
28519
+ *
28520
+ * @example
28521
+ * ```typescript
28522
+ * // Fetch last hour of trades
28523
+ * const trades = await getAggregatedTrades("BTCUSDT");
28524
+ *
28525
+ * // Fetch last 500 trades
28526
+ * const lastTrades = await getAggregatedTrades("BTCUSDT", 500);
28527
+ * console.log(lastTrades[0]); // { id, price, qty, timestamp, isBuyerMaker }
28528
+ * ```
28529
+ */
28530
+ async function getAggregatedTrades(symbol, limit) {
28531
+ bt.loggerService.info(GET_AGGREGATED_TRADES_METHOD_NAME, {
28532
+ symbol,
28533
+ limit,
28534
+ });
28535
+ if (!ExecutionContextService.hasContext()) {
28536
+ throw new Error("getAggregatedTrades requires an execution context");
28537
+ }
28538
+ if (!MethodContextService.hasContext()) {
28539
+ throw new Error("getAggregatedTrades requires a method context");
28540
+ }
28541
+ return await bt.exchangeConnectionService.getAggregatedTrades(symbol, limit);
28542
+ }
28202
28543
 
28203
28544
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
28204
28545
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
@@ -30862,6 +31203,416 @@ async function dumpMessages(resultId, history, result, outputDir = "./dump/strat
30862
31203
  }
30863
31204
  }
30864
31205
 
31206
+ const LOG_PERSIST_METHOD_NAME_WAIT_FOR_INIT = "LogPersistUtils.waitForInit";
31207
+ const LOG_PERSIST_METHOD_NAME_LOG = "LogPersistUtils.log";
31208
+ const LOG_PERSIST_METHOD_NAME_DEBUG = "LogPersistUtils.debug";
31209
+ const LOG_PERSIST_METHOD_NAME_INFO = "LogPersistUtils.info";
31210
+ const LOG_PERSIST_METHOD_NAME_WARN = "LogPersistUtils.warn";
31211
+ const LOG_PERSIST_METHOD_NAME_GET_LIST = "LogPersistUtils.getList";
31212
+ const LOG_MEMORY_METHOD_NAME_LOG = "LogMemoryUtils.log";
31213
+ const LOG_MEMORY_METHOD_NAME_DEBUG = "LogMemoryUtils.debug";
31214
+ const LOG_MEMORY_METHOD_NAME_INFO = "LogMemoryUtils.info";
31215
+ const LOG_MEMORY_METHOD_NAME_WARN = "LogMemoryUtils.warn";
31216
+ const LOG_MEMORY_METHOD_NAME_GET_LIST = "LogMemoryUtils.getList";
31217
+ const LOG_ADAPTER_METHOD_NAME_USE_LOGGER = "LogAdapter.useLogger";
31218
+ const LOG_ADAPTER_METHOD_NAME_USE_PERSIST = "LogAdapter.usePersist";
31219
+ const LOG_ADAPTER_METHOD_NAME_USE_MEMORY = "LogAdapter.useMemory";
31220
+ const LOG_ADAPTER_METHOD_NAME_USE_DUMMY = "LogAdapter.useDummy";
31221
+ /**
31222
+ * Backtest execution time retrieval function.
31223
+ * Returns the 'when' timestamp from the execution context if available, otherwise returns the current time.
31224
+ * This allows log entries to be timestamped according to the backtest timeline rather than real-world time, improving log relevance and user experience during backtest analysis.
31225
+ */
31226
+ const GET_DATE_FN = async () => {
31227
+ if (ExecutionContextService.hasContext()) {
31228
+ return new Date(bt.executionContextService.context.when);
31229
+ }
31230
+ return new Date();
31231
+ };
31232
+ /**
31233
+ * Persistent log adapter.
31234
+ *
31235
+ * Features:
31236
+ * - Persists log entries to disk using PersistLogAdapter
31237
+ * - Lazy initialization with singleshot pattern
31238
+ * - Maintains up to CC_MAX_LOG_LINES most recent entries
31239
+ * - Each entry stored individually keyed by its id
31240
+ *
31241
+ * Use this adapter (default) for log persistence across sessions.
31242
+ */
31243
+ class LogPersistUtils {
31244
+ constructor() {
31245
+ /** Array of log entries */
31246
+ this._entries = [];
31247
+ /**
31248
+ * Singleshot initialization function that loads entries from disk.
31249
+ * Protected by singleshot to ensure one-time execution.
31250
+ */
31251
+ this.waitForInit = singleshot(async () => {
31252
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_WAIT_FOR_INIT);
31253
+ const list = await PersistLogAdapter.readLogData();
31254
+ list.sort((a, b) => a.timestamp - b.timestamp);
31255
+ this._entries = list.slice(-GLOBAL_CONFIG.CC_MAX_LOG_LINES);
31256
+ });
31257
+ /**
31258
+ * Logs a general-purpose message.
31259
+ * Persists entry to disk after appending.
31260
+ * @param topic - The log topic / method name
31261
+ * @param args - Additional arguments
31262
+ */
31263
+ this.log = async (topic, ...args) => {
31264
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_LOG, { topic });
31265
+ await this.waitForInit();
31266
+ const date = await GET_DATE_FN();
31267
+ this._entries.push({
31268
+ id: randomString(),
31269
+ type: "log",
31270
+ timestamp: Date.now(),
31271
+ createdAt: date.toISOString(),
31272
+ topic,
31273
+ args,
31274
+ });
31275
+ this._enforceLimit();
31276
+ await PersistLogAdapter.writeLogData(this._entries);
31277
+ };
31278
+ /**
31279
+ * Logs a debug-level message.
31280
+ * Persists entry to disk after appending.
31281
+ * @param topic - The log topic / method name
31282
+ * @param args - Additional arguments
31283
+ */
31284
+ this.debug = async (topic, ...args) => {
31285
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_DEBUG, { topic });
31286
+ await this.waitForInit();
31287
+ const date = await GET_DATE_FN();
31288
+ this._entries.push({
31289
+ id: randomString(),
31290
+ type: "debug",
31291
+ timestamp: Date.now(),
31292
+ createdAt: date.toISOString(),
31293
+ topic,
31294
+ args,
31295
+ });
31296
+ this._enforceLimit();
31297
+ await PersistLogAdapter.writeLogData(this._entries);
31298
+ };
31299
+ /**
31300
+ * Logs an info-level message.
31301
+ * Persists entry to disk after appending.
31302
+ * @param topic - The log topic / method name
31303
+ * @param args - Additional arguments
31304
+ */
31305
+ this.info = async (topic, ...args) => {
31306
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_INFO, { topic });
31307
+ await this.waitForInit();
31308
+ const date = await GET_DATE_FN();
31309
+ this._entries.push({
31310
+ id: randomString(),
31311
+ type: "info",
31312
+ timestamp: Date.now(),
31313
+ createdAt: date.toISOString(),
31314
+ topic,
31315
+ args,
31316
+ });
31317
+ this._enforceLimit();
31318
+ await PersistLogAdapter.writeLogData(this._entries);
31319
+ };
31320
+ /**
31321
+ * Logs a warning-level message.
31322
+ * Persists entry to disk after appending.
31323
+ * @param topic - The log topic / method name
31324
+ * @param args - Additional arguments
31325
+ */
31326
+ this.warn = async (topic, ...args) => {
31327
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_WARN, { topic });
31328
+ await this.waitForInit();
31329
+ const date = await GET_DATE_FN();
31330
+ this._entries.push({
31331
+ id: randomString(),
31332
+ type: "warn",
31333
+ timestamp: Date.now(),
31334
+ createdAt: date.toISOString(),
31335
+ topic,
31336
+ args,
31337
+ });
31338
+ this._enforceLimit();
31339
+ await PersistLogAdapter.writeLogData(this._entries);
31340
+ };
31341
+ /**
31342
+ * Lists all stored log entries.
31343
+ * @returns Array of all log entries
31344
+ */
31345
+ this.getList = async () => {
31346
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_GET_LIST);
31347
+ await this.waitForInit();
31348
+ return [...this._entries];
31349
+ };
31350
+ }
31351
+ /**
31352
+ * Removes oldest entries if limit is exceeded.
31353
+ */
31354
+ _enforceLimit() {
31355
+ if (this._entries.length > GLOBAL_CONFIG.CC_MAX_LOG_LINES) {
31356
+ this._entries.splice(0, this._entries.length - GLOBAL_CONFIG.CC_MAX_LOG_LINES);
31357
+ }
31358
+ }
31359
+ }
31360
+ /**
31361
+ * In-memory log adapter.
31362
+ *
31363
+ * Features:
31364
+ * - Stores log entries in memory only (no persistence)
31365
+ * - Maintains up to CC_MAX_LOG_LINES most recent entries
31366
+ * - Data is lost when application restarts
31367
+ * - Handles all log levels (log, debug, info, warn)
31368
+ *
31369
+ * Use this adapter for testing or when persistence is not required.
31370
+ */
31371
+ class LogMemoryUtils {
31372
+ constructor() {
31373
+ /** Array of log entries */
31374
+ this._entries = [];
31375
+ /**
31376
+ * Logs a general-purpose message.
31377
+ * Appends entry to in-memory array.
31378
+ * @param topic - The log topic / method name
31379
+ * @param args - Additional arguments
31380
+ */
31381
+ this.log = async (topic, ...args) => {
31382
+ bt.loggerService.info(LOG_MEMORY_METHOD_NAME_LOG, { topic });
31383
+ const date = await GET_DATE_FN();
31384
+ this._entries.push({
31385
+ id: randomString(),
31386
+ type: "log",
31387
+ timestamp: Date.now(),
31388
+ createdAt: date.toISOString(),
31389
+ topic,
31390
+ args,
31391
+ });
31392
+ this._enforceLimit();
31393
+ };
31394
+ /**
31395
+ * Logs a debug-level message.
31396
+ * Appends entry to in-memory array.
31397
+ * @param topic - The log topic / method name
31398
+ * @param args - Additional arguments
31399
+ */
31400
+ this.debug = async (topic, ...args) => {
31401
+ bt.loggerService.info(LOG_MEMORY_METHOD_NAME_DEBUG, { topic });
31402
+ const date = await GET_DATE_FN();
31403
+ this._entries.push({
31404
+ id: randomString(),
31405
+ type: "debug",
31406
+ timestamp: Date.now(),
31407
+ createdAt: date.toISOString(),
31408
+ topic,
31409
+ args,
31410
+ });
31411
+ this._enforceLimit();
31412
+ };
31413
+ /**
31414
+ * Logs an info-level message.
31415
+ * Appends entry to in-memory array.
31416
+ * @param topic - The log topic / method name
31417
+ * @param args - Additional arguments
31418
+ */
31419
+ this.info = async (topic, ...args) => {
31420
+ bt.loggerService.info(LOG_MEMORY_METHOD_NAME_INFO, { topic });
31421
+ const date = await GET_DATE_FN();
31422
+ this._entries.push({
31423
+ id: randomString(),
31424
+ type: "info",
31425
+ timestamp: Date.now(),
31426
+ createdAt: date.toISOString(),
31427
+ topic,
31428
+ args,
31429
+ });
31430
+ this._enforceLimit();
31431
+ };
31432
+ /**
31433
+ * Logs a warning-level message.
31434
+ * Appends entry to in-memory array.
31435
+ * @param topic - The log topic / method name
31436
+ * @param args - Additional arguments
31437
+ */
31438
+ this.warn = async (topic, ...args) => {
31439
+ bt.loggerService.info(LOG_MEMORY_METHOD_NAME_WARN, { topic });
31440
+ const date = await GET_DATE_FN();
31441
+ this._entries.push({
31442
+ id: randomString(),
31443
+ type: "warn",
31444
+ timestamp: Date.now(),
31445
+ createdAt: date.toISOString(),
31446
+ topic,
31447
+ args,
31448
+ });
31449
+ this._enforceLimit();
31450
+ };
31451
+ /**
31452
+ * Lists all stored log entries.
31453
+ * @returns Array of all log entries
31454
+ */
31455
+ this.getList = async () => {
31456
+ bt.loggerService.info(LOG_MEMORY_METHOD_NAME_GET_LIST);
31457
+ return [...this._entries];
31458
+ };
31459
+ }
31460
+ /**
31461
+ * Removes oldest entries if limit is exceeded.
31462
+ */
31463
+ _enforceLimit() {
31464
+ if (this._entries.length > GLOBAL_CONFIG.CC_MAX_LOG_LINES) {
31465
+ this._entries.splice(0, this._entries.length - GLOBAL_CONFIG.CC_MAX_LOG_LINES);
31466
+ }
31467
+ }
31468
+ }
31469
+ /**
31470
+ * Dummy log adapter that discards all writes.
31471
+ *
31472
+ * Features:
31473
+ * - No-op implementation for all methods
31474
+ * - getList always returns empty array
31475
+ *
31476
+ * Use this adapter to disable log storage completely.
31477
+ */
31478
+ class LogDummyUtils {
31479
+ /**
31480
+ * Always returns empty array (no storage).
31481
+ * @returns Empty array
31482
+ */
31483
+ async getList() {
31484
+ return [];
31485
+ }
31486
+ /**
31487
+ * No-op handler for general-purpose log.
31488
+ */
31489
+ log() {
31490
+ }
31491
+ /**
31492
+ * No-op handler for debug-level log.
31493
+ */
31494
+ debug() {
31495
+ }
31496
+ /**
31497
+ * No-op handler for info-level log.
31498
+ */
31499
+ info() {
31500
+ }
31501
+ /**
31502
+ * No-op handler for warning-level log.
31503
+ */
31504
+ warn() {
31505
+ }
31506
+ }
31507
+ /**
31508
+ * Log adapter with pluggable storage backend.
31509
+ *
31510
+ * Features:
31511
+ * - Adapter pattern for swappable log implementations
31512
+ * - Default adapter: LogMemoryUtils (in-memory storage)
31513
+ * - Alternative adapters: LogPersistUtils, LogDummyUtils
31514
+ * - Convenience methods: usePersist(), useMemory(), useDummy()
31515
+ */
31516
+ class LogAdapter {
31517
+ constructor() {
31518
+ /** Internal log utils instance */
31519
+ this._log = new LogMemoryUtils();
31520
+ /**
31521
+ * Lists all stored log entries.
31522
+ * Proxies call to the underlying log adapter.
31523
+ * @returns Array of all log entries
31524
+ */
31525
+ this.getList = async () => {
31526
+ if (this._log.getList) {
31527
+ return await this._log.getList();
31528
+ }
31529
+ return [];
31530
+ };
31531
+ /**
31532
+ * Logs a general-purpose message.
31533
+ * Proxies call to the underlying log adapter.
31534
+ * @param topic - The log topic / method name
31535
+ * @param args - Additional arguments
31536
+ */
31537
+ this.log = (topic, ...args) => {
31538
+ if (this._log.log) {
31539
+ this._log.log(topic, ...args);
31540
+ }
31541
+ };
31542
+ /**
31543
+ * Logs a debug-level message.
31544
+ * Proxies call to the underlying log adapter.
31545
+ * @param topic - The log topic / method name
31546
+ * @param args - Additional arguments
31547
+ */
31548
+ this.debug = (topic, ...args) => {
31549
+ if (this._log.debug) {
31550
+ this._log.debug(topic, ...args);
31551
+ }
31552
+ };
31553
+ /**
31554
+ * Logs an info-level message.
31555
+ * Proxies call to the underlying log adapter.
31556
+ * @param topic - The log topic / method name
31557
+ * @param args - Additional arguments
31558
+ */
31559
+ this.info = (topic, ...args) => {
31560
+ if (this._log.info) {
31561
+ this._log.info(topic, ...args);
31562
+ }
31563
+ };
31564
+ /**
31565
+ * Logs a warning-level message.
31566
+ * Proxies call to the underlying log adapter.
31567
+ * @param topic - The log topic / method name
31568
+ * @param args - Additional arguments
31569
+ */
31570
+ this.warn = (topic, ...args) => {
31571
+ if (this._log.warn) {
31572
+ this._log.warn(topic, ...args);
31573
+ }
31574
+ };
31575
+ /**
31576
+ * Sets the log adapter constructor.
31577
+ * All future log operations will use this adapter.
31578
+ * @param Ctor - Constructor for log adapter
31579
+ */
31580
+ this.useLogger = (Ctor) => {
31581
+ bt.loggerService.info(LOG_ADAPTER_METHOD_NAME_USE_LOGGER);
31582
+ this._log = Reflect.construct(Ctor, []);
31583
+ };
31584
+ /**
31585
+ * Switches to persistent log adapter.
31586
+ * Log entries will be persisted to disk.
31587
+ */
31588
+ this.usePersist = () => {
31589
+ bt.loggerService.info(LOG_ADAPTER_METHOD_NAME_USE_PERSIST);
31590
+ this._log = new LogPersistUtils();
31591
+ };
31592
+ /**
31593
+ * Switches to in-memory log adapter (default).
31594
+ * Log entries will be stored in memory only.
31595
+ */
31596
+ this.useMemory = () => {
31597
+ bt.loggerService.info(LOG_ADAPTER_METHOD_NAME_USE_MEMORY);
31598
+ this._log = new LogMemoryUtils();
31599
+ };
31600
+ /**
31601
+ * Switches to dummy log adapter.
31602
+ * All future log writes will be no-ops.
31603
+ */
31604
+ this.useDummy = () => {
31605
+ bt.loggerService.info(LOG_ADAPTER_METHOD_NAME_USE_DUMMY);
31606
+ this._log = new LogDummyUtils();
31607
+ };
31608
+ }
31609
+ }
31610
+ /**
31611
+ * Global singleton instance of LogAdapter.
31612
+ * Provides unified log management with pluggable backends.
31613
+ */
31614
+ const Log = new LogAdapter();
31615
+
30865
31616
  const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
30866
31617
  const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
30867
31618
  const BACKTEST_METHOD_NAME_STOP = "BacktestUtils.stop";
@@ -38260,4 +39011,4 @@ const set = (object, path, value) => {
38260
39011
  }
38261
39012
  };
38262
39013
 
38263
- export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate, waitForCandle, warmCandles };
39014
+ export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, stopStrategy, validate, waitForCandle, warmCandles };