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.cjs CHANGED
@@ -428,6 +428,12 @@ const GLOBAL_CONFIG = {
428
428
  * Default: 20 levels
429
429
  */
430
430
  CC_ORDER_BOOK_MAX_DEPTH_LEVELS: 1000,
431
+ /**
432
+ * Maximum minutes of aggregated trades to fetch when no limit is provided.
433
+ * If limit is not specified, the system will fetch aggregated trades for this many minutes starting from the current time minus the offset.
434
+ * Binance requirement
435
+ */
436
+ CC_AGGREGATED_TRADES_MAX_MINUTES: 60,
431
437
  /**
432
438
  * Maximum number of notifications to keep in storage.
433
439
  * Older notifications are removed when this limit is exceeded.
@@ -442,6 +448,14 @@ const GLOBAL_CONFIG = {
442
448
  * Default: 50 signals
443
449
  */
444
450
  CC_MAX_SIGNALS: 50,
451
+ /**
452
+ * Maximum number of log lines to keep in storage.
453
+ * Older log lines are removed when this limit is exceeded.
454
+ * This helps prevent unbounded log growth which can consume memory and degrade performance over time.
455
+ *
456
+ * Default: 1000 log lines
457
+ */
458
+ CC_MAX_LOG_LINES: 1000,
445
459
  /**
446
460
  * Enables mutex locking for candle fetching to prevent concurrent fetches of the same candles.
447
461
  * This can help avoid redundant API calls and ensure data consistency when multiple processes/threads attempt to fetch candles simultaneously.
@@ -776,6 +790,11 @@ const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_WRITE_DATA = "PersistNotificationUt
776
790
  const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_JSON = "PersistNotificationUtils.useJson";
777
791
  const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_DUMMY = "PersistNotificationUtils.useDummy";
778
792
  const PERSIST_NOTIFICATION_UTILS_METHOD_NAME_USE_PERSIST_NOTIFICATION_ADAPTER = "PersistNotificationUtils.usePersistNotificationAdapter";
793
+ const PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA = "PersistLogUtils.readLogData";
794
+ const PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA = "PersistLogUtils.writeLogData";
795
+ const PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON = "PersistLogUtils.useJson";
796
+ const PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY = "PersistLogUtils.useDummy";
797
+ const PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER = "PersistLogUtils.usePersistLogAdapter";
779
798
  const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
780
799
  const BASE_UNLINK_RETRY_COUNT = 5;
781
800
  const BASE_UNLINK_RETRY_DELAY = 1000;
@@ -1946,6 +1965,106 @@ class PersistNotificationUtils {
1946
1965
  * Used by NotificationPersistLiveUtils/NotificationPersistBacktestUtils for notification persistence.
1947
1966
  */
1948
1967
  const PersistNotificationAdapter = new PersistNotificationUtils();
1968
+ /**
1969
+ * Utility class for managing log entry persistence.
1970
+ *
1971
+ * Features:
1972
+ * - Memoized storage instance
1973
+ * - Custom adapter support
1974
+ * - Atomic read/write operations for LogData
1975
+ * - Each log entry stored as separate file keyed by id
1976
+ * - Crash-safe log state management
1977
+ *
1978
+ * Used by LogPersistUtils for log entry persistence.
1979
+ */
1980
+ class PersistLogUtils {
1981
+ constructor() {
1982
+ this.PersistLogFactory = PersistBase;
1983
+ this._logStorage = null;
1984
+ /**
1985
+ * Reads persisted log entries.
1986
+ *
1987
+ * Called by LogPersistUtils.waitForInit() to restore state.
1988
+ * Uses keys() from PersistBase to iterate over all stored entries.
1989
+ * Returns empty array if no entries exist.
1990
+ *
1991
+ * @returns Promise resolving to array of log entries
1992
+ */
1993
+ this.readLogData = async () => {
1994
+ bt.loggerService.info(PERSIST_LOG_UTILS_METHOD_NAME_READ_DATA);
1995
+ const isInitial = !this._logStorage;
1996
+ const stateStorage = this.getLogStorage();
1997
+ await stateStorage.waitForInit(isInitial);
1998
+ const entries = [];
1999
+ for await (const entryId of stateStorage.keys()) {
2000
+ const entry = await stateStorage.readValue(entryId);
2001
+ entries.push(entry);
2002
+ }
2003
+ return entries;
2004
+ };
2005
+ /**
2006
+ * Writes log entries to disk with atomic file writes.
2007
+ *
2008
+ * Called by LogPersistUtils after each log call to persist state.
2009
+ * Uses entry.id as the storage key for individual file storage.
2010
+ * Uses atomic writes to prevent corruption on crashes.
2011
+ *
2012
+ * @param logData - Log entries to persist
2013
+ * @returns Promise that resolves when write is complete
2014
+ */
2015
+ this.writeLogData = async (logData) => {
2016
+ bt.loggerService.info(PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA);
2017
+ const isInitial = !this._logStorage;
2018
+ const stateStorage = this.getLogStorage();
2019
+ await stateStorage.waitForInit(isInitial);
2020
+ for (const entry of logData) {
2021
+ if (await stateStorage.hasValue(entry.id)) {
2022
+ continue;
2023
+ }
2024
+ await stateStorage.writeValue(entry.id, entry);
2025
+ }
2026
+ };
2027
+ }
2028
+ getLogStorage() {
2029
+ if (!this._logStorage) {
2030
+ this._logStorage = Reflect.construct(this.PersistLogFactory, [
2031
+ `log`,
2032
+ `./dump/data/log/`,
2033
+ ]);
2034
+ }
2035
+ return this._logStorage;
2036
+ }
2037
+ /**
2038
+ * Registers a custom persistence adapter.
2039
+ *
2040
+ * @param Ctor - Custom PersistBase constructor
2041
+ */
2042
+ usePersistLogAdapter(Ctor) {
2043
+ bt.loggerService.info(PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER);
2044
+ this.PersistLogFactory = Ctor;
2045
+ }
2046
+ /**
2047
+ * Switches to the default JSON persist adapter.
2048
+ * All future persistence writes will use JSON storage.
2049
+ */
2050
+ useJson() {
2051
+ bt.loggerService.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON);
2052
+ this.usePersistLogAdapter(PersistBase);
2053
+ }
2054
+ /**
2055
+ * Switches to a dummy persist adapter that discards all writes.
2056
+ * All future persistence writes will be no-ops.
2057
+ */
2058
+ useDummy() {
2059
+ bt.loggerService.log(PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY);
2060
+ this.usePersistLogAdapter(PersistDummy);
2061
+ }
2062
+ }
2063
+ /**
2064
+ * Global singleton instance of PersistLogUtils.
2065
+ * Used by LogPersistUtils for log entry persistence.
2066
+ */
2067
+ const PersistLogAdapter = new PersistLogUtils();
1949
2068
 
1950
2069
  var _a$2, _b$2;
1951
2070
  const BUSY_DELAY = 100;
@@ -2687,6 +2806,55 @@ class ClientExchange {
2687
2806
  GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$5);
2688
2807
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
2689
2808
  }
2809
+ /**
2810
+ * Fetches aggregated trades backwards from execution context time.
2811
+ *
2812
+ * Algorithm:
2813
+ * 1. Align when down to the nearest minute boundary (1-minute granularity)
2814
+ * 2. If limit is not specified: fetch one window of CC_AGGREGATED_TRADES_MAX_MINUTES
2815
+ * 3. If limit is specified: paginate backwards in CC_AGGREGATED_TRADES_MAX_MINUTES
2816
+ * chunks until at least limit trades are collected, then slice to limit
2817
+ *
2818
+ * Look-ahead bias prevention:
2819
+ * - `to` is always aligned down to the minute (never exceeds current when)
2820
+ * - Each pagination window goes strictly backwards from alignedWhen
2821
+ *
2822
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
2823
+ * @param limit - Optional maximum number of trades to return. If not specified,
2824
+ * returns all trades within the last CC_AGGREGATED_TRADES_MAX_MINUTES window.
2825
+ * @returns Promise resolving to array of aggregated trade data
2826
+ */
2827
+ async getAggregatedTrades(symbol, limit) {
2828
+ this.params.logger.debug("ClientExchange getAggregatedTrades", {
2829
+ symbol,
2830
+ limit,
2831
+ });
2832
+ const whenTimestamp = this.params.execution.context.when.getTime();
2833
+ // Align to 1-minute boundary to prevent look-ahead bias
2834
+ const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, 1);
2835
+ const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$5 - MS_PER_MINUTE$5;
2836
+ // No limit: fetch a single window and return as-is
2837
+ if (limit === undefined) {
2838
+ const to = new Date(alignedTo);
2839
+ const from = new Date(alignedTo - windowMs);
2840
+ return await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest);
2841
+ }
2842
+ // With limit: paginate backwards until we have enough trades
2843
+ const result = [];
2844
+ let windowEnd = alignedTo;
2845
+ while (result.length < limit) {
2846
+ const windowStart = windowEnd - windowMs;
2847
+ const to = new Date(windowEnd);
2848
+ const from = new Date(windowStart);
2849
+ const chunk = await this.params.getAggregatedTrades(symbol, from, to, this.params.execution.context.backtest);
2850
+ // Prepend chunk (older data goes first)
2851
+ result.unshift(...chunk);
2852
+ // Move window backwards
2853
+ windowEnd = windowStart;
2854
+ }
2855
+ // Slice to requested limit (most recent trades)
2856
+ return result.slice(-limit);
2857
+ }
2690
2858
  }
2691
2859
 
2692
2860
  /**
@@ -2723,6 +2891,18 @@ const DEFAULT_FORMAT_PRICE_FN$1 = async (_symbol, price, _backtest) => {
2723
2891
  const DEFAULT_GET_ORDER_BOOK_FN$1 = async (_symbol, _depth, _from, _to, _backtest) => {
2724
2892
  throw new Error(`getOrderBook is not implemented for this exchange`);
2725
2893
  };
2894
+ /**
2895
+ * Default implementation for getAggregatedTrades.
2896
+ * Throws an error indicating the method is not implemented.
2897
+ *
2898
+ * @param _symbol - Trading pair symbol (unused)
2899
+ * @param _from - Start of time range (unused - can be ignored in live implementations)
2900
+ * @param _to - End of time range (unused - can be ignored in live implementations)
2901
+ * @param _backtest - Whether running in backtest mode (unused)
2902
+ */
2903
+ const DEFAULT_GET_AGGREGATED_TRADES_FN$1 = async (_symbol, _from, _to, _backtest) => {
2904
+ throw new Error(`getAggregatedTrades is not implemented for this exchange`);
2905
+ };
2726
2906
  /**
2727
2907
  * Connection service routing exchange operations to correct ClientExchange instance.
2728
2908
  *
@@ -2761,7 +2941,7 @@ class ExchangeConnectionService {
2761
2941
  * @returns Configured ClientExchange instance
2762
2942
  */
2763
2943
  this.getExchange = functoolsKit.memoize(([exchangeName]) => `${exchangeName}`, (exchangeName) => {
2764
- 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);
2944
+ 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);
2765
2945
  return new ClientExchange({
2766
2946
  execution: this.executionContextService,
2767
2947
  logger: this.loggerService,
@@ -2770,6 +2950,7 @@ class ExchangeConnectionService {
2770
2950
  formatPrice,
2771
2951
  formatQuantity,
2772
2952
  getOrderBook,
2953
+ getAggregatedTrades,
2773
2954
  callbacks,
2774
2955
  });
2775
2956
  });
@@ -2875,6 +3056,22 @@ class ExchangeConnectionService {
2875
3056
  });
2876
3057
  return await this.getExchange(this.methodContextService.context.exchangeName).getOrderBook(symbol, depth);
2877
3058
  };
3059
+ /**
3060
+ * Fetches aggregated trades for a trading pair using configured exchange.
3061
+ *
3062
+ * Routes to exchange determined by methodContextService.context.exchangeName.
3063
+ *
3064
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
3065
+ * @param limit - Optional maximum number of trades to fetch. If empty returns one window of data.
3066
+ * @returns Promise resolving to array of aggregated trade data
3067
+ */
3068
+ this.getAggregatedTrades = async (symbol, limit) => {
3069
+ this.loggerService.log("exchangeConnectionService getAggregatedTrades", {
3070
+ symbol,
3071
+ limit,
3072
+ });
3073
+ return await this.getExchange(this.methodContextService.context.exchangeName).getAggregatedTrades(symbol, limit);
3074
+ };
2878
3075
  /**
2879
3076
  * Fetches raw candles with flexible date/limit parameters.
2880
3077
  *
@@ -11248,6 +11445,34 @@ class ExchangeCoreService {
11248
11445
  backtest,
11249
11446
  });
11250
11447
  };
11448
+ /**
11449
+ * Fetches aggregated trades with execution context.
11450
+ *
11451
+ * @param symbol - Trading pair symbol
11452
+ * @param when - Timestamp for context (used in backtest mode)
11453
+ * @param backtest - Whether running in backtest mode
11454
+ * @param limit - Optional maximum number of trades to fetch
11455
+ * @returns Promise resolving to array of aggregated trade data
11456
+ */
11457
+ this.getAggregatedTrades = async (symbol, when, backtest, limit) => {
11458
+ this.loggerService.log("exchangeCoreService getAggregatedTrades", {
11459
+ symbol,
11460
+ when,
11461
+ backtest,
11462
+ limit,
11463
+ });
11464
+ if (!MethodContextService.hasContext()) {
11465
+ throw new Error("exchangeCoreService getAggregatedTrades requires a method context");
11466
+ }
11467
+ await this.validate(this.methodContextService.context.exchangeName);
11468
+ return await ExecutionContextService.runInContext(async () => {
11469
+ return await this.exchangeConnectionService.getAggregatedTrades(symbol, limit);
11470
+ }, {
11471
+ symbol,
11472
+ when,
11473
+ backtest,
11474
+ });
11475
+ };
11251
11476
  /**
11252
11477
  * Fetches raw candles with flexible date/limit parameters and execution context.
11253
11478
  *
@@ -26667,6 +26892,7 @@ const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
26667
26892
  const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
26668
26893
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
26669
26894
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
26895
+ const EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES = "ExchangeUtils.getAggregatedTrades";
26670
26896
  const MS_PER_MINUTE$3 = 60000;
26671
26897
  /**
26672
26898
  * Gets current timestamp from execution context if available.
@@ -26722,6 +26948,18 @@ const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price, _backtest) => {
26722
26948
  const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest) => {
26723
26949
  throw new Error(`getOrderBook is not implemented for this exchange`);
26724
26950
  };
26951
+ /**
26952
+ * Default implementation for getAggregatedTrades.
26953
+ * Throws an error indicating the method is not implemented.
26954
+ *
26955
+ * @param _symbol - Trading pair symbol (unused)
26956
+ * @param _from - Start of time range (unused - can be ignored in live implementations)
26957
+ * @param _to - End of time range (unused - can be ignored in live implementations)
26958
+ * @param _backtest - Whether running in backtest mode (unused)
26959
+ */
26960
+ const DEFAULT_GET_AGGREGATED_TRADES_FN = async (_symbol, _from, _to, _backtest) => {
26961
+ throw new Error(`getAggregatedTrades is not implemented for this exchange`);
26962
+ };
26725
26963
  const INTERVAL_MINUTES$3 = {
26726
26964
  "1m": 1,
26727
26965
  "3m": 3,
@@ -26767,11 +27005,13 @@ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
26767
27005
  const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
26768
27006
  const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
26769
27007
  const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
27008
+ const getAggregatedTrades = schema.getAggregatedTrades ?? DEFAULT_GET_AGGREGATED_TRADES_FN;
26770
27009
  return {
26771
27010
  getCandles,
26772
27011
  formatQuantity,
26773
27012
  formatPrice,
26774
27013
  getOrderBook,
27014
+ getAggregatedTrades,
26775
27015
  };
26776
27016
  };
26777
27017
  /**
@@ -27083,6 +27323,58 @@ class ExchangeInstance {
27083
27323
  const isBacktest = await GET_BACKTEST_FN();
27084
27324
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
27085
27325
  };
27326
+ /**
27327
+ * Fetch aggregated trades for a trading pair.
27328
+ *
27329
+ * Calculates time range backwards from current timestamp (or execution context when).
27330
+ * Aligns `to` to 1-minute boundary to prevent look-ahead bias.
27331
+ * If limit is not specified, returns all trades within one CC_AGGREGATED_TRADES_MAX_MINUTES window.
27332
+ * If limit is specified, paginates backwards until at least limit trades are collected.
27333
+ *
27334
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
27335
+ * @param limit - Optional maximum number of trades to return
27336
+ * @returns Promise resolving to array of aggregated trade data
27337
+ *
27338
+ * @example
27339
+ * ```typescript
27340
+ * const instance = new ExchangeInstance("binance");
27341
+ * const trades = await instance.getAggregatedTrades("BTCUSDT");
27342
+ * const lastN = await instance.getAggregatedTrades("BTCUSDT", 500);
27343
+ * ```
27344
+ */
27345
+ this.getAggregatedTrades = async (symbol, limit) => {
27346
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES, {
27347
+ exchangeName: this.exchangeName,
27348
+ symbol,
27349
+ limit,
27350
+ });
27351
+ const when = await GET_TIMESTAMP_FN();
27352
+ // Align to 1-minute boundary to prevent look-ahead bias
27353
+ const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), 1);
27354
+ const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$3 - MS_PER_MINUTE$3;
27355
+ const isBacktest = await GET_BACKTEST_FN();
27356
+ // No limit: fetch a single window and return as-is
27357
+ if (limit === undefined) {
27358
+ const to = new Date(alignedTo);
27359
+ const from = new Date(alignedTo - windowMs);
27360
+ return await this._methods.getAggregatedTrades(symbol, from, to, isBacktest);
27361
+ }
27362
+ // With limit: paginate backwards until we have enough trades
27363
+ const result = [];
27364
+ let windowEnd = alignedTo;
27365
+ while (result.length < limit) {
27366
+ const windowStart = windowEnd - windowMs;
27367
+ const to = new Date(windowEnd);
27368
+ const from = new Date(windowStart);
27369
+ const chunk = await this._methods.getAggregatedTrades(symbol, from, to, isBacktest);
27370
+ // Prepend chunk (older data goes first)
27371
+ result.unshift(...chunk);
27372
+ // Move window backwards
27373
+ windowEnd = windowStart;
27374
+ }
27375
+ // Slice to requested limit (most recent trades)
27376
+ return result.slice(-limit);
27377
+ };
27086
27378
  /**
27087
27379
  * Fetches raw candles with flexible date/limit parameters.
27088
27380
  *
@@ -27352,6 +27644,19 @@ class ExchangeUtils {
27352
27644
  const instance = this._getInstance(context.exchangeName);
27353
27645
  return await instance.getOrderBook(symbol, depth);
27354
27646
  };
27647
+ /**
27648
+ * Fetch aggregated trades for a trading pair.
27649
+ *
27650
+ * @param symbol - Trading pair symbol
27651
+ * @param context - Execution context with exchange name
27652
+ * @param limit - Optional maximum number of trades to return
27653
+ * @returns Promise resolving to array of aggregated trade data
27654
+ */
27655
+ this.getAggregatedTrades = async (symbol, context, limit) => {
27656
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES);
27657
+ const instance = this._getInstance(context.exchangeName);
27658
+ return await instance.getAggregatedTrades(symbol, limit);
27659
+ };
27355
27660
  /**
27356
27661
  * Fetches raw candles with flexible date/limit parameters.
27357
27662
  *
@@ -27891,6 +28196,7 @@ const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
27891
28196
  const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
27892
28197
  const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
27893
28198
  const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
28199
+ const GET_AGGREGATED_TRADES_METHOD_NAME = "exchange.getAggregatedTrades";
27894
28200
  /**
27895
28201
  * Checks if trade context is active (execution and method contexts).
27896
28202
  *
@@ -28219,6 +28525,41 @@ async function getNextCandles(symbol, interval, limit) {
28219
28525
  }
28220
28526
  return await bt.exchangeConnectionService.getNextCandles(symbol, interval, limit);
28221
28527
  }
28528
+ /**
28529
+ * Fetches aggregated trades for a trading pair from the registered exchange.
28530
+ *
28531
+ * Trades are fetched backwards from the current execution context time.
28532
+ * If limit is not specified, returns all trades within one CC_AGGREGATED_TRADES_MAX_MINUTES window.
28533
+ * If limit is specified, paginates backwards until at least limit trades are collected.
28534
+ *
28535
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
28536
+ * @param limit - Optional maximum number of trades to fetch
28537
+ * @returns Promise resolving to array of aggregated trade data
28538
+ * @throws Error if execution or method context is missing
28539
+ *
28540
+ * @example
28541
+ * ```typescript
28542
+ * // Fetch last hour of trades
28543
+ * const trades = await getAggregatedTrades("BTCUSDT");
28544
+ *
28545
+ * // Fetch last 500 trades
28546
+ * const lastTrades = await getAggregatedTrades("BTCUSDT", 500);
28547
+ * console.log(lastTrades[0]); // { id, price, qty, timestamp, isBuyerMaker }
28548
+ * ```
28549
+ */
28550
+ async function getAggregatedTrades(symbol, limit) {
28551
+ bt.loggerService.info(GET_AGGREGATED_TRADES_METHOD_NAME, {
28552
+ symbol,
28553
+ limit,
28554
+ });
28555
+ if (!ExecutionContextService.hasContext()) {
28556
+ throw new Error("getAggregatedTrades requires an execution context");
28557
+ }
28558
+ if (!MethodContextService.hasContext()) {
28559
+ throw new Error("getAggregatedTrades requires a method context");
28560
+ }
28561
+ return await bt.exchangeConnectionService.getAggregatedTrades(symbol, limit);
28562
+ }
28222
28563
 
28223
28564
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
28224
28565
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
@@ -30882,6 +31223,416 @@ async function dumpMessages(resultId, history, result, outputDir = "./dump/strat
30882
31223
  }
30883
31224
  }
30884
31225
 
31226
+ const LOG_PERSIST_METHOD_NAME_WAIT_FOR_INIT = "LogPersistUtils.waitForInit";
31227
+ const LOG_PERSIST_METHOD_NAME_LOG = "LogPersistUtils.log";
31228
+ const LOG_PERSIST_METHOD_NAME_DEBUG = "LogPersistUtils.debug";
31229
+ const LOG_PERSIST_METHOD_NAME_INFO = "LogPersistUtils.info";
31230
+ const LOG_PERSIST_METHOD_NAME_WARN = "LogPersistUtils.warn";
31231
+ const LOG_PERSIST_METHOD_NAME_GET_LIST = "LogPersistUtils.getList";
31232
+ const LOG_MEMORY_METHOD_NAME_LOG = "LogMemoryUtils.log";
31233
+ const LOG_MEMORY_METHOD_NAME_DEBUG = "LogMemoryUtils.debug";
31234
+ const LOG_MEMORY_METHOD_NAME_INFO = "LogMemoryUtils.info";
31235
+ const LOG_MEMORY_METHOD_NAME_WARN = "LogMemoryUtils.warn";
31236
+ const LOG_MEMORY_METHOD_NAME_GET_LIST = "LogMemoryUtils.getList";
31237
+ const LOG_ADAPTER_METHOD_NAME_USE_LOGGER = "LogAdapter.useLogger";
31238
+ const LOG_ADAPTER_METHOD_NAME_USE_PERSIST = "LogAdapter.usePersist";
31239
+ const LOG_ADAPTER_METHOD_NAME_USE_MEMORY = "LogAdapter.useMemory";
31240
+ const LOG_ADAPTER_METHOD_NAME_USE_DUMMY = "LogAdapter.useDummy";
31241
+ /**
31242
+ * Backtest execution time retrieval function.
31243
+ * Returns the 'when' timestamp from the execution context if available, otherwise returns the current time.
31244
+ * 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.
31245
+ */
31246
+ const GET_DATE_FN = async () => {
31247
+ if (ExecutionContextService.hasContext()) {
31248
+ return new Date(bt.executionContextService.context.when);
31249
+ }
31250
+ return new Date();
31251
+ };
31252
+ /**
31253
+ * Persistent log adapter.
31254
+ *
31255
+ * Features:
31256
+ * - Persists log entries to disk using PersistLogAdapter
31257
+ * - Lazy initialization with singleshot pattern
31258
+ * - Maintains up to CC_MAX_LOG_LINES most recent entries
31259
+ * - Each entry stored individually keyed by its id
31260
+ *
31261
+ * Use this adapter (default) for log persistence across sessions.
31262
+ */
31263
+ class LogPersistUtils {
31264
+ constructor() {
31265
+ /** Array of log entries */
31266
+ this._entries = [];
31267
+ /**
31268
+ * Singleshot initialization function that loads entries from disk.
31269
+ * Protected by singleshot to ensure one-time execution.
31270
+ */
31271
+ this.waitForInit = functoolsKit.singleshot(async () => {
31272
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_WAIT_FOR_INIT);
31273
+ const list = await PersistLogAdapter.readLogData();
31274
+ list.sort((a, b) => a.timestamp - b.timestamp);
31275
+ this._entries = list.slice(-GLOBAL_CONFIG.CC_MAX_LOG_LINES);
31276
+ });
31277
+ /**
31278
+ * Logs a general-purpose message.
31279
+ * Persists entry to disk after appending.
31280
+ * @param topic - The log topic / method name
31281
+ * @param args - Additional arguments
31282
+ */
31283
+ this.log = async (topic, ...args) => {
31284
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_LOG, { topic });
31285
+ await this.waitForInit();
31286
+ const date = await GET_DATE_FN();
31287
+ this._entries.push({
31288
+ id: functoolsKit.randomString(),
31289
+ type: "log",
31290
+ timestamp: Date.now(),
31291
+ createdAt: date.toISOString(),
31292
+ topic,
31293
+ args,
31294
+ });
31295
+ this._enforceLimit();
31296
+ await PersistLogAdapter.writeLogData(this._entries);
31297
+ };
31298
+ /**
31299
+ * Logs a debug-level message.
31300
+ * Persists entry to disk after appending.
31301
+ * @param topic - The log topic / method name
31302
+ * @param args - Additional arguments
31303
+ */
31304
+ this.debug = async (topic, ...args) => {
31305
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_DEBUG, { topic });
31306
+ await this.waitForInit();
31307
+ const date = await GET_DATE_FN();
31308
+ this._entries.push({
31309
+ id: functoolsKit.randomString(),
31310
+ type: "debug",
31311
+ timestamp: Date.now(),
31312
+ createdAt: date.toISOString(),
31313
+ topic,
31314
+ args,
31315
+ });
31316
+ this._enforceLimit();
31317
+ await PersistLogAdapter.writeLogData(this._entries);
31318
+ };
31319
+ /**
31320
+ * Logs an info-level message.
31321
+ * Persists entry to disk after appending.
31322
+ * @param topic - The log topic / method name
31323
+ * @param args - Additional arguments
31324
+ */
31325
+ this.info = async (topic, ...args) => {
31326
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_INFO, { topic });
31327
+ await this.waitForInit();
31328
+ const date = await GET_DATE_FN();
31329
+ this._entries.push({
31330
+ id: functoolsKit.randomString(),
31331
+ type: "info",
31332
+ timestamp: Date.now(),
31333
+ createdAt: date.toISOString(),
31334
+ topic,
31335
+ args,
31336
+ });
31337
+ this._enforceLimit();
31338
+ await PersistLogAdapter.writeLogData(this._entries);
31339
+ };
31340
+ /**
31341
+ * Logs a warning-level message.
31342
+ * Persists entry to disk after appending.
31343
+ * @param topic - The log topic / method name
31344
+ * @param args - Additional arguments
31345
+ */
31346
+ this.warn = async (topic, ...args) => {
31347
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_WARN, { topic });
31348
+ await this.waitForInit();
31349
+ const date = await GET_DATE_FN();
31350
+ this._entries.push({
31351
+ id: functoolsKit.randomString(),
31352
+ type: "warn",
31353
+ timestamp: Date.now(),
31354
+ createdAt: date.toISOString(),
31355
+ topic,
31356
+ args,
31357
+ });
31358
+ this._enforceLimit();
31359
+ await PersistLogAdapter.writeLogData(this._entries);
31360
+ };
31361
+ /**
31362
+ * Lists all stored log entries.
31363
+ * @returns Array of all log entries
31364
+ */
31365
+ this.getList = async () => {
31366
+ bt.loggerService.info(LOG_PERSIST_METHOD_NAME_GET_LIST);
31367
+ await this.waitForInit();
31368
+ return [...this._entries];
31369
+ };
31370
+ }
31371
+ /**
31372
+ * Removes oldest entries if limit is exceeded.
31373
+ */
31374
+ _enforceLimit() {
31375
+ if (this._entries.length > GLOBAL_CONFIG.CC_MAX_LOG_LINES) {
31376
+ this._entries.splice(0, this._entries.length - GLOBAL_CONFIG.CC_MAX_LOG_LINES);
31377
+ }
31378
+ }
31379
+ }
31380
+ /**
31381
+ * In-memory log adapter.
31382
+ *
31383
+ * Features:
31384
+ * - Stores log entries in memory only (no persistence)
31385
+ * - Maintains up to CC_MAX_LOG_LINES most recent entries
31386
+ * - Data is lost when application restarts
31387
+ * - Handles all log levels (log, debug, info, warn)
31388
+ *
31389
+ * Use this adapter for testing or when persistence is not required.
31390
+ */
31391
+ class LogMemoryUtils {
31392
+ constructor() {
31393
+ /** Array of log entries */
31394
+ this._entries = [];
31395
+ /**
31396
+ * Logs a general-purpose message.
31397
+ * Appends entry to in-memory array.
31398
+ * @param topic - The log topic / method name
31399
+ * @param args - Additional arguments
31400
+ */
31401
+ this.log = async (topic, ...args) => {
31402
+ bt.loggerService.info(LOG_MEMORY_METHOD_NAME_LOG, { topic });
31403
+ const date = await GET_DATE_FN();
31404
+ this._entries.push({
31405
+ id: functoolsKit.randomString(),
31406
+ type: "log",
31407
+ timestamp: Date.now(),
31408
+ createdAt: date.toISOString(),
31409
+ topic,
31410
+ args,
31411
+ });
31412
+ this._enforceLimit();
31413
+ };
31414
+ /**
31415
+ * Logs a debug-level message.
31416
+ * Appends entry to in-memory array.
31417
+ * @param topic - The log topic / method name
31418
+ * @param args - Additional arguments
31419
+ */
31420
+ this.debug = async (topic, ...args) => {
31421
+ bt.loggerService.info(LOG_MEMORY_METHOD_NAME_DEBUG, { topic });
31422
+ const date = await GET_DATE_FN();
31423
+ this._entries.push({
31424
+ id: functoolsKit.randomString(),
31425
+ type: "debug",
31426
+ timestamp: Date.now(),
31427
+ createdAt: date.toISOString(),
31428
+ topic,
31429
+ args,
31430
+ });
31431
+ this._enforceLimit();
31432
+ };
31433
+ /**
31434
+ * Logs an info-level message.
31435
+ * Appends entry to in-memory array.
31436
+ * @param topic - The log topic / method name
31437
+ * @param args - Additional arguments
31438
+ */
31439
+ this.info = async (topic, ...args) => {
31440
+ bt.loggerService.info(LOG_MEMORY_METHOD_NAME_INFO, { topic });
31441
+ const date = await GET_DATE_FN();
31442
+ this._entries.push({
31443
+ id: functoolsKit.randomString(),
31444
+ type: "info",
31445
+ timestamp: Date.now(),
31446
+ createdAt: date.toISOString(),
31447
+ topic,
31448
+ args,
31449
+ });
31450
+ this._enforceLimit();
31451
+ };
31452
+ /**
31453
+ * Logs a warning-level message.
31454
+ * Appends entry to in-memory array.
31455
+ * @param topic - The log topic / method name
31456
+ * @param args - Additional arguments
31457
+ */
31458
+ this.warn = async (topic, ...args) => {
31459
+ bt.loggerService.info(LOG_MEMORY_METHOD_NAME_WARN, { topic });
31460
+ const date = await GET_DATE_FN();
31461
+ this._entries.push({
31462
+ id: functoolsKit.randomString(),
31463
+ type: "warn",
31464
+ timestamp: Date.now(),
31465
+ createdAt: date.toISOString(),
31466
+ topic,
31467
+ args,
31468
+ });
31469
+ this._enforceLimit();
31470
+ };
31471
+ /**
31472
+ * Lists all stored log entries.
31473
+ * @returns Array of all log entries
31474
+ */
31475
+ this.getList = async () => {
31476
+ bt.loggerService.info(LOG_MEMORY_METHOD_NAME_GET_LIST);
31477
+ return [...this._entries];
31478
+ };
31479
+ }
31480
+ /**
31481
+ * Removes oldest entries if limit is exceeded.
31482
+ */
31483
+ _enforceLimit() {
31484
+ if (this._entries.length > GLOBAL_CONFIG.CC_MAX_LOG_LINES) {
31485
+ this._entries.splice(0, this._entries.length - GLOBAL_CONFIG.CC_MAX_LOG_LINES);
31486
+ }
31487
+ }
31488
+ }
31489
+ /**
31490
+ * Dummy log adapter that discards all writes.
31491
+ *
31492
+ * Features:
31493
+ * - No-op implementation for all methods
31494
+ * - getList always returns empty array
31495
+ *
31496
+ * Use this adapter to disable log storage completely.
31497
+ */
31498
+ class LogDummyUtils {
31499
+ /**
31500
+ * Always returns empty array (no storage).
31501
+ * @returns Empty array
31502
+ */
31503
+ async getList() {
31504
+ return [];
31505
+ }
31506
+ /**
31507
+ * No-op handler for general-purpose log.
31508
+ */
31509
+ log() {
31510
+ }
31511
+ /**
31512
+ * No-op handler for debug-level log.
31513
+ */
31514
+ debug() {
31515
+ }
31516
+ /**
31517
+ * No-op handler for info-level log.
31518
+ */
31519
+ info() {
31520
+ }
31521
+ /**
31522
+ * No-op handler for warning-level log.
31523
+ */
31524
+ warn() {
31525
+ }
31526
+ }
31527
+ /**
31528
+ * Log adapter with pluggable storage backend.
31529
+ *
31530
+ * Features:
31531
+ * - Adapter pattern for swappable log implementations
31532
+ * - Default adapter: LogMemoryUtils (in-memory storage)
31533
+ * - Alternative adapters: LogPersistUtils, LogDummyUtils
31534
+ * - Convenience methods: usePersist(), useMemory(), useDummy()
31535
+ */
31536
+ class LogAdapter {
31537
+ constructor() {
31538
+ /** Internal log utils instance */
31539
+ this._log = new LogMemoryUtils();
31540
+ /**
31541
+ * Lists all stored log entries.
31542
+ * Proxies call to the underlying log adapter.
31543
+ * @returns Array of all log entries
31544
+ */
31545
+ this.getList = async () => {
31546
+ if (this._log.getList) {
31547
+ return await this._log.getList();
31548
+ }
31549
+ return [];
31550
+ };
31551
+ /**
31552
+ * Logs a general-purpose message.
31553
+ * Proxies call to the underlying log adapter.
31554
+ * @param topic - The log topic / method name
31555
+ * @param args - Additional arguments
31556
+ */
31557
+ this.log = (topic, ...args) => {
31558
+ if (this._log.log) {
31559
+ this._log.log(topic, ...args);
31560
+ }
31561
+ };
31562
+ /**
31563
+ * Logs a debug-level message.
31564
+ * Proxies call to the underlying log adapter.
31565
+ * @param topic - The log topic / method name
31566
+ * @param args - Additional arguments
31567
+ */
31568
+ this.debug = (topic, ...args) => {
31569
+ if (this._log.debug) {
31570
+ this._log.debug(topic, ...args);
31571
+ }
31572
+ };
31573
+ /**
31574
+ * Logs an info-level message.
31575
+ * Proxies call to the underlying log adapter.
31576
+ * @param topic - The log topic / method name
31577
+ * @param args - Additional arguments
31578
+ */
31579
+ this.info = (topic, ...args) => {
31580
+ if (this._log.info) {
31581
+ this._log.info(topic, ...args);
31582
+ }
31583
+ };
31584
+ /**
31585
+ * Logs a warning-level message.
31586
+ * Proxies call to the underlying log adapter.
31587
+ * @param topic - The log topic / method name
31588
+ * @param args - Additional arguments
31589
+ */
31590
+ this.warn = (topic, ...args) => {
31591
+ if (this._log.warn) {
31592
+ this._log.warn(topic, ...args);
31593
+ }
31594
+ };
31595
+ /**
31596
+ * Sets the log adapter constructor.
31597
+ * All future log operations will use this adapter.
31598
+ * @param Ctor - Constructor for log adapter
31599
+ */
31600
+ this.useLogger = (Ctor) => {
31601
+ bt.loggerService.info(LOG_ADAPTER_METHOD_NAME_USE_LOGGER);
31602
+ this._log = Reflect.construct(Ctor, []);
31603
+ };
31604
+ /**
31605
+ * Switches to persistent log adapter.
31606
+ * Log entries will be persisted to disk.
31607
+ */
31608
+ this.usePersist = () => {
31609
+ bt.loggerService.info(LOG_ADAPTER_METHOD_NAME_USE_PERSIST);
31610
+ this._log = new LogPersistUtils();
31611
+ };
31612
+ /**
31613
+ * Switches to in-memory log adapter (default).
31614
+ * Log entries will be stored in memory only.
31615
+ */
31616
+ this.useMemory = () => {
31617
+ bt.loggerService.info(LOG_ADAPTER_METHOD_NAME_USE_MEMORY);
31618
+ this._log = new LogMemoryUtils();
31619
+ };
31620
+ /**
31621
+ * Switches to dummy log adapter.
31622
+ * All future log writes will be no-ops.
31623
+ */
31624
+ this.useDummy = () => {
31625
+ bt.loggerService.info(LOG_ADAPTER_METHOD_NAME_USE_DUMMY);
31626
+ this._log = new LogDummyUtils();
31627
+ };
31628
+ }
31629
+ }
31630
+ /**
31631
+ * Global singleton instance of LogAdapter.
31632
+ * Provides unified log management with pluggable backends.
31633
+ */
31634
+ const Log = new LogAdapter();
31635
+
30885
31636
  const BACKTEST_METHOD_NAME_RUN = "BacktestUtils.run";
30886
31637
  const BACKTEST_METHOD_NAME_BACKGROUND = "BacktestUtils.background";
30887
31638
  const BACKTEST_METHOD_NAME_STOP = "BacktestUtils.stop";
@@ -38289,6 +39040,7 @@ exports.Exchange = Exchange;
38289
39040
  exports.ExecutionContextService = ExecutionContextService;
38290
39041
  exports.Heat = Heat;
38291
39042
  exports.Live = Live;
39043
+ exports.Log = Log;
38292
39044
  exports.Markdown = Markdown;
38293
39045
  exports.MarkdownFileBase = MarkdownFileBase;
38294
39046
  exports.MarkdownFolderBase = MarkdownFolderBase;
@@ -38301,6 +39053,7 @@ exports.Performance = Performance;
38301
39053
  exports.PersistBase = PersistBase;
38302
39054
  exports.PersistBreakevenAdapter = PersistBreakevenAdapter;
38303
39055
  exports.PersistCandleAdapter = PersistCandleAdapter;
39056
+ exports.PersistLogAdapter = PersistLogAdapter;
38304
39057
  exports.PersistNotificationAdapter = PersistNotificationAdapter;
38305
39058
  exports.PersistPartialAdapter = PersistPartialAdapter;
38306
39059
  exports.PersistRiskAdapter = PersistRiskAdapter;
@@ -38341,6 +39094,7 @@ exports.formatPrice = formatPrice;
38341
39094
  exports.formatQuantity = formatQuantity;
38342
39095
  exports.get = get;
38343
39096
  exports.getActionSchema = getActionSchema;
39097
+ exports.getAggregatedTrades = getAggregatedTrades;
38344
39098
  exports.getAveragePrice = getAveragePrice;
38345
39099
  exports.getBacktestTimeframe = getBacktestTimeframe;
38346
39100
  exports.getCandles = getCandles;