backtest-kit 3.0.10 → 3.0.12

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.
Files changed (5) hide show
  1. package/README.md +24 -0
  2. package/build/index.cjs +1465 -1331
  3. package/build/index.mjs +1465 -1333
  4. package/package.json +1 -1
  5. package/types.d.ts +154 -109
package/build/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { createActivator } from 'di-kit';
2
2
  import { scoped } from 'di-scoped';
3
3
  import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, errorData, trycatch, retry, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, singlerun } from 'functools-kit';
4
4
  import * as fs from 'fs/promises';
5
- import fs__default from 'fs/promises';
5
+ import fs__default, { stat, opendir, readFile } from 'fs/promises';
6
6
  import path, { join, dirname } from 'path';
7
7
  import crypto from 'crypto';
8
8
  import os from 'os';
@@ -695,7 +695,7 @@ async function writeFileAtomic(file, data, options = {}) {
695
695
  var _a$2;
696
696
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
697
697
  // Calculate step in milliseconds for candle close time validation
698
- const INTERVAL_MINUTES$5 = {
698
+ const INTERVAL_MINUTES$6 = {
699
699
  "1m": 1,
700
700
  "3m": 3,
701
701
  "5m": 5,
@@ -707,7 +707,7 @@ const INTERVAL_MINUTES$5 = {
707
707
  "6h": 360,
708
708
  "8h": 480,
709
709
  };
710
- const MS_PER_MINUTE$3 = 60000;
710
+ const MS_PER_MINUTE$4 = 60000;
711
711
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
712
712
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
713
713
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
@@ -1605,7 +1605,7 @@ class PersistCandleUtils {
1605
1605
  const isInitial = !this.getCandlesStorage.has(key);
1606
1606
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1607
1607
  await stateStorage.waitForInit(isInitial);
1608
- const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$3;
1608
+ const stepMs = INTERVAL_MINUTES$6[interval] * MS_PER_MINUTE$4;
1609
1609
  // Calculate expected timestamps and fetch each candle directly
1610
1610
  const cachedCandles = [];
1611
1611
  for (let i = 0; i < limit; i++) {
@@ -1661,7 +1661,7 @@ class PersistCandleUtils {
1661
1661
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1662
1662
  await stateStorage.waitForInit(isInitial);
1663
1663
  // Calculate step in milliseconds to determine candle close time
1664
- const stepMs = INTERVAL_MINUTES$5[interval] * MS_PER_MINUTE$3;
1664
+ const stepMs = INTERVAL_MINUTES$6[interval] * MS_PER_MINUTE$4;
1665
1665
  const now = Date.now();
1666
1666
  // Write each candle as a separate file, skipping incomplete candles
1667
1667
  for (const candle of candles) {
@@ -1918,8 +1918,8 @@ class PersistNotificationUtils {
1918
1918
  */
1919
1919
  const PersistNotificationAdapter = new PersistNotificationUtils();
1920
1920
 
1921
- const MS_PER_MINUTE$2 = 60000;
1922
- const INTERVAL_MINUTES$4 = {
1921
+ const MS_PER_MINUTE$3 = 60000;
1922
+ const INTERVAL_MINUTES$5 = {
1923
1923
  "1m": 1,
1924
1924
  "3m": 3,
1925
1925
  "5m": 5,
@@ -1948,8 +1948,8 @@ const INTERVAL_MINUTES$4 = {
1948
1948
  * @param intervalMinutes - Interval in minutes
1949
1949
  * @returns Aligned timestamp rounded down to interval boundary
1950
1950
  */
1951
- const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
1952
- const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
1951
+ const ALIGN_TO_INTERVAL_FN$2 = (timestamp, intervalMinutes) => {
1952
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$3;
1953
1953
  return Math.floor(timestamp / intervalMs) * intervalMs;
1954
1954
  };
1955
1955
  /**
@@ -2094,9 +2094,9 @@ const WRITE_CANDLES_CACHE_FN$1 = trycatch(queued(async (candles, dto, self) => {
2094
2094
  * @returns Promise resolving to array of candle data
2095
2095
  */
2096
2096
  const GET_CANDLES_FN = async (dto, since, self) => {
2097
- const step = INTERVAL_MINUTES$4[dto.interval];
2097
+ const step = INTERVAL_MINUTES$5[dto.interval];
2098
2098
  const sinceTimestamp = since.getTime();
2099
- const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$2;
2099
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$3;
2100
2100
  // Try to read from cache first
2101
2101
  const cachedCandles = await READ_CANDLES_CACHE_FN$1(dto, sinceTimestamp, untilTimestamp, self);
2102
2102
  if (cachedCandles !== null) {
@@ -2204,14 +2204,14 @@ class ClientExchange {
2204
2204
  interval,
2205
2205
  limit,
2206
2206
  });
2207
- const step = INTERVAL_MINUTES$4[interval];
2207
+ const step = INTERVAL_MINUTES$5[interval];
2208
2208
  if (!step) {
2209
2209
  throw new Error(`ClientExchange unknown interval=${interval}`);
2210
2210
  }
2211
- const stepMs = step * MS_PER_MINUTE$2;
2211
+ const stepMs = step * MS_PER_MINUTE$3;
2212
2212
  // Align when down to interval boundary
2213
2213
  const whenTimestamp = this.params.execution.context.when.getTime();
2214
- const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
2214
+ const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
2215
2215
  // Calculate since: go back limit candles from aligned when
2216
2216
  const sinceTimestamp = alignedWhen - limit * stepMs;
2217
2217
  const since = new Date(sinceTimestamp);
@@ -2285,15 +2285,15 @@ class ClientExchange {
2285
2285
  if (!this.params.execution.context.backtest) {
2286
2286
  throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
2287
2287
  }
2288
- const step = INTERVAL_MINUTES$4[interval];
2288
+ const step = INTERVAL_MINUTES$5[interval];
2289
2289
  if (!step) {
2290
2290
  throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
2291
2291
  }
2292
- const stepMs = step * MS_PER_MINUTE$2;
2292
+ const stepMs = step * MS_PER_MINUTE$3;
2293
2293
  const now = Date.now();
2294
2294
  // Align when down to interval boundary
2295
2295
  const whenTimestamp = this.params.execution.context.when.getTime();
2296
- const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
2296
+ const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
2297
2297
  // since = alignedWhen (start from aligned when, going forward)
2298
2298
  const sinceTimestamp = alignedWhen;
2299
2299
  const since = new Date(sinceTimestamp);
@@ -2453,13 +2453,13 @@ class ClientExchange {
2453
2453
  sDate,
2454
2454
  eDate,
2455
2455
  });
2456
- const step = INTERVAL_MINUTES$4[interval];
2456
+ const step = INTERVAL_MINUTES$5[interval];
2457
2457
  if (!step) {
2458
2458
  throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
2459
2459
  }
2460
- const stepMs = step * MS_PER_MINUTE$2;
2460
+ const stepMs = step * MS_PER_MINUTE$3;
2461
2461
  const whenTimestamp = this.params.execution.context.when.getTime();
2462
- const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
2462
+ const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
2463
2463
  let sinceTimestamp;
2464
2464
  let calculatedLimit;
2465
2465
  // Case 1: all three parameters provided
@@ -2471,7 +2471,7 @@ class ClientExchange {
2471
2471
  throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2472
2472
  }
2473
2473
  // Align sDate down to interval boundary
2474
- sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
2474
+ sinceTimestamp = ALIGN_TO_INTERVAL_FN$2(sDate, step);
2475
2475
  calculatedLimit = limit;
2476
2476
  }
2477
2477
  // Case 2: sDate + eDate (no limit) - calculate limit from date range
@@ -2483,8 +2483,8 @@ class ClientExchange {
2483
2483
  throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2484
2484
  }
2485
2485
  // Align sDate down to interval boundary
2486
- sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
2487
- const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
2486
+ sinceTimestamp = ALIGN_TO_INTERVAL_FN$2(sDate, step);
2487
+ const alignedEDate = ALIGN_TO_INTERVAL_FN$2(eDate, step);
2488
2488
  calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
2489
2489
  if (calculatedLimit <= 0) {
2490
2490
  throw new Error(`ClientExchange getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
@@ -2496,14 +2496,14 @@ class ClientExchange {
2496
2496
  throw new Error(`ClientExchange getRawCandles: eDate (${eDate}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
2497
2497
  }
2498
2498
  // Align eDate down and calculate sinceTimestamp
2499
- const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
2499
+ const alignedEDate = ALIGN_TO_INTERVAL_FN$2(eDate, step);
2500
2500
  sinceTimestamp = alignedEDate - limit * stepMs;
2501
2501
  calculatedLimit = limit;
2502
2502
  }
2503
2503
  // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
2504
2504
  else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
2505
2505
  // Align sDate down to interval boundary
2506
- sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
2506
+ sinceTimestamp = ALIGN_TO_INTERVAL_FN$2(sDate, step);
2507
2507
  const endTimestamp = sinceTimestamp + limit * stepMs;
2508
2508
  if (endTimestamp > whenTimestamp) {
2509
2509
  throw new Error(`ClientExchange getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds execution context when (${whenTimestamp}). Look-ahead bias protection.`);
@@ -2583,10 +2583,10 @@ class ClientExchange {
2583
2583
  depth,
2584
2584
  });
2585
2585
  const whenTimestamp = this.params.execution.context.when.getTime();
2586
- const alignedTo = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
2586
+ const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
2587
2587
  const to = new Date(alignedTo);
2588
2588
  const from = new Date(alignedTo -
2589
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$2);
2589
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$3);
2590
2590
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
2591
2591
  }
2592
2592
  }
@@ -3051,7 +3051,7 @@ const beginTime = (run) => (...args) => {
3051
3051
  return fn();
3052
3052
  };
3053
3053
 
3054
- const INTERVAL_MINUTES$3 = {
3054
+ const INTERVAL_MINUTES$4 = {
3055
3055
  "1m": 1,
3056
3056
  "3m": 3,
3057
3057
  "5m": 5,
@@ -3564,7 +3564,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
3564
3564
  }
3565
3565
  const currentTime = self.params.execution.context.when.getTime();
3566
3566
  {
3567
- const intervalMinutes = INTERVAL_MINUTES$3[self.params.interval];
3567
+ const intervalMinutes = INTERVAL_MINUTES$4[self.params.interval];
3568
3568
  const intervalMs = intervalMinutes * 60 * 1000;
3569
3569
  // Проверяем что прошел нужный интервал с последнего getSignal
3570
3570
  if (self._lastSignalTimestamp !== null &&
@@ -8175,7 +8175,7 @@ class StrategyConnectionService {
8175
8175
  * Maps FrameInterval to minutes for timestamp calculation.
8176
8176
  * Used to generate timeframe arrays with proper spacing.
8177
8177
  */
8178
- const INTERVAL_MINUTES$2 = {
8178
+ const INTERVAL_MINUTES$3 = {
8179
8179
  "1m": 1,
8180
8180
  "3m": 3,
8181
8181
  "5m": 5,
@@ -8230,7 +8230,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
8230
8230
  symbol,
8231
8231
  });
8232
8232
  const { interval, startDate, endDate } = self.params;
8233
- const intervalMinutes = INTERVAL_MINUTES$2[interval];
8233
+ const intervalMinutes = INTERVAL_MINUTES$3[interval];
8234
8234
  if (!intervalMinutes) {
8235
8235
  throw new Error(`ClientFrame unknown interval: ${interval}`);
8236
8236
  }
@@ -26020,402 +26020,1262 @@ async function getBacktestTimeframe(symbol) {
26020
26020
  return await bt.frameCoreService.getTimeframe(symbol, bt.methodContextService.context.frameName);
26021
26021
  }
26022
26022
 
26023
- const METHOD_NAME = "validate.validate";
26023
+ const EXCHANGE_METHOD_NAME_GET_CANDLES = "ExchangeUtils.getCandles";
26024
+ const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
26025
+ const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
26026
+ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
26027
+ const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
26028
+ const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
26029
+ const MS_PER_MINUTE$2 = 60000;
26024
26030
  /**
26025
- * Retrieves all registered exchanges as a map
26026
- * @private
26027
- * @returns Map of exchange names
26031
+ * Gets current timestamp from execution context if available.
26032
+ * Returns current Date() if no execution context exists (non-trading GUI).
26028
26033
  */
26029
- const getExchangeMap = async () => {
26030
- const exchangeMap = {};
26031
- for (const { exchangeName } of await bt.exchangeValidationService.list()) {
26032
- Object.assign(exchangeMap, { [exchangeName]: exchangeName });
26034
+ const GET_TIMESTAMP_FN = async () => {
26035
+ if (ExecutionContextService.hasContext()) {
26036
+ return new Date(bt.executionContextService.context.when);
26033
26037
  }
26034
- return exchangeMap;
26038
+ return new Date();
26035
26039
  };
26036
26040
  /**
26037
- * Retrieves all registered frames as a map
26038
- * @private
26039
- * @returns Map of frame names
26041
+ * Gets backtest mode flag from execution context if available.
26042
+ * Returns false if no execution context exists (live mode).
26040
26043
  */
26041
- const getFrameMap = async () => {
26042
- const frameMap = {};
26043
- for (const { frameName } of await bt.frameValidationService.list()) {
26044
- Object.assign(frameMap, { [frameName]: frameName });
26044
+ const GET_BACKTEST_FN = async () => {
26045
+ if (ExecutionContextService.hasContext()) {
26046
+ return bt.executionContextService.context.backtest;
26045
26047
  }
26046
- return frameMap;
26048
+ return false;
26047
26049
  };
26048
26050
  /**
26049
- * Retrieves all registered strategies as a map
26050
- * @private
26051
- * @returns Map of strategy names
26051
+ * Default implementation for getCandles.
26052
+ * Throws an error indicating the method is not implemented.
26052
26053
  */
26053
- const getStrategyMap = async () => {
26054
- const strategyMap = {};
26055
- for (const { strategyName } of await bt.strategyValidationService.list()) {
26056
- Object.assign(strategyMap, { [strategyName]: strategyName });
26057
- }
26058
- return strategyMap;
26054
+ const DEFAULT_GET_CANDLES_FN = async (_symbol, _interval, _since, _limit, _backtest) => {
26055
+ throw new Error(`getCandles is not implemented for this exchange`);
26059
26056
  };
26060
26057
  /**
26061
- * Retrieves all registered risk profiles as a map
26062
- * @private
26063
- * @returns Map of risk names
26058
+ * Default implementation for formatQuantity.
26059
+ * Returns Bitcoin precision on Binance (8 decimal places).
26064
26060
  */
26065
- const getRiskMap = async () => {
26066
- const riskMap = {};
26067
- for (const { riskName } of await bt.riskValidationService.list()) {
26068
- Object.assign(riskMap, { [riskName]: riskName });
26069
- }
26070
- return riskMap;
26061
+ const DEFAULT_FORMAT_QUANTITY_FN = async (_symbol, quantity, _backtest) => {
26062
+ return quantity.toFixed(8);
26071
26063
  };
26072
26064
  /**
26073
- * Retrieves all registered action handlers as a map
26074
- * @private
26075
- * @returns Map of action names
26065
+ * Default implementation for formatPrice.
26066
+ * Returns Bitcoin precision on Binance (2 decimal places).
26076
26067
  */
26077
- const getActionMap = async () => {
26078
- const actionMap = {};
26079
- for (const { actionName } of await bt.actionValidationService.list()) {
26080
- Object.assign(actionMap, { [actionName]: actionName });
26081
- }
26082
- return actionMap;
26068
+ const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price, _backtest) => {
26069
+ return price.toFixed(2);
26083
26070
  };
26084
26071
  /**
26085
- * Retrieves all registered sizing strategies as a map
26086
- * @private
26087
- * @returns Map of sizing names
26072
+ * Default implementation for getOrderBook.
26073
+ * Throws an error indicating the method is not implemented.
26074
+ *
26075
+ * @param _symbol - Trading pair symbol (unused)
26076
+ * @param _depth - Maximum depth levels (unused)
26077
+ * @param _from - Start of time range (unused - can be ignored in live implementations)
26078
+ * @param _to - End of time range (unused - can be ignored in live implementations)
26079
+ * @param _backtest - Whether running in backtest mode (unused)
26088
26080
  */
26089
- const getSizingMap = async () => {
26090
- const sizingMap = {};
26091
- for (const { sizingName } of await bt.sizingValidationService.list()) {
26092
- Object.assign(sizingMap, { [sizingName]: sizingName });
26093
- }
26094
- return sizingMap;
26081
+ const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest) => {
26082
+ throw new Error(`getOrderBook is not implemented for this exchange`);
26095
26083
  };
26096
- /**
26097
- * Retrieves all registered walkers as a map
26098
- * @private
26099
- * @returns Map of walker names
26100
- */
26101
- const getWalkerMap = async () => {
26102
- const walkerMap = {};
26103
- for (const { walkerName } of await bt.walkerValidationService.list()) {
26104
- Object.assign(walkerMap, { [walkerName]: walkerName });
26105
- }
26106
- return walkerMap;
26084
+ const INTERVAL_MINUTES$2 = {
26085
+ "1m": 1,
26086
+ "3m": 3,
26087
+ "5m": 5,
26088
+ "15m": 15,
26089
+ "30m": 30,
26090
+ "1h": 60,
26091
+ "2h": 120,
26092
+ "4h": 240,
26093
+ "6h": 360,
26094
+ "8h": 480,
26107
26095
  };
26108
26096
  /**
26109
- * Internal validation function that processes all provided entity enums.
26097
+ * Aligns timestamp down to the nearest interval boundary.
26098
+ * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
26110
26099
  *
26111
- * Iterates through each enum's values and validates them against their
26112
- * respective validation services. Uses memoized validation for performance.
26100
+ * Candle timestamp convention:
26101
+ * - Candle timestamp = openTime (when candle opens)
26102
+ * - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
26113
26103
  *
26114
- * If entity enums are not provided, fetches all registered entities from
26115
- * their respective validation services and validates them.
26104
+ * Adapter contract:
26105
+ * - Adapter must return candles with timestamp = openTime
26106
+ * - First returned candle.timestamp must equal aligned since
26107
+ * - Adapter must return exactly `limit` candles
26116
26108
  *
26117
- * @private
26118
- * @param args - Validation arguments containing entity name enums
26119
- * @throws {Error} If any entity name is not found in its registry
26109
+ * @param timestamp - Timestamp in milliseconds
26110
+ * @param intervalMinutes - Interval in minutes
26111
+ * @returns Aligned timestamp rounded down to interval boundary
26120
26112
  */
26121
- const validateInternal = async (args) => {
26122
- const { ExchangeName = await getExchangeMap(), FrameName = await getFrameMap(), StrategyName = await getStrategyMap(), RiskName = await getRiskMap(), ActionName = await getActionMap(), SizingName = await getSizingMap(), WalkerName = await getWalkerMap(), } = args;
26123
- for (const exchangeName of Object.values(ExchangeName)) {
26124
- bt.exchangeValidationService.validate(exchangeName, METHOD_NAME);
26125
- }
26126
- for (const frameName of Object.values(FrameName)) {
26127
- bt.frameValidationService.validate(frameName, METHOD_NAME);
26128
- }
26129
- for (const strategyName of Object.values(StrategyName)) {
26130
- bt.strategyValidationService.validate(strategyName, METHOD_NAME);
26131
- }
26132
- for (const riskName of Object.values(RiskName)) {
26133
- bt.riskValidationService.validate(riskName, METHOD_NAME);
26134
- }
26135
- for (const actionName of Object.values(ActionName)) {
26136
- bt.actionValidationService.validate(actionName, METHOD_NAME);
26137
- }
26138
- for (const sizingName of Object.values(SizingName)) {
26139
- bt.sizingValidationService.validate(sizingName, METHOD_NAME);
26140
- }
26141
- for (const walkerName of Object.values(WalkerName)) {
26142
- bt.walkerValidationService.validate(walkerName, METHOD_NAME);
26143
- }
26113
+ const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
26114
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
26115
+ return Math.floor(timestamp / intervalMs) * intervalMs;
26144
26116
  };
26145
26117
  /**
26146
- * Validates the existence of all provided entity names across validation services.
26147
- *
26148
- * This function accepts enum objects for various entity types (exchanges, frames,
26149
- * strategies, risks, sizings, walkers) and validates that each entity
26150
- * name exists in its respective registry. Validation results are memoized for performance.
26151
- *
26152
- * If no arguments are provided (or specific entity types are omitted), the function
26153
- * automatically fetches and validates ALL registered entities from their respective
26154
- * validation services. This is useful for comprehensive validation of the entire setup.
26155
- *
26156
- * Use this before running backtests or optimizations to ensure all referenced
26157
- * entities are properly registered and configured.
26158
- *
26159
- * @public
26160
- * @param args - Partial validation arguments containing entity name enums to validate.
26161
- * If empty or omitted, validates all registered entities.
26162
- * @throws {Error} If any entity name is not found in its validation service
26163
- *
26164
- * @example
26165
- * ```typescript
26166
- * // Validate ALL registered entities (exchanges, frames, strategies, etc.)
26167
- * await validate({});
26168
- * ```
26169
- *
26170
- * @example
26171
- * ```typescript
26172
- * // Define your entity name enums
26173
- * enum ExchangeName {
26174
- * BINANCE = "binance",
26175
- * BYBIT = "bybit"
26176
- * }
26177
- *
26178
- * enum StrategyName {
26179
- * MOMENTUM_BTC = "momentum-btc"
26180
- * }
26181
- *
26182
- * // Validate specific entities before running backtest
26183
- * await validate({
26184
- * ExchangeName,
26185
- * StrategyName,
26186
- * });
26187
- * ```
26188
- *
26189
- * @example
26190
- * ```typescript
26191
- * // Validate specific entity types
26192
- * await validate({
26193
- * RiskName: { CONSERVATIVE: "conservative" },
26194
- * SizingName: { FIXED_1000: "fixed-1000" },
26195
- * });
26196
- * ```
26197
- */
26198
- async function validate(args = {}) {
26199
- bt.loggerService.log(METHOD_NAME);
26200
- return await validateInternal(args);
26201
- }
26202
-
26203
- const GET_STRATEGY_METHOD_NAME = "get.getStrategySchema";
26204
- const GET_EXCHANGE_METHOD_NAME = "get.getExchangeSchema";
26205
- const GET_FRAME_METHOD_NAME = "get.getFrameSchema";
26206
- const GET_WALKER_METHOD_NAME = "get.getWalkerSchema";
26207
- const GET_SIZING_METHOD_NAME = "get.getSizingSchema";
26208
- const GET_RISK_METHOD_NAME = "get.getRiskSchema";
26209
- const GET_ACTION_METHOD_NAME = "get.getActionSchema";
26210
- /**
26211
- * Retrieves a registered strategy schema by name.
26212
- *
26213
- * @param strategyName - Unique strategy identifier
26214
- * @returns The strategy schema configuration object
26215
- * @throws Error if strategy is not registered
26216
- *
26217
- * @example
26218
- * ```typescript
26219
- * const strategy = getStrategy("my-strategy");
26220
- * console.log(strategy.interval); // "5m"
26221
- * console.log(strategy.getSignal); // async function
26222
- * ```
26223
- */
26224
- function getStrategySchema(strategyName) {
26225
- bt.loggerService.log(GET_STRATEGY_METHOD_NAME, {
26226
- strategyName,
26227
- });
26228
- bt.strategyValidationService.validate(strategyName, GET_STRATEGY_METHOD_NAME);
26229
- return bt.strategySchemaService.get(strategyName);
26230
- }
26231
- /**
26232
- * Retrieves a registered exchange schema by name.
26233
- *
26234
- * @param exchangeName - Unique exchange identifier
26235
- * @returns The exchange schema configuration object
26236
- * @throws Error if exchange is not registered
26237
- *
26238
- * @example
26239
- * ```typescript
26240
- * const exchange = getExchange("binance");
26241
- * console.log(exchange.getCandles); // async function
26242
- * console.log(exchange.formatPrice); // async function
26243
- * ```
26244
- */
26245
- function getExchangeSchema(exchangeName) {
26246
- bt.loggerService.log(GET_EXCHANGE_METHOD_NAME, {
26247
- exchangeName,
26248
- });
26249
- bt.exchangeValidationService.validate(exchangeName, GET_EXCHANGE_METHOD_NAME);
26250
- return bt.exchangeSchemaService.get(exchangeName);
26251
- }
26252
- /**
26253
- * Retrieves a registered frame schema by name.
26254
- *
26255
- * @param frameName - Unique frame identifier
26256
- * @returns The frame schema configuration object
26257
- * @throws Error if frame is not registered
26258
- *
26259
- * @example
26260
- * ```typescript
26261
- * const frame = getFrame("1d-backtest");
26262
- * console.log(frame.interval); // "1m"
26263
- * console.log(frame.startDate); // Date object
26264
- * console.log(frame.endDate); // Date object
26265
- * ```
26266
- */
26267
- function getFrameSchema(frameName) {
26268
- bt.loggerService.log(GET_FRAME_METHOD_NAME, {
26269
- frameName,
26270
- });
26271
- bt.frameValidationService.validate(frameName, GET_FRAME_METHOD_NAME);
26272
- return bt.frameSchemaService.get(frameName);
26273
- }
26274
- /**
26275
- * Retrieves a registered walker schema by name.
26276
- *
26277
- * @param walkerName - Unique walker identifier
26278
- * @returns The walker schema configuration object
26279
- * @throws Error if walker is not registered
26280
- *
26281
- * @example
26282
- * ```typescript
26283
- * const walker = getWalker("llm-prompt-optimizer");
26284
- * console.log(walker.exchangeName); // "binance"
26285
- * console.log(walker.frameName); // "1d-backtest"
26286
- * console.log(walker.strategies); // ["my-strategy-v1", "my-strategy-v2"]
26287
- * console.log(walker.metric); // "sharpeRatio"
26288
- * ```
26289
- */
26290
- function getWalkerSchema(walkerName) {
26291
- bt.loggerService.log(GET_WALKER_METHOD_NAME, {
26292
- walkerName,
26293
- });
26294
- bt.walkerValidationService.validate(walkerName, GET_WALKER_METHOD_NAME);
26295
- return bt.walkerSchemaService.get(walkerName);
26296
- }
26297
- /**
26298
- * Retrieves a registered sizing schema by name.
26299
- *
26300
- * @param sizingName - Unique sizing identifier
26301
- * @returns The sizing schema configuration object
26302
- * @throws Error if sizing is not registered
26118
+ * Creates exchange instance with methods resolved once during construction.
26119
+ * Applies default implementations where schema methods are not provided.
26303
26120
  *
26304
- * @example
26305
- * ```typescript
26306
- * const sizing = getSizing("conservative");
26307
- * console.log(sizing.method); // "fixed-percentage"
26308
- * console.log(sizing.riskPercentage); // 1
26309
- * console.log(sizing.maxPositionPercentage); // 10
26310
- * ```
26121
+ * @param schema - Exchange schema from registry
26122
+ * @returns Object with resolved exchange methods
26311
26123
  */
26312
- function getSizingSchema(sizingName) {
26313
- bt.loggerService.log(GET_SIZING_METHOD_NAME, {
26314
- sizingName,
26315
- });
26316
- bt.sizingValidationService.validate(sizingName, GET_SIZING_METHOD_NAME);
26317
- return bt.sizingSchemaService.get(sizingName);
26318
- }
26124
+ const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
26125
+ const getCandles = schema.getCandles ?? DEFAULT_GET_CANDLES_FN;
26126
+ const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
26127
+ const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
26128
+ const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
26129
+ return {
26130
+ getCandles,
26131
+ formatQuantity,
26132
+ formatPrice,
26133
+ getOrderBook,
26134
+ };
26135
+ };
26319
26136
  /**
26320
- * Retrieves a registered risk schema by name.
26137
+ * Attempts to read candles from cache.
26321
26138
  *
26322
- * @param riskName - Unique risk identifier
26323
- * @returns The risk schema configuration object
26324
- * @throws Error if risk is not registered
26139
+ * Cache lookup calculates expected timestamps:
26140
+ * sinceTimestamp + i * stepMs for i = 0..limit-1
26141
+ * Returns all candles if found, null if any missing.
26325
26142
  *
26326
- * @example
26327
- * ```typescript
26328
- * const risk = getRisk("conservative");
26329
- * console.log(risk.maxConcurrentPositions); // 5
26330
- * console.log(risk.validations); // Array of validation functions
26331
- * ```
26143
+ * @param dto - Data transfer object containing symbol, interval, and limit
26144
+ * @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
26145
+ * @param untilTimestamp - Unused, kept for API compatibility
26146
+ * @param exchangeName - Exchange name
26147
+ * @returns Cached candles array (exactly limit) or null if cache miss
26332
26148
  */
26333
- function getRiskSchema(riskName) {
26334
- bt.loggerService.log(GET_RISK_METHOD_NAME, {
26335
- riskName,
26336
- });
26337
- bt.riskValidationService.validate(riskName, GET_RISK_METHOD_NAME);
26338
- return bt.riskSchemaService.get(riskName);
26339
- }
26149
+ const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
26150
+ // PersistCandleAdapter.readCandlesData calculates expected timestamps:
26151
+ // sinceTimestamp + i * stepMs for i = 0..limit-1
26152
+ // Returns all candles if found, null if any missing
26153
+ const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
26154
+ // Return cached data only if we have exactly the requested limit
26155
+ if (cachedCandles?.length === dto.limit) {
26156
+ bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
26157
+ return cachedCandles;
26158
+ }
26159
+ bt.loggerService.warn(`ExchangeInstance READ_CANDLES_CACHE_FN: cache inconsistent (count or range mismatch) for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
26160
+ return null;
26161
+ }, {
26162
+ fallback: async (error) => {
26163
+ const message = `ExchangeInstance READ_CANDLES_CACHE_FN: cache read failed`;
26164
+ const payload = {
26165
+ error: errorData(error),
26166
+ message: getErrorMessage(error),
26167
+ };
26168
+ bt.loggerService.warn(message, payload);
26169
+ console.warn(message, payload);
26170
+ errorEmitter.next(error);
26171
+ },
26172
+ defaultValue: null,
26173
+ });
26340
26174
  /**
26341
- * Retrieves a registered action schema by name.
26175
+ * Writes candles to cache with error handling.
26342
26176
  *
26343
- * @param actionName - Unique action identifier
26344
- * @returns The action schema configuration object
26345
- * @throws Error if action is not registered
26177
+ * The candles passed to this function should be validated:
26178
+ * - First candle.timestamp equals aligned sinceTimestamp (openTime)
26179
+ * - Exact number of candles as requested (limit)
26180
+ * - Sequential timestamps: sinceTimestamp + i * stepMs
26346
26181
  *
26347
- * @example
26348
- * ```typescript
26349
- * const action = getAction("telegram-notifier");
26350
- * console.log(action.handler); // Class constructor or object
26351
- * console.log(action.callbacks); // Optional lifecycle callbacks
26352
- * ```
26182
+ * @param candles - Array of validated candle data to cache
26183
+ * @param dto - Data transfer object containing symbol, interval, and limit
26184
+ * @param exchangeName - Exchange name
26353
26185
  */
26354
- function getActionSchema(actionName) {
26355
- bt.loggerService.log(GET_ACTION_METHOD_NAME, {
26356
- actionName,
26357
- });
26358
- bt.actionValidationService.validate(actionName, GET_ACTION_METHOD_NAME);
26359
- return bt.actionSchemaService.get(actionName);
26360
- }
26361
-
26362
- const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
26363
- const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
26364
- const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
26365
- const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
26366
- const GET_DATE_METHOD_NAME = "exchange.getDate";
26367
- const GET_MODE_METHOD_NAME = "exchange.getMode";
26368
- const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
26369
- const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
26370
- const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
26371
- const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
26372
- const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
26373
- const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
26186
+ const WRITE_CANDLES_CACHE_FN = trycatch(queued(async (candles, dto, exchangeName) => {
26187
+ await PersistCandleAdapter.writeCandlesData(candles, dto.symbol, dto.interval, exchangeName);
26188
+ bt.loggerService.debug(`ExchangeInstance WRITE_CANDLES_CACHE_FN: cache updated for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, count=${candles.length}`);
26189
+ }), {
26190
+ fallback: async (error) => {
26191
+ const message = `ExchangeInstance WRITE_CANDLES_CACHE_FN: cache write failed`;
26192
+ const payload = {
26193
+ error: errorData(error),
26194
+ message: getErrorMessage(error),
26195
+ };
26196
+ bt.loggerService.warn(message, payload);
26197
+ console.warn(message, payload);
26198
+ errorEmitter.next(error);
26199
+ },
26200
+ defaultValue: null,
26201
+ });
26374
26202
  /**
26375
- * Checks if trade context is active (execution and method contexts).
26376
- *
26377
- * Returns true when both contexts are active, which is required for calling
26378
- * exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
26379
- * getDate, and getMode.
26203
+ * Instance class for exchange operations on a specific exchange.
26380
26204
  *
26381
- * @returns true if trade context is active, false otherwise
26205
+ * Provides isolated exchange operations for a single exchange.
26206
+ * Each instance maintains its own context and exposes IExchangeSchema methods.
26207
+ * The schema is retrieved once during construction for better performance.
26382
26208
  *
26383
26209
  * @example
26384
26210
  * ```typescript
26385
- * import { hasTradeContext, getCandles } from "backtest-kit";
26386
- *
26387
- * if (hasTradeContext()) {
26388
- * const candles = await getCandles("BTCUSDT", "1m", 100);
26389
- * } else {
26390
- * console.log("Trade context not active");
26391
- * }
26392
- * ```
26393
- */
26394
- function hasTradeContext() {
26395
- bt.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
26396
- return ExecutionContextService.hasContext() && MethodContextService.hasContext();
26397
- }
26398
- /**
26399
- * Fetches historical candle data from the registered exchange.
26400
- *
26401
- * Candles are fetched backwards from the current execution context time.
26402
- * Uses the exchange's getCandles implementation.
26403
- *
26404
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
26405
- * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
26406
- * @param limit - Number of candles to fetch
26407
- * @returns Promise resolving to array of candle data
26211
+ * const instance = new ExchangeInstance("binance");
26408
26212
  *
26409
- * @example
26410
- * ```typescript
26411
- * const candles = await getCandles("BTCUSDT", "1m", 100);
26412
- * console.log(candles[0]); // { timestamp, open, high, low, close, volume }
26213
+ * const candles = await instance.getCandles("BTCUSDT", "1m", 100);
26214
+ * const vwap = await instance.getAveragePrice("BTCUSDT");
26215
+ * const formattedQty = await instance.formatQuantity("BTCUSDT", 0.001);
26216
+ * const formattedPrice = await instance.formatPrice("BTCUSDT", 50000.123);
26413
26217
  * ```
26414
26218
  */
26415
- async function getCandles(symbol, interval, limit) {
26416
- bt.loggerService.info(GET_CANDLES_METHOD_NAME, {
26417
- symbol,
26418
- interval,
26219
+ class ExchangeInstance {
26220
+ /**
26221
+ * Creates a new ExchangeInstance for a specific exchange.
26222
+ *
26223
+ * @param exchangeName - Exchange name (e.g., "binance")
26224
+ */
26225
+ constructor(exchangeName) {
26226
+ this.exchangeName = exchangeName;
26227
+ /**
26228
+ * Fetch candles from data source (API or database).
26229
+ *
26230
+ * Automatically calculates the start date based on Date.now() and the requested interval/limit.
26231
+ * Uses the same logic as ClientExchange to ensure backwards compatibility.
26232
+ *
26233
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
26234
+ * @param interval - Candle time interval (e.g., "1m", "1h")
26235
+ * @param limit - Maximum number of candles to fetch
26236
+ * @returns Promise resolving to array of OHLCV candle data
26237
+ *
26238
+ * @example
26239
+ * ```typescript
26240
+ * const instance = new ExchangeInstance("binance");
26241
+ * const candles = await instance.getCandles("BTCUSDT", "1m", 100);
26242
+ * ```
26243
+ */
26244
+ this.getCandles = async (symbol, interval, limit) => {
26245
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_CANDLES, {
26246
+ exchangeName: this.exchangeName,
26247
+ symbol,
26248
+ interval,
26249
+ limit,
26250
+ });
26251
+ const getCandles = this._methods.getCandles;
26252
+ const step = INTERVAL_MINUTES$2[interval];
26253
+ if (!step) {
26254
+ throw new Error(`ExchangeInstance unknown interval=${interval}`);
26255
+ }
26256
+ const stepMs = step * MS_PER_MINUTE$2;
26257
+ // Align when down to interval boundary
26258
+ const when = await GET_TIMESTAMP_FN();
26259
+ const whenTimestamp = when.getTime();
26260
+ const alignedWhen = ALIGN_TO_INTERVAL_FN$1(whenTimestamp, step);
26261
+ // Calculate since: go back limit candles from aligned when
26262
+ const sinceTimestamp = alignedWhen - limit * stepMs;
26263
+ const since = new Date(sinceTimestamp);
26264
+ const untilTimestamp = alignedWhen;
26265
+ // Try to read from cache first
26266
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26267
+ if (cachedCandles !== null) {
26268
+ return cachedCandles;
26269
+ }
26270
+ let allData = [];
26271
+ // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
26272
+ if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26273
+ let remaining = limit;
26274
+ let currentSince = new Date(since.getTime());
26275
+ const isBacktest = await GET_BACKTEST_FN();
26276
+ while (remaining > 0) {
26277
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26278
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26279
+ allData.push(...chunkData);
26280
+ remaining -= chunkLimit;
26281
+ if (remaining > 0) {
26282
+ // Move currentSince forward by the number of candles fetched
26283
+ currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
26284
+ }
26285
+ }
26286
+ }
26287
+ else {
26288
+ const isBacktest = await GET_BACKTEST_FN();
26289
+ allData = await getCandles(symbol, interval, since, limit, isBacktest);
26290
+ }
26291
+ // Apply distinct by timestamp to remove duplicates
26292
+ const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26293
+ if (allData.length !== uniqueData.length) {
26294
+ bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26295
+ }
26296
+ // Validate adapter returned data
26297
+ if (uniqueData.length === 0) {
26298
+ throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
26299
+ `Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
26300
+ }
26301
+ if (uniqueData[0].timestamp !== sinceTimestamp) {
26302
+ throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
26303
+ `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26304
+ `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26305
+ }
26306
+ if (uniqueData.length !== limit) {
26307
+ throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
26308
+ `Expected ${limit} candles, got ${uniqueData.length}. ` +
26309
+ `Adapter must return exact number of candles requested.`);
26310
+ }
26311
+ // Write to cache after successful fetch
26312
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
26313
+ return uniqueData;
26314
+ };
26315
+ /**
26316
+ * Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
26317
+ * The number of candles is configurable via GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT.
26318
+ *
26319
+ * Formula:
26320
+ * - Typical Price = (high + low + close) / 3
26321
+ * - VWAP = sum(typical_price * volume) / sum(volume)
26322
+ *
26323
+ * If volume is zero, returns simple average of close prices.
26324
+ *
26325
+ * @param symbol - Trading pair symbol
26326
+ * @returns Promise resolving to VWAP price
26327
+ * @throws Error if no candles available
26328
+ *
26329
+ * @example
26330
+ * ```typescript
26331
+ * const instance = new ExchangeInstance("binance");
26332
+ * const vwap = await instance.getAveragePrice("BTCUSDT");
26333
+ * console.log(vwap); // 50125.43
26334
+ * ```
26335
+ */
26336
+ this.getAveragePrice = async (symbol) => {
26337
+ bt.loggerService.debug(`ExchangeInstance getAveragePrice`, {
26338
+ exchangeName: this.exchangeName,
26339
+ symbol,
26340
+ });
26341
+ const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
26342
+ if (candles.length === 0) {
26343
+ throw new Error(`ExchangeInstance getAveragePrice: no candles data for symbol=${symbol}`);
26344
+ }
26345
+ // VWAP (Volume Weighted Average Price)
26346
+ // Используем типичную цену (typical price) = (high + low + close) / 3
26347
+ const sumPriceVolume = candles.reduce((acc, candle) => {
26348
+ const typicalPrice = (candle.high + candle.low + candle.close) / 3;
26349
+ return acc + typicalPrice * candle.volume;
26350
+ }, 0);
26351
+ const totalVolume = candles.reduce((acc, candle) => acc + candle.volume, 0);
26352
+ if (totalVolume === 0) {
26353
+ // Если объем нулевой, возвращаем простое среднее close цен
26354
+ const sum = candles.reduce((acc, candle) => acc + candle.close, 0);
26355
+ return sum / candles.length;
26356
+ }
26357
+ const vwap = sumPriceVolume / totalVolume;
26358
+ return vwap;
26359
+ };
26360
+ /**
26361
+ * Format quantity according to exchange precision rules.
26362
+ *
26363
+ * @param symbol - Trading pair symbol
26364
+ * @param quantity - Raw quantity value
26365
+ * @returns Promise resolving to formatted quantity string
26366
+ *
26367
+ * @example
26368
+ * ```typescript
26369
+ * const instance = new ExchangeInstance("binance");
26370
+ * const formatted = await instance.formatQuantity("BTCUSDT", 0.001);
26371
+ * console.log(formatted); // "0.00100000"
26372
+ * ```
26373
+ */
26374
+ this.formatQuantity = async (symbol, quantity) => {
26375
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_FORMAT_QUANTITY, {
26376
+ exchangeName: this.exchangeName,
26377
+ symbol,
26378
+ quantity,
26379
+ });
26380
+ const isBacktest = await GET_BACKTEST_FN();
26381
+ return await this._methods.formatQuantity(symbol, quantity, isBacktest);
26382
+ };
26383
+ /**
26384
+ * Format price according to exchange precision rules.
26385
+ *
26386
+ * @param symbol - Trading pair symbol
26387
+ * @param price - Raw price value
26388
+ * @returns Promise resolving to formatted price string
26389
+ *
26390
+ * @example
26391
+ * ```typescript
26392
+ * const instance = new ExchangeInstance("binance");
26393
+ * const formatted = await instance.formatPrice("BTCUSDT", 50000.123);
26394
+ * console.log(formatted); // "50000.12"
26395
+ * ```
26396
+ */
26397
+ this.formatPrice = async (symbol, price) => {
26398
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_FORMAT_PRICE, {
26399
+ exchangeName: this.exchangeName,
26400
+ symbol,
26401
+ price,
26402
+ });
26403
+ const isBacktest = await GET_BACKTEST_FN();
26404
+ return await this._methods.formatPrice(symbol, price, isBacktest);
26405
+ };
26406
+ /**
26407
+ * Fetch order book for a trading pair.
26408
+ *
26409
+ * Calculates time range using CC_ORDER_BOOK_TIME_OFFSET_MINUTES (default 10 minutes)
26410
+ * and passes it to the exchange schema implementation. The implementation may use
26411
+ * the time range (backtest) or ignore it (live trading).
26412
+ *
26413
+ * @param symbol - Trading pair symbol
26414
+ * @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
26415
+ * @returns Promise resolving to order book data
26416
+ * @throws Error if getOrderBook is not implemented
26417
+ *
26418
+ * @example
26419
+ * ```typescript
26420
+ * const instance = new ExchangeInstance("binance");
26421
+ * const orderBook = await instance.getOrderBook("BTCUSDT");
26422
+ * console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
26423
+ * const deepOrderBook = await instance.getOrderBook("BTCUSDT", 100);
26424
+ * ```
26425
+ */
26426
+ this.getOrderBook = async (symbol, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) => {
26427
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_ORDER_BOOK, {
26428
+ exchangeName: this.exchangeName,
26429
+ symbol,
26430
+ depth,
26431
+ });
26432
+ const when = await GET_TIMESTAMP_FN();
26433
+ const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
26434
+ const to = new Date(alignedTo);
26435
+ const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$2);
26436
+ const isBacktest = await GET_BACKTEST_FN();
26437
+ return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
26438
+ };
26439
+ /**
26440
+ * Fetches raw candles with flexible date/limit parameters.
26441
+ *
26442
+ * Uses Date.now() instead of execution context when for look-ahead bias protection.
26443
+ *
26444
+ * Parameter combinations:
26445
+ * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= now
26446
+ * 2. sDate + eDate: calculates limit from date range, validates eDate <= now
26447
+ * 3. eDate + limit: calculates sDate backward, validates eDate <= now
26448
+ * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= now
26449
+ * 5. Only limit: uses Date.now() as reference (backward)
26450
+ *
26451
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
26452
+ * @param interval - Candle interval (e.g., "1m", "1h")
26453
+ * @param limit - Optional number of candles to fetch
26454
+ * @param sDate - Optional start date in milliseconds
26455
+ * @param eDate - Optional end date in milliseconds
26456
+ * @returns Promise resolving to array of candle data
26457
+ *
26458
+ * @example
26459
+ * ```typescript
26460
+ * const instance = new ExchangeInstance("binance");
26461
+ *
26462
+ * // Fetch 100 candles backward from now
26463
+ * const candles = await instance.getRawCandles("BTCUSDT", "1m", 100);
26464
+ *
26465
+ * // Fetch candles for specific date range
26466
+ * const rangeCandles = await instance.getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
26467
+ * ```
26468
+ */
26469
+ this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
26470
+ bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_RAW_CANDLES, {
26471
+ exchangeName: this.exchangeName,
26472
+ symbol,
26473
+ interval,
26474
+ limit,
26475
+ sDate,
26476
+ eDate,
26477
+ });
26478
+ const step = INTERVAL_MINUTES$2[interval];
26479
+ if (!step) {
26480
+ throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
26481
+ }
26482
+ const stepMs = step * MS_PER_MINUTE$2;
26483
+ const when = await GET_TIMESTAMP_FN();
26484
+ const nowTimestamp = when.getTime();
26485
+ const alignedNow = ALIGN_TO_INTERVAL_FN$1(nowTimestamp, step);
26486
+ let sinceTimestamp;
26487
+ let calculatedLimit;
26488
+ // Case 1: all three parameters provided
26489
+ if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
26490
+ if (sDate >= eDate) {
26491
+ throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
26492
+ }
26493
+ if (eDate > nowTimestamp) {
26494
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
26495
+ }
26496
+ // Align sDate down to interval boundary
26497
+ sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
26498
+ calculatedLimit = limit;
26499
+ }
26500
+ // Case 2: sDate + eDate (no limit) - calculate limit from date range
26501
+ else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
26502
+ if (sDate >= eDate) {
26503
+ throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
26504
+ }
26505
+ if (eDate > nowTimestamp) {
26506
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
26507
+ }
26508
+ // Align sDate down to interval boundary
26509
+ sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
26510
+ const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
26511
+ calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
26512
+ if (calculatedLimit <= 0) {
26513
+ throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
26514
+ }
26515
+ }
26516
+ // Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
26517
+ else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
26518
+ if (eDate > nowTimestamp) {
26519
+ throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
26520
+ }
26521
+ // Align eDate down and calculate sinceTimestamp
26522
+ const alignedEDate = ALIGN_TO_INTERVAL_FN$1(eDate, step);
26523
+ sinceTimestamp = alignedEDate - limit * stepMs;
26524
+ calculatedLimit = limit;
26525
+ }
26526
+ // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
26527
+ else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
26528
+ // Align sDate down to interval boundary
26529
+ sinceTimestamp = ALIGN_TO_INTERVAL_FN$1(sDate, step);
26530
+ const endTimestamp = sinceTimestamp + limit * stepMs;
26531
+ if (endTimestamp > nowTimestamp) {
26532
+ throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
26533
+ }
26534
+ calculatedLimit = limit;
26535
+ }
26536
+ // Case 5: Only limit - use Date.now() as reference (backward)
26537
+ else if (sDate === undefined && eDate === undefined && limit !== undefined) {
26538
+ sinceTimestamp = alignedNow - limit * stepMs;
26539
+ calculatedLimit = limit;
26540
+ }
26541
+ // Invalid: no parameters or only sDate or only eDate
26542
+ else {
26543
+ throw new Error(`ExchangeInstance getRawCandles: invalid parameter combination. ` +
26544
+ `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
26545
+ `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
26546
+ }
26547
+ // Try to read from cache first
26548
+ const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
26549
+ const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
26550
+ if (cachedCandles !== null) {
26551
+ return cachedCandles;
26552
+ }
26553
+ // Fetch candles
26554
+ const since = new Date(sinceTimestamp);
26555
+ let allData = [];
26556
+ const isBacktest = await GET_BACKTEST_FN();
26557
+ const getCandles = this._methods.getCandles;
26558
+ if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
26559
+ let remaining = calculatedLimit;
26560
+ let currentSince = new Date(since.getTime());
26561
+ while (remaining > 0) {
26562
+ const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26563
+ const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
26564
+ allData.push(...chunkData);
26565
+ remaining -= chunkLimit;
26566
+ if (remaining > 0) {
26567
+ currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
26568
+ }
26569
+ }
26570
+ }
26571
+ else {
26572
+ allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
26573
+ }
26574
+ // Apply distinct by timestamp to remove duplicates
26575
+ const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
26576
+ if (allData.length !== uniqueData.length) {
26577
+ bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
26578
+ }
26579
+ // Validate adapter returned data
26580
+ if (uniqueData.length === 0) {
26581
+ throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
26582
+ `Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
26583
+ }
26584
+ if (uniqueData[0].timestamp !== sinceTimestamp) {
26585
+ throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
26586
+ `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
26587
+ `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
26588
+ }
26589
+ if (uniqueData.length !== calculatedLimit) {
26590
+ throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
26591
+ `Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
26592
+ `Adapter must return exact number of candles requested.`);
26593
+ }
26594
+ // Write to cache after successful fetch
26595
+ await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
26596
+ return uniqueData;
26597
+ };
26598
+ const schema = bt.exchangeSchemaService.get(this.exchangeName);
26599
+ this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
26600
+ }
26601
+ }
26602
+ /**
26603
+ * Utility class for exchange operations.
26604
+ *
26605
+ * Provides simplified access to exchange schema methods with validation.
26606
+ * Exported as singleton instance for convenient usage.
26607
+ *
26608
+ * @example
26609
+ * ```typescript
26610
+ * import { Exchange } from "./classes/Exchange";
26611
+ *
26612
+ * const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
26613
+ * exchangeName: "binance"
26614
+ * });
26615
+ * const vwap = await Exchange.getAveragePrice("BTCUSDT", {
26616
+ * exchangeName: "binance"
26617
+ * });
26618
+ * const formatted = await Exchange.formatQuantity("BTCUSDT", 0.001, {
26619
+ * exchangeName: "binance"
26620
+ * });
26621
+ * ```
26622
+ */
26623
+ class ExchangeUtils {
26624
+ constructor() {
26625
+ /**
26626
+ * Memoized function to get or create ExchangeInstance for an exchange.
26627
+ * Each exchange gets its own isolated instance.
26628
+ */
26629
+ this._getInstance = memoize(([exchangeName]) => exchangeName, (exchangeName) => new ExchangeInstance(exchangeName));
26630
+ /**
26631
+ * Fetch candles from data source (API or database).
26632
+ *
26633
+ * Automatically calculates the start date based on Date.now() and the requested interval/limit.
26634
+ * Uses the same logic as ClientExchange to ensure backwards compatibility.
26635
+ *
26636
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
26637
+ * @param interval - Candle time interval (e.g., "1m", "1h")
26638
+ * @param limit - Maximum number of candles to fetch
26639
+ * @param context - Execution context with exchange name
26640
+ * @returns Promise resolving to array of OHLCV candle data
26641
+ */
26642
+ this.getCandles = async (symbol, interval, limit, context) => {
26643
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_CANDLES);
26644
+ const instance = this._getInstance(context.exchangeName);
26645
+ return await instance.getCandles(symbol, interval, limit);
26646
+ };
26647
+ /**
26648
+ * Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
26649
+ *
26650
+ * @param symbol - Trading pair symbol
26651
+ * @param context - Execution context with exchange name
26652
+ * @returns Promise resolving to VWAP price
26653
+ */
26654
+ this.getAveragePrice = async (symbol, context) => {
26655
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE);
26656
+ const instance = this._getInstance(context.exchangeName);
26657
+ return await instance.getAveragePrice(symbol);
26658
+ };
26659
+ /**
26660
+ * Format quantity according to exchange precision rules.
26661
+ *
26662
+ * @param symbol - Trading pair symbol
26663
+ * @param quantity - Raw quantity value
26664
+ * @param context - Execution context with exchange name
26665
+ * @returns Promise resolving to formatted quantity string
26666
+ */
26667
+ this.formatQuantity = async (symbol, quantity, context) => {
26668
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_FORMAT_QUANTITY);
26669
+ const instance = this._getInstance(context.exchangeName);
26670
+ return await instance.formatQuantity(symbol, quantity);
26671
+ };
26672
+ /**
26673
+ * Format price according to exchange precision rules.
26674
+ *
26675
+ * @param symbol - Trading pair symbol
26676
+ * @param price - Raw price value
26677
+ * @param context - Execution context with exchange name
26678
+ * @returns Promise resolving to formatted price string
26679
+ */
26680
+ this.formatPrice = async (symbol, price, context) => {
26681
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_FORMAT_PRICE);
26682
+ const instance = this._getInstance(context.exchangeName);
26683
+ return await instance.formatPrice(symbol, price);
26684
+ };
26685
+ /**
26686
+ * Fetch order book for a trading pair.
26687
+ *
26688
+ * Delegates to ExchangeInstance which calculates time range and passes it
26689
+ * to the exchange schema implementation. The from/to parameters may be used
26690
+ * (backtest) or ignored (live) depending on the implementation.
26691
+ *
26692
+ * @param symbol - Trading pair symbol
26693
+ * @param context - Execution context with exchange name
26694
+ * @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
26695
+ * @returns Promise resolving to order book data
26696
+ */
26697
+ this.getOrderBook = async (symbol, context, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) => {
26698
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_ORDER_BOOK);
26699
+ const instance = this._getInstance(context.exchangeName);
26700
+ return await instance.getOrderBook(symbol, depth);
26701
+ };
26702
+ /**
26703
+ * Fetches raw candles with flexible date/limit parameters.
26704
+ *
26705
+ * Uses Date.now() instead of execution context when for look-ahead bias protection.
26706
+ *
26707
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
26708
+ * @param interval - Candle interval (e.g., "1m", "1h")
26709
+ * @param context - Execution context with exchange name
26710
+ * @param limit - Optional number of candles to fetch
26711
+ * @param sDate - Optional start date in milliseconds
26712
+ * @param eDate - Optional end date in milliseconds
26713
+ * @returns Promise resolving to array of candle data
26714
+ */
26715
+ this.getRawCandles = async (symbol, interval, context, limit, sDate, eDate) => {
26716
+ bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_RAW_CANDLES);
26717
+ const instance = this._getInstance(context.exchangeName);
26718
+ return await instance.getRawCandles(symbol, interval, limit, sDate, eDate);
26719
+ };
26720
+ }
26721
+ }
26722
+ /**
26723
+ * Singleton instance of ExchangeUtils for convenient exchange operations.
26724
+ *
26725
+ * @example
26726
+ * ```typescript
26727
+ * import { Exchange } from "./classes/Exchange";
26728
+ *
26729
+ * // Using static-like API with context
26730
+ * const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
26731
+ * exchangeName: "binance"
26732
+ * });
26733
+ * const vwap = await Exchange.getAveragePrice("BTCUSDT", {
26734
+ * exchangeName: "binance"
26735
+ * });
26736
+ * const qty = await Exchange.formatQuantity("BTCUSDT", 0.001, {
26737
+ * exchangeName: "binance"
26738
+ * });
26739
+ * const price = await Exchange.formatPrice("BTCUSDT", 50000.123, {
26740
+ * exchangeName: "binance"
26741
+ * });
26742
+ *
26743
+ * // Using instance API (no context needed, exchange set in constructor)
26744
+ * const binance = new ExchangeInstance("binance");
26745
+ * const candles2 = await binance.getCandles("BTCUSDT", "1m", 100);
26746
+ * const vwap2 = await binance.getAveragePrice("BTCUSDT");
26747
+ * ```
26748
+ */
26749
+ const Exchange = new ExchangeUtils();
26750
+
26751
+ const WARM_CANDLES_METHOD_NAME = "cache.warmCandles";
26752
+ const CHECK_CANDLES_METHOD_NAME = "cache.checkCandles";
26753
+ const MS_PER_MINUTE$1 = 60000;
26754
+ const INTERVAL_MINUTES$1 = {
26755
+ "1m": 1,
26756
+ "3m": 3,
26757
+ "5m": 5,
26758
+ "15m": 15,
26759
+ "30m": 30,
26760
+ "1h": 60,
26761
+ "2h": 120,
26762
+ "4h": 240,
26763
+ "6h": 360,
26764
+ "8h": 480,
26765
+ };
26766
+ const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
26767
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
26768
+ return Math.floor(timestamp / intervalMs) * intervalMs;
26769
+ };
26770
+ const BAR_LENGTH = 30;
26771
+ const BAR_FILLED_CHAR = "\u2588";
26772
+ const BAR_EMPTY_CHAR = "\u2591";
26773
+ const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
26774
+ const percent = Math.round((fetched / total) * 100);
26775
+ const filled = Math.round((fetched / total) * BAR_LENGTH);
26776
+ const empty = BAR_LENGTH - filled;
26777
+ const bar = BAR_FILLED_CHAR.repeat(filled) + BAR_EMPTY_CHAR.repeat(empty);
26778
+ process.stdout.write(`\r[${bar}] ${percent}% (${fetched}/${total}) ${symbol} ${interval}`);
26779
+ if (fetched === total) {
26780
+ process.stdout.write("\n");
26781
+ }
26782
+ };
26783
+ /**
26784
+ * Checks cached candle timestamps for correct interval alignment.
26785
+ * Reads JSON files directly from persist storage without using abstractions.
26786
+ *
26787
+ * @param params - Validation parameters
26788
+ */
26789
+ async function checkCandles(params) {
26790
+ const { symbol, exchangeName, interval, baseDir = "./dump/data/candle" } = params;
26791
+ bt.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
26792
+ const step = INTERVAL_MINUTES$1[interval];
26793
+ if (!step) {
26794
+ throw new Error(`checkCandles: unsupported interval=${interval}`);
26795
+ }
26796
+ const stepMs = step * MS_PER_MINUTE$1;
26797
+ const dir = join(baseDir, exchangeName, symbol, interval);
26798
+ try {
26799
+ await stat(dir);
26800
+ }
26801
+ catch {
26802
+ throw new Error(`checkCandles: cache directory not found: ${dir}`);
26803
+ }
26804
+ // First pass: count files via async iterator (no memory allocation for file list)
26805
+ let totalFiles = 0;
26806
+ for await (const entry of await opendir(dir)) {
26807
+ if (entry.isFile() && entry.name.endsWith(".json")) {
26808
+ totalFiles++;
26809
+ }
26810
+ }
26811
+ if (totalFiles === 0) {
26812
+ throw new Error(`checkCandles: no cached candles in ${dir}`);
26813
+ }
26814
+ // Second pass: check each file via async iterator with progress
26815
+ let checkd = 0;
26816
+ let prevTimestamp = null;
26817
+ let prevName = null;
26818
+ PRINT_PROGRESS_FN(checkd, totalFiles, symbol, interval);
26819
+ for await (const entry of await opendir(dir)) {
26820
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
26821
+ continue;
26822
+ }
26823
+ const filePath = join(dir, entry.name);
26824
+ const raw = await readFile(filePath, "utf-8");
26825
+ const candle = JSON.parse(raw);
26826
+ const { timestamp } = candle;
26827
+ const aligned = ALIGN_TO_INTERVAL_FN(timestamp, step);
26828
+ if (timestamp !== aligned) {
26829
+ throw new Error(`checkCandles: ${entry.name} timestamp=${timestamp} is not aligned to ${interval} boundary (expected=${aligned})`);
26830
+ }
26831
+ if (prevTimestamp !== null) {
26832
+ const gap = timestamp - prevTimestamp;
26833
+ if (gap !== stepMs) {
26834
+ throw new Error(`checkCandles: gap between ${prevName} and ${entry.name} is ${gap}ms, expected ${stepMs}ms`);
26835
+ }
26836
+ }
26837
+ prevTimestamp = timestamp;
26838
+ prevName = entry.name;
26839
+ checkd++;
26840
+ PRINT_PROGRESS_FN(checkd, totalFiles, symbol, interval);
26841
+ }
26842
+ console.log(`checkCandles: OK ${totalFiles} candles ${symbol} ${interval}`);
26843
+ }
26844
+ /**
26845
+ * Pre-caches candles for a date range into persist storage.
26846
+ * Downloads all candles matching the interval from `from` to `to`.
26847
+ *
26848
+ * @param params - Cache parameters
26849
+ */
26850
+ async function warmCandles(params) {
26851
+ const { symbol, exchangeName, interval, from, to } = params;
26852
+ bt.loggerService.info(WARM_CANDLES_METHOD_NAME, {
26853
+ symbol,
26854
+ exchangeName,
26855
+ interval,
26856
+ from,
26857
+ to,
26858
+ });
26859
+ const step = INTERVAL_MINUTES$1[interval];
26860
+ if (!step) {
26861
+ throw new Error(`warmCandles: unsupported interval=${interval}`);
26862
+ }
26863
+ const stepMs = step * MS_PER_MINUTE$1;
26864
+ const instance = new ExchangeInstance(exchangeName);
26865
+ const sinceTimestamp = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
26866
+ const untilTimestamp = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
26867
+ const totalCandles = Math.ceil((untilTimestamp - sinceTimestamp) / stepMs);
26868
+ if (totalCandles <= 0) {
26869
+ throw new Error(`warmCandles: no candles to cache (from >= to after alignment)`);
26870
+ }
26871
+ let fetched = 0;
26872
+ let currentSince = sinceTimestamp;
26873
+ PRINT_PROGRESS_FN(fetched, totalCandles, symbol, interval);
26874
+ while (fetched < totalCandles) {
26875
+ const chunkLimit = Math.min(totalCandles - fetched, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
26876
+ await instance.getRawCandles(symbol, interval, chunkLimit, currentSince);
26877
+ fetched += chunkLimit;
26878
+ currentSince += chunkLimit * stepMs;
26879
+ PRINT_PROGRESS_FN(fetched, totalCandles, symbol, interval);
26880
+ }
26881
+ }
26882
+
26883
+ const METHOD_NAME = "validate.validate";
26884
+ /**
26885
+ * Retrieves all registered exchanges as a map
26886
+ * @private
26887
+ * @returns Map of exchange names
26888
+ */
26889
+ const getExchangeMap = async () => {
26890
+ const exchangeMap = {};
26891
+ for (const { exchangeName } of await bt.exchangeValidationService.list()) {
26892
+ Object.assign(exchangeMap, { [exchangeName]: exchangeName });
26893
+ }
26894
+ return exchangeMap;
26895
+ };
26896
+ /**
26897
+ * Retrieves all registered frames as a map
26898
+ * @private
26899
+ * @returns Map of frame names
26900
+ */
26901
+ const getFrameMap = async () => {
26902
+ const frameMap = {};
26903
+ for (const { frameName } of await bt.frameValidationService.list()) {
26904
+ Object.assign(frameMap, { [frameName]: frameName });
26905
+ }
26906
+ return frameMap;
26907
+ };
26908
+ /**
26909
+ * Retrieves all registered strategies as a map
26910
+ * @private
26911
+ * @returns Map of strategy names
26912
+ */
26913
+ const getStrategyMap = async () => {
26914
+ const strategyMap = {};
26915
+ for (const { strategyName } of await bt.strategyValidationService.list()) {
26916
+ Object.assign(strategyMap, { [strategyName]: strategyName });
26917
+ }
26918
+ return strategyMap;
26919
+ };
26920
+ /**
26921
+ * Retrieves all registered risk profiles as a map
26922
+ * @private
26923
+ * @returns Map of risk names
26924
+ */
26925
+ const getRiskMap = async () => {
26926
+ const riskMap = {};
26927
+ for (const { riskName } of await bt.riskValidationService.list()) {
26928
+ Object.assign(riskMap, { [riskName]: riskName });
26929
+ }
26930
+ return riskMap;
26931
+ };
26932
+ /**
26933
+ * Retrieves all registered action handlers as a map
26934
+ * @private
26935
+ * @returns Map of action names
26936
+ */
26937
+ const getActionMap = async () => {
26938
+ const actionMap = {};
26939
+ for (const { actionName } of await bt.actionValidationService.list()) {
26940
+ Object.assign(actionMap, { [actionName]: actionName });
26941
+ }
26942
+ return actionMap;
26943
+ };
26944
+ /**
26945
+ * Retrieves all registered sizing strategies as a map
26946
+ * @private
26947
+ * @returns Map of sizing names
26948
+ */
26949
+ const getSizingMap = async () => {
26950
+ const sizingMap = {};
26951
+ for (const { sizingName } of await bt.sizingValidationService.list()) {
26952
+ Object.assign(sizingMap, { [sizingName]: sizingName });
26953
+ }
26954
+ return sizingMap;
26955
+ };
26956
+ /**
26957
+ * Retrieves all registered walkers as a map
26958
+ * @private
26959
+ * @returns Map of walker names
26960
+ */
26961
+ const getWalkerMap = async () => {
26962
+ const walkerMap = {};
26963
+ for (const { walkerName } of await bt.walkerValidationService.list()) {
26964
+ Object.assign(walkerMap, { [walkerName]: walkerName });
26965
+ }
26966
+ return walkerMap;
26967
+ };
26968
+ /**
26969
+ * Internal validation function that processes all provided entity enums.
26970
+ *
26971
+ * Iterates through each enum's values and validates them against their
26972
+ * respective validation services. Uses memoized validation for performance.
26973
+ *
26974
+ * If entity enums are not provided, fetches all registered entities from
26975
+ * their respective validation services and validates them.
26976
+ *
26977
+ * @private
26978
+ * @param args - Validation arguments containing entity name enums
26979
+ * @throws {Error} If any entity name is not found in its registry
26980
+ */
26981
+ const validateInternal = async (args) => {
26982
+ const { ExchangeName = await getExchangeMap(), FrameName = await getFrameMap(), StrategyName = await getStrategyMap(), RiskName = await getRiskMap(), ActionName = await getActionMap(), SizingName = await getSizingMap(), WalkerName = await getWalkerMap(), } = args;
26983
+ for (const exchangeName of Object.values(ExchangeName)) {
26984
+ bt.exchangeValidationService.validate(exchangeName, METHOD_NAME);
26985
+ }
26986
+ for (const frameName of Object.values(FrameName)) {
26987
+ bt.frameValidationService.validate(frameName, METHOD_NAME);
26988
+ }
26989
+ for (const strategyName of Object.values(StrategyName)) {
26990
+ bt.strategyValidationService.validate(strategyName, METHOD_NAME);
26991
+ }
26992
+ for (const riskName of Object.values(RiskName)) {
26993
+ bt.riskValidationService.validate(riskName, METHOD_NAME);
26994
+ }
26995
+ for (const actionName of Object.values(ActionName)) {
26996
+ bt.actionValidationService.validate(actionName, METHOD_NAME);
26997
+ }
26998
+ for (const sizingName of Object.values(SizingName)) {
26999
+ bt.sizingValidationService.validate(sizingName, METHOD_NAME);
27000
+ }
27001
+ for (const walkerName of Object.values(WalkerName)) {
27002
+ bt.walkerValidationService.validate(walkerName, METHOD_NAME);
27003
+ }
27004
+ };
27005
+ /**
27006
+ * Validates the existence of all provided entity names across validation services.
27007
+ *
27008
+ * This function accepts enum objects for various entity types (exchanges, frames,
27009
+ * strategies, risks, sizings, walkers) and validates that each entity
27010
+ * name exists in its respective registry. Validation results are memoized for performance.
27011
+ *
27012
+ * If no arguments are provided (or specific entity types are omitted), the function
27013
+ * automatically fetches and validates ALL registered entities from their respective
27014
+ * validation services. This is useful for comprehensive validation of the entire setup.
27015
+ *
27016
+ * Use this before running backtests or optimizations to ensure all referenced
27017
+ * entities are properly registered and configured.
27018
+ *
27019
+ * @public
27020
+ * @param args - Partial validation arguments containing entity name enums to validate.
27021
+ * If empty or omitted, validates all registered entities.
27022
+ * @throws {Error} If any entity name is not found in its validation service
27023
+ *
27024
+ * @example
27025
+ * ```typescript
27026
+ * // Validate ALL registered entities (exchanges, frames, strategies, etc.)
27027
+ * await validate({});
27028
+ * ```
27029
+ *
27030
+ * @example
27031
+ * ```typescript
27032
+ * // Define your entity name enums
27033
+ * enum ExchangeName {
27034
+ * BINANCE = "binance",
27035
+ * BYBIT = "bybit"
27036
+ * }
27037
+ *
27038
+ * enum StrategyName {
27039
+ * MOMENTUM_BTC = "momentum-btc"
27040
+ * }
27041
+ *
27042
+ * // Validate specific entities before running backtest
27043
+ * await validate({
27044
+ * ExchangeName,
27045
+ * StrategyName,
27046
+ * });
27047
+ * ```
27048
+ *
27049
+ * @example
27050
+ * ```typescript
27051
+ * // Validate specific entity types
27052
+ * await validate({
27053
+ * RiskName: { CONSERVATIVE: "conservative" },
27054
+ * SizingName: { FIXED_1000: "fixed-1000" },
27055
+ * });
27056
+ * ```
27057
+ */
27058
+ async function validate(args = {}) {
27059
+ bt.loggerService.log(METHOD_NAME);
27060
+ return await validateInternal(args);
27061
+ }
27062
+
27063
+ const GET_STRATEGY_METHOD_NAME = "get.getStrategySchema";
27064
+ const GET_EXCHANGE_METHOD_NAME = "get.getExchangeSchema";
27065
+ const GET_FRAME_METHOD_NAME = "get.getFrameSchema";
27066
+ const GET_WALKER_METHOD_NAME = "get.getWalkerSchema";
27067
+ const GET_SIZING_METHOD_NAME = "get.getSizingSchema";
27068
+ const GET_RISK_METHOD_NAME = "get.getRiskSchema";
27069
+ const GET_ACTION_METHOD_NAME = "get.getActionSchema";
27070
+ /**
27071
+ * Retrieves a registered strategy schema by name.
27072
+ *
27073
+ * @param strategyName - Unique strategy identifier
27074
+ * @returns The strategy schema configuration object
27075
+ * @throws Error if strategy is not registered
27076
+ *
27077
+ * @example
27078
+ * ```typescript
27079
+ * const strategy = getStrategy("my-strategy");
27080
+ * console.log(strategy.interval); // "5m"
27081
+ * console.log(strategy.getSignal); // async function
27082
+ * ```
27083
+ */
27084
+ function getStrategySchema(strategyName) {
27085
+ bt.loggerService.log(GET_STRATEGY_METHOD_NAME, {
27086
+ strategyName,
27087
+ });
27088
+ bt.strategyValidationService.validate(strategyName, GET_STRATEGY_METHOD_NAME);
27089
+ return bt.strategySchemaService.get(strategyName);
27090
+ }
27091
+ /**
27092
+ * Retrieves a registered exchange schema by name.
27093
+ *
27094
+ * @param exchangeName - Unique exchange identifier
27095
+ * @returns The exchange schema configuration object
27096
+ * @throws Error if exchange is not registered
27097
+ *
27098
+ * @example
27099
+ * ```typescript
27100
+ * const exchange = getExchange("binance");
27101
+ * console.log(exchange.getCandles); // async function
27102
+ * console.log(exchange.formatPrice); // async function
27103
+ * ```
27104
+ */
27105
+ function getExchangeSchema(exchangeName) {
27106
+ bt.loggerService.log(GET_EXCHANGE_METHOD_NAME, {
27107
+ exchangeName,
27108
+ });
27109
+ bt.exchangeValidationService.validate(exchangeName, GET_EXCHANGE_METHOD_NAME);
27110
+ return bt.exchangeSchemaService.get(exchangeName);
27111
+ }
27112
+ /**
27113
+ * Retrieves a registered frame schema by name.
27114
+ *
27115
+ * @param frameName - Unique frame identifier
27116
+ * @returns The frame schema configuration object
27117
+ * @throws Error if frame is not registered
27118
+ *
27119
+ * @example
27120
+ * ```typescript
27121
+ * const frame = getFrame("1d-backtest");
27122
+ * console.log(frame.interval); // "1m"
27123
+ * console.log(frame.startDate); // Date object
27124
+ * console.log(frame.endDate); // Date object
27125
+ * ```
27126
+ */
27127
+ function getFrameSchema(frameName) {
27128
+ bt.loggerService.log(GET_FRAME_METHOD_NAME, {
27129
+ frameName,
27130
+ });
27131
+ bt.frameValidationService.validate(frameName, GET_FRAME_METHOD_NAME);
27132
+ return bt.frameSchemaService.get(frameName);
27133
+ }
27134
+ /**
27135
+ * Retrieves a registered walker schema by name.
27136
+ *
27137
+ * @param walkerName - Unique walker identifier
27138
+ * @returns The walker schema configuration object
27139
+ * @throws Error if walker is not registered
27140
+ *
27141
+ * @example
27142
+ * ```typescript
27143
+ * const walker = getWalker("llm-prompt-optimizer");
27144
+ * console.log(walker.exchangeName); // "binance"
27145
+ * console.log(walker.frameName); // "1d-backtest"
27146
+ * console.log(walker.strategies); // ["my-strategy-v1", "my-strategy-v2"]
27147
+ * console.log(walker.metric); // "sharpeRatio"
27148
+ * ```
27149
+ */
27150
+ function getWalkerSchema(walkerName) {
27151
+ bt.loggerService.log(GET_WALKER_METHOD_NAME, {
27152
+ walkerName,
27153
+ });
27154
+ bt.walkerValidationService.validate(walkerName, GET_WALKER_METHOD_NAME);
27155
+ return bt.walkerSchemaService.get(walkerName);
27156
+ }
27157
+ /**
27158
+ * Retrieves a registered sizing schema by name.
27159
+ *
27160
+ * @param sizingName - Unique sizing identifier
27161
+ * @returns The sizing schema configuration object
27162
+ * @throws Error if sizing is not registered
27163
+ *
27164
+ * @example
27165
+ * ```typescript
27166
+ * const sizing = getSizing("conservative");
27167
+ * console.log(sizing.method); // "fixed-percentage"
27168
+ * console.log(sizing.riskPercentage); // 1
27169
+ * console.log(sizing.maxPositionPercentage); // 10
27170
+ * ```
27171
+ */
27172
+ function getSizingSchema(sizingName) {
27173
+ bt.loggerService.log(GET_SIZING_METHOD_NAME, {
27174
+ sizingName,
27175
+ });
27176
+ bt.sizingValidationService.validate(sizingName, GET_SIZING_METHOD_NAME);
27177
+ return bt.sizingSchemaService.get(sizingName);
27178
+ }
27179
+ /**
27180
+ * Retrieves a registered risk schema by name.
27181
+ *
27182
+ * @param riskName - Unique risk identifier
27183
+ * @returns The risk schema configuration object
27184
+ * @throws Error if risk is not registered
27185
+ *
27186
+ * @example
27187
+ * ```typescript
27188
+ * const risk = getRisk("conservative");
27189
+ * console.log(risk.maxConcurrentPositions); // 5
27190
+ * console.log(risk.validations); // Array of validation functions
27191
+ * ```
27192
+ */
27193
+ function getRiskSchema(riskName) {
27194
+ bt.loggerService.log(GET_RISK_METHOD_NAME, {
27195
+ riskName,
27196
+ });
27197
+ bt.riskValidationService.validate(riskName, GET_RISK_METHOD_NAME);
27198
+ return bt.riskSchemaService.get(riskName);
27199
+ }
27200
+ /**
27201
+ * Retrieves a registered action schema by name.
27202
+ *
27203
+ * @param actionName - Unique action identifier
27204
+ * @returns The action schema configuration object
27205
+ * @throws Error if action is not registered
27206
+ *
27207
+ * @example
27208
+ * ```typescript
27209
+ * const action = getAction("telegram-notifier");
27210
+ * console.log(action.handler); // Class constructor or object
27211
+ * console.log(action.callbacks); // Optional lifecycle callbacks
27212
+ * ```
27213
+ */
27214
+ function getActionSchema(actionName) {
27215
+ bt.loggerService.log(GET_ACTION_METHOD_NAME, {
27216
+ actionName,
27217
+ });
27218
+ bt.actionValidationService.validate(actionName, GET_ACTION_METHOD_NAME);
27219
+ return bt.actionSchemaService.get(actionName);
27220
+ }
27221
+
27222
+ const GET_CANDLES_METHOD_NAME = "exchange.getCandles";
27223
+ const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
27224
+ const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
27225
+ const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
27226
+ const GET_DATE_METHOD_NAME = "exchange.getDate";
27227
+ const GET_MODE_METHOD_NAME = "exchange.getMode";
27228
+ const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
27229
+ const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
27230
+ const HAS_TRADE_CONTEXT_METHOD_NAME = "exchange.hasTradeContext";
27231
+ const GET_ORDER_BOOK_METHOD_NAME = "exchange.getOrderBook";
27232
+ const GET_RAW_CANDLES_METHOD_NAME = "exchange.getRawCandles";
27233
+ const GET_NEXT_CANDLES_METHOD_NAME = "exchange.getNextCandles";
27234
+ /**
27235
+ * Checks if trade context is active (execution and method contexts).
27236
+ *
27237
+ * Returns true when both contexts are active, which is required for calling
27238
+ * exchange functions like getCandles, getAveragePrice, formatPrice, formatQuantity,
27239
+ * getDate, and getMode.
27240
+ *
27241
+ * @returns true if trade context is active, false otherwise
27242
+ *
27243
+ * @example
27244
+ * ```typescript
27245
+ * import { hasTradeContext, getCandles } from "backtest-kit";
27246
+ *
27247
+ * if (hasTradeContext()) {
27248
+ * const candles = await getCandles("BTCUSDT", "1m", 100);
27249
+ * } else {
27250
+ * console.log("Trade context not active");
27251
+ * }
27252
+ * ```
27253
+ */
27254
+ function hasTradeContext() {
27255
+ bt.loggerService.info(HAS_TRADE_CONTEXT_METHOD_NAME);
27256
+ return ExecutionContextService.hasContext() && MethodContextService.hasContext();
27257
+ }
27258
+ /**
27259
+ * Fetches historical candle data from the registered exchange.
27260
+ *
27261
+ * Candles are fetched backwards from the current execution context time.
27262
+ * Uses the exchange's getCandles implementation.
27263
+ *
27264
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
27265
+ * @param interval - Candle interval ("1m" | "3m" | "5m" | "15m" | "30m" | "1h" | "2h" | "4h" | "6h" | "8h")
27266
+ * @param limit - Number of candles to fetch
27267
+ * @returns Promise resolving to array of candle data
27268
+ *
27269
+ * @example
27270
+ * ```typescript
27271
+ * const candles = await getCandles("BTCUSDT", "1m", 100);
27272
+ * console.log(candles[0]); // { timestamp, open, high, low, close, volume }
27273
+ * ```
27274
+ */
27275
+ async function getCandles(symbol, interval, limit) {
27276
+ bt.loggerService.info(GET_CANDLES_METHOD_NAME, {
27277
+ symbol,
27278
+ interval,
26419
27279
  limit,
26420
27280
  });
26421
27281
  if (!ExecutionContextService.hasContext()) {
@@ -35155,143 +36015,7 @@ class NotificationBacktestAdapter {
35155
36015
  * @param data - The risk contract data
35156
36016
  */
35157
36017
  this.handleRisk = async (data) => {
35158
- return await this._notificationBacktestUtils.handleRisk(data);
35159
- };
35160
- /**
35161
- * Handles error event.
35162
- * Proxies call to the underlying notification adapter.
35163
- * @param error - The error object
35164
- */
35165
- this.handleError = async (error) => {
35166
- return await this._notificationBacktestUtils.handleError(error);
35167
- };
35168
- /**
35169
- * Handles critical error event.
35170
- * Proxies call to the underlying notification adapter.
35171
- * @param error - The error object
35172
- */
35173
- this.handleCriticalError = async (error) => {
35174
- return await this._notificationBacktestUtils.handleCriticalError(error);
35175
- };
35176
- /**
35177
- * Handles validation error event.
35178
- * Proxies call to the underlying notification adapter.
35179
- * @param error - The error object
35180
- */
35181
- this.handleValidationError = async (error) => {
35182
- return await this._notificationBacktestUtils.handleValidationError(error);
35183
- };
35184
- /**
35185
- * Gets all stored notifications.
35186
- * Proxies call to the underlying notification adapter.
35187
- * @returns Array of all notification models
35188
- */
35189
- this.getData = async () => {
35190
- return await this._notificationBacktestUtils.getData();
35191
- };
35192
- /**
35193
- * Clears all stored notifications.
35194
- * Proxies call to the underlying notification adapter.
35195
- */
35196
- this.clear = async () => {
35197
- return await this._notificationBacktestUtils.clear();
35198
- };
35199
- /**
35200
- * Sets the notification adapter constructor.
35201
- * All future notification operations will use this adapter.
35202
- *
35203
- * @param Ctor - Constructor for notification adapter
35204
- */
35205
- this.useNotificationAdapter = (Ctor) => {
35206
- bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_ADAPTER);
35207
- this._notificationBacktestUtils = Reflect.construct(Ctor, []);
35208
- };
35209
- /**
35210
- * Switches to dummy notification adapter.
35211
- * All future notification writes will be no-ops.
35212
- */
35213
- this.useDummy = () => {
35214
- bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_DUMMY);
35215
- this._notificationBacktestUtils = new NotificationDummyBacktestUtils();
35216
- };
35217
- /**
35218
- * Switches to in-memory notification adapter (default).
35219
- * Notifications will be stored in memory only.
35220
- */
35221
- this.useMemory = () => {
35222
- bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_MEMORY);
35223
- this._notificationBacktestUtils = new NotificationMemoryBacktestUtils();
35224
- };
35225
- /**
35226
- * Switches to persistent notification adapter.
35227
- * Notifications will be persisted to disk.
35228
- */
35229
- this.usePersist = () => {
35230
- bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_PERSIST);
35231
- this._notificationBacktestUtils = new NotificationPersistBacktestUtils();
35232
- };
35233
- }
35234
- }
35235
- /**
35236
- * Live trading notification adapter with pluggable notification backend.
35237
- *
35238
- * Features:
35239
- * - Adapter pattern for swappable notification implementations
35240
- * - Default adapter: NotificationMemoryLiveUtils (in-memory storage)
35241
- * - Alternative adapters: NotificationPersistLiveUtils, NotificationDummyLiveUtils
35242
- * - Convenience methods: usePersist(), useMemory(), useDummy()
35243
- */
35244
- class NotificationLiveAdapter {
35245
- constructor() {
35246
- /** Internal notification utils instance */
35247
- this._notificationLiveUtils = new NotificationMemoryLiveUtils();
35248
- /**
35249
- * Handles signal events.
35250
- * Proxies call to the underlying notification adapter.
35251
- * @param data - The strategy tick result data
35252
- */
35253
- this.handleSignal = async (data) => {
35254
- return await this._notificationLiveUtils.handleSignal(data);
35255
- };
35256
- /**
35257
- * Handles partial profit availability event.
35258
- * Proxies call to the underlying notification adapter.
35259
- * @param data - The partial profit contract data
35260
- */
35261
- this.handlePartialProfit = async (data) => {
35262
- return await this._notificationLiveUtils.handlePartialProfit(data);
35263
- };
35264
- /**
35265
- * Handles partial loss availability event.
35266
- * Proxies call to the underlying notification adapter.
35267
- * @param data - The partial loss contract data
35268
- */
35269
- this.handlePartialLoss = async (data) => {
35270
- return await this._notificationLiveUtils.handlePartialLoss(data);
35271
- };
35272
- /**
35273
- * Handles breakeven availability event.
35274
- * Proxies call to the underlying notification adapter.
35275
- * @param data - The breakeven contract data
35276
- */
35277
- this.handleBreakeven = async (data) => {
35278
- return await this._notificationLiveUtils.handleBreakeven(data);
35279
- };
35280
- /**
35281
- * Handles strategy commit events.
35282
- * Proxies call to the underlying notification adapter.
35283
- * @param data - The strategy commit contract data
35284
- */
35285
- this.handleStrategyCommit = async (data) => {
35286
- return await this._notificationLiveUtils.handleStrategyCommit(data);
35287
- };
35288
- /**
35289
- * Handles risk rejection event.
35290
- * Proxies call to the underlying notification adapter.
35291
- * @param data - The risk contract data
35292
- */
35293
- this.handleRisk = async (data) => {
35294
- return await this._notificationLiveUtils.handleRisk(data);
36018
+ return await this._notificationBacktestUtils.handleRisk(data);
35295
36019
  };
35296
36020
  /**
35297
36021
  * Handles error event.
@@ -35299,7 +36023,7 @@ class NotificationLiveAdapter {
35299
36023
  * @param error - The error object
35300
36024
  */
35301
36025
  this.handleError = async (error) => {
35302
- return await this._notificationLiveUtils.handleError(error);
36026
+ return await this._notificationBacktestUtils.handleError(error);
35303
36027
  };
35304
36028
  /**
35305
36029
  * Handles critical error event.
@@ -35307,7 +36031,7 @@ class NotificationLiveAdapter {
35307
36031
  * @param error - The error object
35308
36032
  */
35309
36033
  this.handleCriticalError = async (error) => {
35310
- return await this._notificationLiveUtils.handleCriticalError(error);
36034
+ return await this._notificationBacktestUtils.handleCriticalError(error);
35311
36035
  };
35312
36036
  /**
35313
36037
  * Handles validation error event.
@@ -35315,7 +36039,7 @@ class NotificationLiveAdapter {
35315
36039
  * @param error - The error object
35316
36040
  */
35317
36041
  this.handleValidationError = async (error) => {
35318
- return await this._notificationLiveUtils.handleValidationError(error);
36042
+ return await this._notificationBacktestUtils.handleValidationError(error);
35319
36043
  };
35320
36044
  /**
35321
36045
  * Gets all stored notifications.
@@ -35323,14 +36047,14 @@ class NotificationLiveAdapter {
35323
36047
  * @returns Array of all notification models
35324
36048
  */
35325
36049
  this.getData = async () => {
35326
- return await this._notificationLiveUtils.getData();
36050
+ return await this._notificationBacktestUtils.getData();
35327
36051
  };
35328
36052
  /**
35329
36053
  * Clears all stored notifications.
35330
36054
  * Proxies call to the underlying notification adapter.
35331
36055
  */
35332
36056
  this.clear = async () => {
35333
- return await this._notificationLiveUtils.clear();
36057
+ return await this._notificationBacktestUtils.clear();
35334
36058
  };
35335
36059
  /**
35336
36060
  * Sets the notification adapter constructor.
@@ -35339,897 +36063,305 @@ class NotificationLiveAdapter {
35339
36063
  * @param Ctor - Constructor for notification adapter
35340
36064
  */
35341
36065
  this.useNotificationAdapter = (Ctor) => {
35342
- bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_ADAPTER);
35343
- this._notificationLiveUtils = Reflect.construct(Ctor, []);
36066
+ bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_ADAPTER);
36067
+ this._notificationBacktestUtils = Reflect.construct(Ctor, []);
35344
36068
  };
35345
36069
  /**
35346
36070
  * Switches to dummy notification adapter.
35347
36071
  * All future notification writes will be no-ops.
35348
36072
  */
35349
36073
  this.useDummy = () => {
35350
- bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_DUMMY);
35351
- this._notificationLiveUtils = new NotificationDummyLiveUtils();
36074
+ bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_DUMMY);
36075
+ this._notificationBacktestUtils = new NotificationDummyBacktestUtils();
35352
36076
  };
35353
36077
  /**
35354
36078
  * Switches to in-memory notification adapter (default).
35355
36079
  * Notifications will be stored in memory only.
35356
36080
  */
35357
36081
  this.useMemory = () => {
35358
- bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_MEMORY);
35359
- this._notificationLiveUtils = new NotificationMemoryLiveUtils();
36082
+ bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_MEMORY);
36083
+ this._notificationBacktestUtils = new NotificationMemoryBacktestUtils();
35360
36084
  };
35361
36085
  /**
35362
36086
  * Switches to persistent notification adapter.
35363
36087
  * Notifications will be persisted to disk.
35364
36088
  */
35365
36089
  this.usePersist = () => {
35366
- bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_PERSIST);
35367
- this._notificationLiveUtils = new NotificationPersistLiveUtils();
36090
+ bt.loggerService.info(NOTIFICATION_BACKTEST_ADAPTER_METHOD_NAME_USE_PERSIST);
36091
+ this._notificationBacktestUtils = new NotificationPersistBacktestUtils();
35368
36092
  };
35369
36093
  }
35370
36094
  }
35371
36095
  /**
35372
- * Main notification adapter that manages both backtest and live notification storage.
36096
+ * Live trading notification adapter with pluggable notification backend.
35373
36097
  *
35374
36098
  * Features:
35375
- * - Subscribes to signal emitters for automatic notification updates
35376
- * - Provides unified access to both backtest and live notifications
35377
- * - Singleshot enable pattern prevents duplicate subscriptions
35378
- * - Cleanup function for proper unsubscription
36099
+ * - Adapter pattern for swappable notification implementations
36100
+ * - Default adapter: NotificationMemoryLiveUtils (in-memory storage)
36101
+ * - Alternative adapters: NotificationPersistLiveUtils, NotificationDummyLiveUtils
36102
+ * - Convenience methods: usePersist(), useMemory(), useDummy()
35379
36103
  */
35380
- class NotificationAdapter {
36104
+ class NotificationLiveAdapter {
35381
36105
  constructor() {
36106
+ /** Internal notification utils instance */
36107
+ this._notificationLiveUtils = new NotificationMemoryLiveUtils();
35382
36108
  /**
35383
- * Enables notification storage by subscribing to signal emitters.
35384
- * Uses singleshot to ensure one-time subscription.
35385
- *
35386
- * @returns Cleanup function that unsubscribes from all emitters
36109
+ * Handles signal events.
36110
+ * Proxies call to the underlying notification adapter.
36111
+ * @param data - The strategy tick result data
35387
36112
  */
35388
- this.enable = singleshot(() => {
35389
- bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_ENABLE);
35390
- let unLive;
35391
- let unBacktest;
35392
- {
35393
- const unBacktestSignal = signalBacktestEmitter.subscribe((data) => NotificationBacktest.handleSignal(data));
35394
- const unBacktestPartialProfit = partialProfitSubject
35395
- .filter(({ backtest }) => backtest)
35396
- .connect((data) => NotificationBacktest.handlePartialProfit(data));
35397
- const unBacktestPartialLoss = partialLossSubject
35398
- .filter(({ backtest }) => backtest)
35399
- .connect((data) => NotificationBacktest.handlePartialLoss(data));
35400
- const unBacktestBreakeven = breakevenSubject
35401
- .filter(({ backtest }) => backtest)
35402
- .connect((data) => NotificationBacktest.handleBreakeven(data));
35403
- const unBacktestStrategyCommit = strategyCommitSubject
35404
- .filter(({ backtest }) => backtest)
35405
- .connect((data) => NotificationBacktest.handleStrategyCommit(data));
35406
- const unBacktestRisk = riskSubject
35407
- .filter(({ backtest }) => backtest)
35408
- .connect((data) => NotificationBacktest.handleRisk(data));
35409
- const unBacktestError = errorEmitter.subscribe((error) => NotificationBacktest.handleError(error));
35410
- const unBacktestExit = exitEmitter.subscribe((error) => NotificationBacktest.handleCriticalError(error));
35411
- const unBacktestValidation = validationSubject.subscribe((error) => NotificationBacktest.handleValidationError(error));
35412
- unBacktest = compose(() => unBacktestSignal(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation());
35413
- }
35414
- {
35415
- const unLiveSignal = signalLiveEmitter.subscribe((data) => NotificationLive.handleSignal(data));
35416
- const unLivePartialProfit = partialProfitSubject
35417
- .filter(({ backtest }) => !backtest)
35418
- .connect((data) => NotificationLive.handlePartialProfit(data));
35419
- const unLivePartialLoss = partialLossSubject
35420
- .filter(({ backtest }) => !backtest)
35421
- .connect((data) => NotificationLive.handlePartialLoss(data));
35422
- const unLiveBreakeven = breakevenSubject
35423
- .filter(({ backtest }) => !backtest)
35424
- .connect((data) => NotificationLive.handleBreakeven(data));
35425
- const unLiveStrategyCommit = strategyCommitSubject
35426
- .filter(({ backtest }) => !backtest)
35427
- .connect((data) => NotificationLive.handleStrategyCommit(data));
35428
- const unLiveRisk = riskSubject
35429
- .filter(({ backtest }) => !backtest)
35430
- .connect((data) => NotificationLive.handleRisk(data));
35431
- const unLiveError = errorEmitter.subscribe((error) => NotificationLive.handleError(error));
35432
- const unLiveExit = exitEmitter.subscribe((error) => NotificationLive.handleCriticalError(error));
35433
- const unLiveValidation = validationSubject.subscribe((error) => NotificationLive.handleValidationError(error));
35434
- unLive = compose(() => unLiveSignal(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation());
35435
- }
35436
- return () => {
35437
- unLive();
35438
- unBacktest();
35439
- this.enable.clear();
35440
- };
35441
- });
36113
+ this.handleSignal = async (data) => {
36114
+ return await this._notificationLiveUtils.handleSignal(data);
36115
+ };
35442
36116
  /**
35443
- * Disables notification storage by unsubscribing from all emitters.
35444
- * Safe to call multiple times.
36117
+ * Handles partial profit availability event.
36118
+ * Proxies call to the underlying notification adapter.
36119
+ * @param data - The partial profit contract data
35445
36120
  */
35446
- this.disable = () => {
35447
- bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_DISABLE);
35448
- if (this.enable.hasValue()) {
35449
- const lastSubscription = this.enable();
35450
- lastSubscription();
35451
- }
36121
+ this.handlePartialProfit = async (data) => {
36122
+ return await this._notificationLiveUtils.handlePartialProfit(data);
35452
36123
  };
35453
36124
  /**
35454
- * Gets all backtest/live notifications from storage.
35455
- *
35456
- * @returns Array of all backtest notification models
35457
- * @throws Error if NotificationAdapter is not enabled
36125
+ * Handles partial loss availability event.
36126
+ * Proxies call to the underlying notification adapter.
36127
+ * @param data - The partial loss contract data
35458
36128
  */
35459
- this.getData = async (isBacktest) => {
35460
- bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_GET_DATA_BACKTEST, {
35461
- backtest: isBacktest,
35462
- });
35463
- if (!this.enable.hasValue()) {
35464
- throw new Error("NotificationAdapter is not enabled. Call enable() first.");
35465
- }
35466
- if (isBacktest) {
35467
- return await NotificationBacktest.getData();
35468
- }
35469
- return await NotificationLive.getData();
36129
+ this.handlePartialLoss = async (data) => {
36130
+ return await this._notificationLiveUtils.handlePartialLoss(data);
35470
36131
  };
35471
36132
  /**
35472
- * Clears all backtest/live notifications from storage.
35473
- *
35474
- * @throws Error if NotificationAdapter is not enabled
36133
+ * Handles breakeven availability event.
36134
+ * Proxies call to the underlying notification adapter.
36135
+ * @param data - The breakeven contract data
35475
36136
  */
35476
- this.clear = async (isBacktest) => {
35477
- bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_CLEAR_LIVE, {
35478
- backtest: isBacktest,
35479
- });
35480
- if (!this.enable.hasValue()) {
35481
- throw new Error("NotificationAdapter is not enabled. Call enable() first.");
35482
- }
35483
- if (isBacktest) {
35484
- return await NotificationBacktest.clear();
35485
- }
35486
- return await NotificationLive.clear();
35487
- };
35488
- }
35489
- }
35490
- /**
35491
- * Global singleton instance of NotificationAdapter.
35492
- * Provides unified notification management for backtest and live trading.
35493
- */
35494
- const Notification = new NotificationAdapter();
35495
- /**
35496
- * Global singleton instance of NotificationLiveAdapter.
35497
- * Provides live trading notification storage with pluggable backends.
35498
- */
35499
- const NotificationLive = new NotificationLiveAdapter();
35500
- /**
35501
- * Global singleton instance of NotificationBacktestAdapter.
35502
- * Provides backtest notification storage with pluggable backends.
35503
- */
35504
- const NotificationBacktest = new NotificationBacktestAdapter();
35505
-
35506
- const EXCHANGE_METHOD_NAME_GET_CANDLES = "ExchangeUtils.getCandles";
35507
- const EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE = "ExchangeUtils.getAveragePrice";
35508
- const EXCHANGE_METHOD_NAME_FORMAT_QUANTITY = "ExchangeUtils.formatQuantity";
35509
- const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
35510
- const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
35511
- const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
35512
- const MS_PER_MINUTE$1 = 60000;
35513
- /**
35514
- * Gets current timestamp from execution context if available.
35515
- * Returns current Date() if no execution context exists (non-trading GUI).
35516
- */
35517
- const GET_TIMESTAMP_FN = async () => {
35518
- if (ExecutionContextService.hasContext()) {
35519
- return new Date(bt.executionContextService.context.when);
35520
- }
35521
- return new Date();
35522
- };
35523
- /**
35524
- * Gets backtest mode flag from execution context if available.
35525
- * Returns false if no execution context exists (live mode).
35526
- */
35527
- const GET_BACKTEST_FN = async () => {
35528
- if (ExecutionContextService.hasContext()) {
35529
- return bt.executionContextService.context.backtest;
35530
- }
35531
- return false;
35532
- };
35533
- /**
35534
- * Default implementation for getCandles.
35535
- * Throws an error indicating the method is not implemented.
35536
- */
35537
- const DEFAULT_GET_CANDLES_FN = async (_symbol, _interval, _since, _limit, _backtest) => {
35538
- throw new Error(`getCandles is not implemented for this exchange`);
35539
- };
35540
- /**
35541
- * Default implementation for formatQuantity.
35542
- * Returns Bitcoin precision on Binance (8 decimal places).
35543
- */
35544
- const DEFAULT_FORMAT_QUANTITY_FN = async (_symbol, quantity, _backtest) => {
35545
- return quantity.toFixed(8);
35546
- };
35547
- /**
35548
- * Default implementation for formatPrice.
35549
- * Returns Bitcoin precision on Binance (2 decimal places).
35550
- */
35551
- const DEFAULT_FORMAT_PRICE_FN = async (_symbol, price, _backtest) => {
35552
- return price.toFixed(2);
35553
- };
35554
- /**
35555
- * Default implementation for getOrderBook.
35556
- * Throws an error indicating the method is not implemented.
35557
- *
35558
- * @param _symbol - Trading pair symbol (unused)
35559
- * @param _depth - Maximum depth levels (unused)
35560
- * @param _from - Start of time range (unused - can be ignored in live implementations)
35561
- * @param _to - End of time range (unused - can be ignored in live implementations)
35562
- * @param _backtest - Whether running in backtest mode (unused)
35563
- */
35564
- const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest) => {
35565
- throw new Error(`getOrderBook is not implemented for this exchange`);
35566
- };
35567
- const INTERVAL_MINUTES$1 = {
35568
- "1m": 1,
35569
- "3m": 3,
35570
- "5m": 5,
35571
- "15m": 15,
35572
- "30m": 30,
35573
- "1h": 60,
35574
- "2h": 120,
35575
- "4h": 240,
35576
- "6h": 360,
35577
- "8h": 480,
35578
- };
35579
- /**
35580
- * Aligns timestamp down to the nearest interval boundary.
35581
- * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
35582
- *
35583
- * Candle timestamp convention:
35584
- * - Candle timestamp = openTime (when candle opens)
35585
- * - Candle with timestamp 00:00 covers period [00:00, 00:15) for 15m interval
35586
- *
35587
- * Adapter contract:
35588
- * - Adapter must return candles with timestamp = openTime
35589
- * - First returned candle.timestamp must equal aligned since
35590
- * - Adapter must return exactly `limit` candles
35591
- *
35592
- * @param timestamp - Timestamp in milliseconds
35593
- * @param intervalMinutes - Interval in minutes
35594
- * @returns Aligned timestamp rounded down to interval boundary
35595
- */
35596
- const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
35597
- const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
35598
- return Math.floor(timestamp / intervalMs) * intervalMs;
35599
- };
35600
- /**
35601
- * Creates exchange instance with methods resolved once during construction.
35602
- * Applies default implementations where schema methods are not provided.
35603
- *
35604
- * @param schema - Exchange schema from registry
35605
- * @returns Object with resolved exchange methods
35606
- */
35607
- const CREATE_EXCHANGE_INSTANCE_FN = (schema) => {
35608
- const getCandles = schema.getCandles ?? DEFAULT_GET_CANDLES_FN;
35609
- const formatQuantity = schema.formatQuantity ?? DEFAULT_FORMAT_QUANTITY_FN;
35610
- const formatPrice = schema.formatPrice ?? DEFAULT_FORMAT_PRICE_FN;
35611
- const getOrderBook = schema.getOrderBook ?? DEFAULT_GET_ORDER_BOOK_FN;
35612
- return {
35613
- getCandles,
35614
- formatQuantity,
35615
- formatPrice,
35616
- getOrderBook,
35617
- };
35618
- };
35619
- /**
35620
- * Attempts to read candles from cache.
35621
- *
35622
- * Cache lookup calculates expected timestamps:
35623
- * sinceTimestamp + i * stepMs for i = 0..limit-1
35624
- * Returns all candles if found, null if any missing.
35625
- *
35626
- * @param dto - Data transfer object containing symbol, interval, and limit
35627
- * @param sinceTimestamp - Aligned start timestamp (openTime of first candle)
35628
- * @param untilTimestamp - Unused, kept for API compatibility
35629
- * @param exchangeName - Exchange name
35630
- * @returns Cached candles array (exactly limit) or null if cache miss
35631
- */
35632
- const READ_CANDLES_CACHE_FN = trycatch(async (dto, sinceTimestamp, untilTimestamp, exchangeName) => {
35633
- // PersistCandleAdapter.readCandlesData calculates expected timestamps:
35634
- // sinceTimestamp + i * stepMs for i = 0..limit-1
35635
- // Returns all candles if found, null if any missing
35636
- const cachedCandles = await PersistCandleAdapter.readCandlesData(dto.symbol, dto.interval, exchangeName, dto.limit, sinceTimestamp, untilTimestamp);
35637
- // Return cached data only if we have exactly the requested limit
35638
- if (cachedCandles?.length === dto.limit) {
35639
- bt.loggerService.debug(`ExchangeInstance READ_CANDLES_CACHE_FN: cache hit for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
35640
- return cachedCandles;
35641
- }
35642
- bt.loggerService.warn(`ExchangeInstance READ_CANDLES_CACHE_FN: cache inconsistent (count or range mismatch) for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, limit=${dto.limit}`);
35643
- return null;
35644
- }, {
35645
- fallback: async (error) => {
35646
- const message = `ExchangeInstance READ_CANDLES_CACHE_FN: cache read failed`;
35647
- const payload = {
35648
- error: errorData(error),
35649
- message: getErrorMessage(error),
35650
- };
35651
- bt.loggerService.warn(message, payload);
35652
- console.warn(message, payload);
35653
- errorEmitter.next(error);
35654
- },
35655
- defaultValue: null,
35656
- });
35657
- /**
35658
- * Writes candles to cache with error handling.
35659
- *
35660
- * The candles passed to this function should be validated:
35661
- * - First candle.timestamp equals aligned sinceTimestamp (openTime)
35662
- * - Exact number of candles as requested (limit)
35663
- * - Sequential timestamps: sinceTimestamp + i * stepMs
35664
- *
35665
- * @param candles - Array of validated candle data to cache
35666
- * @param dto - Data transfer object containing symbol, interval, and limit
35667
- * @param exchangeName - Exchange name
35668
- */
35669
- const WRITE_CANDLES_CACHE_FN = trycatch(queued(async (candles, dto, exchangeName) => {
35670
- await PersistCandleAdapter.writeCandlesData(candles, dto.symbol, dto.interval, exchangeName);
35671
- bt.loggerService.debug(`ExchangeInstance WRITE_CANDLES_CACHE_FN: cache updated for exchangeName=${exchangeName}, symbol=${dto.symbol}, interval=${dto.interval}, count=${candles.length}`);
35672
- }), {
35673
- fallback: async (error) => {
35674
- const message = `ExchangeInstance WRITE_CANDLES_CACHE_FN: cache write failed`;
35675
- const payload = {
35676
- error: errorData(error),
35677
- message: getErrorMessage(error),
36137
+ this.handleBreakeven = async (data) => {
36138
+ return await this._notificationLiveUtils.handleBreakeven(data);
35678
36139
  };
35679
- bt.loggerService.warn(message, payload);
35680
- console.warn(message, payload);
35681
- errorEmitter.next(error);
35682
- },
35683
- defaultValue: null,
35684
- });
35685
- /**
35686
- * Instance class for exchange operations on a specific exchange.
35687
- *
35688
- * Provides isolated exchange operations for a single exchange.
35689
- * Each instance maintains its own context and exposes IExchangeSchema methods.
35690
- * The schema is retrieved once during construction for better performance.
35691
- *
35692
- * @example
35693
- * ```typescript
35694
- * const instance = new ExchangeInstance("binance");
35695
- *
35696
- * const candles = await instance.getCandles("BTCUSDT", "1m", 100);
35697
- * const vwap = await instance.getAveragePrice("BTCUSDT");
35698
- * const formattedQty = await instance.formatQuantity("BTCUSDT", 0.001);
35699
- * const formattedPrice = await instance.formatPrice("BTCUSDT", 50000.123);
35700
- * ```
35701
- */
35702
- class ExchangeInstance {
35703
- /**
35704
- * Creates a new ExchangeInstance for a specific exchange.
35705
- *
35706
- * @param exchangeName - Exchange name (e.g., "binance")
35707
- */
35708
- constructor(exchangeName) {
35709
- this.exchangeName = exchangeName;
35710
36140
  /**
35711
- * Fetch candles from data source (API or database).
35712
- *
35713
- * Automatically calculates the start date based on Date.now() and the requested interval/limit.
35714
- * Uses the same logic as ClientExchange to ensure backwards compatibility.
35715
- *
35716
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
35717
- * @param interval - Candle time interval (e.g., "1m", "1h")
35718
- * @param limit - Maximum number of candles to fetch
35719
- * @returns Promise resolving to array of OHLCV candle data
35720
- *
35721
- * @example
35722
- * ```typescript
35723
- * const instance = new ExchangeInstance("binance");
35724
- * const candles = await instance.getCandles("BTCUSDT", "1m", 100);
35725
- * ```
36141
+ * Handles strategy commit events.
36142
+ * Proxies call to the underlying notification adapter.
36143
+ * @param data - The strategy commit contract data
35726
36144
  */
35727
- this.getCandles = async (symbol, interval, limit) => {
35728
- bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_CANDLES, {
35729
- exchangeName: this.exchangeName,
35730
- symbol,
35731
- interval,
35732
- limit,
35733
- });
35734
- const getCandles = this._methods.getCandles;
35735
- const step = INTERVAL_MINUTES$1[interval];
35736
- if (!step) {
35737
- throw new Error(`ExchangeInstance unknown interval=${interval}`);
35738
- }
35739
- const stepMs = step * MS_PER_MINUTE$1;
35740
- // Align when down to interval boundary
35741
- const when = await GET_TIMESTAMP_FN();
35742
- const whenTimestamp = when.getTime();
35743
- const alignedWhen = ALIGN_TO_INTERVAL_FN(whenTimestamp, step);
35744
- // Calculate since: go back limit candles from aligned when
35745
- const sinceTimestamp = alignedWhen - limit * stepMs;
35746
- const since = new Date(sinceTimestamp);
35747
- const untilTimestamp = alignedWhen;
35748
- // Try to read from cache first
35749
- const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit }, sinceTimestamp, untilTimestamp, this.exchangeName);
35750
- if (cachedCandles !== null) {
35751
- return cachedCandles;
35752
- }
35753
- let allData = [];
35754
- // If limit exceeds CC_MAX_CANDLES_PER_REQUEST, fetch data in chunks
35755
- if (limit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
35756
- let remaining = limit;
35757
- let currentSince = new Date(since.getTime());
35758
- const isBacktest = await GET_BACKTEST_FN();
35759
- while (remaining > 0) {
35760
- const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
35761
- const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
35762
- allData.push(...chunkData);
35763
- remaining -= chunkLimit;
35764
- if (remaining > 0) {
35765
- // Move currentSince forward by the number of candles fetched
35766
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
35767
- }
35768
- }
35769
- }
35770
- else {
35771
- const isBacktest = await GET_BACKTEST_FN();
35772
- allData = await getCandles(symbol, interval, since, limit, isBacktest);
35773
- }
35774
- // Apply distinct by timestamp to remove duplicates
35775
- const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
35776
- if (allData.length !== uniqueData.length) {
35777
- bt.loggerService.warn(`ExchangeInstance getCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
35778
- }
35779
- // Validate adapter returned data
35780
- if (uniqueData.length === 0) {
35781
- throw new Error(`ExchangeInstance getCandles: adapter returned empty array. ` +
35782
- `Expected ${limit} candles starting from openTime=${sinceTimestamp}.`);
35783
- }
35784
- if (uniqueData[0].timestamp !== sinceTimestamp) {
35785
- throw new Error(`ExchangeInstance getCandles: first candle timestamp mismatch. ` +
35786
- `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
35787
- `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
35788
- }
35789
- if (uniqueData.length !== limit) {
35790
- throw new Error(`ExchangeInstance getCandles: candle count mismatch. ` +
35791
- `Expected ${limit} candles, got ${uniqueData.length}. ` +
35792
- `Adapter must return exact number of candles requested.`);
35793
- }
35794
- // Write to cache after successful fetch
35795
- await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit }, this.exchangeName);
35796
- return uniqueData;
36145
+ this.handleStrategyCommit = async (data) => {
36146
+ return await this._notificationLiveUtils.handleStrategyCommit(data);
35797
36147
  };
35798
36148
  /**
35799
- * Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
35800
- * The number of candles is configurable via GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT.
35801
- *
35802
- * Formula:
35803
- * - Typical Price = (high + low + close) / 3
35804
- * - VWAP = sum(typical_price * volume) / sum(volume)
35805
- *
35806
- * If volume is zero, returns simple average of close prices.
35807
- *
35808
- * @param symbol - Trading pair symbol
35809
- * @returns Promise resolving to VWAP price
35810
- * @throws Error if no candles available
35811
- *
35812
- * @example
35813
- * ```typescript
35814
- * const instance = new ExchangeInstance("binance");
35815
- * const vwap = await instance.getAveragePrice("BTCUSDT");
35816
- * console.log(vwap); // 50125.43
35817
- * ```
35818
- */
35819
- this.getAveragePrice = async (symbol) => {
35820
- bt.loggerService.debug(`ExchangeInstance getAveragePrice`, {
35821
- exchangeName: this.exchangeName,
35822
- symbol,
35823
- });
35824
- const candles = await this.getCandles(symbol, "1m", GLOBAL_CONFIG.CC_AVG_PRICE_CANDLES_COUNT);
35825
- if (candles.length === 0) {
35826
- throw new Error(`ExchangeInstance getAveragePrice: no candles data for symbol=${symbol}`);
35827
- }
35828
- // VWAP (Volume Weighted Average Price)
35829
- // Используем типичную цену (typical price) = (high + low + close) / 3
35830
- const sumPriceVolume = candles.reduce((acc, candle) => {
35831
- const typicalPrice = (candle.high + candle.low + candle.close) / 3;
35832
- return acc + typicalPrice * candle.volume;
35833
- }, 0);
35834
- const totalVolume = candles.reduce((acc, candle) => acc + candle.volume, 0);
35835
- if (totalVolume === 0) {
35836
- // Если объем нулевой, возвращаем простое среднее close цен
35837
- const sum = candles.reduce((acc, candle) => acc + candle.close, 0);
35838
- return sum / candles.length;
35839
- }
35840
- const vwap = sumPriceVolume / totalVolume;
35841
- return vwap;
36149
+ * Handles risk rejection event.
36150
+ * Proxies call to the underlying notification adapter.
36151
+ * @param data - The risk contract data
36152
+ */
36153
+ this.handleRisk = async (data) => {
36154
+ return await this._notificationLiveUtils.handleRisk(data);
35842
36155
  };
35843
36156
  /**
35844
- * Format quantity according to exchange precision rules.
35845
- *
35846
- * @param symbol - Trading pair symbol
35847
- * @param quantity - Raw quantity value
35848
- * @returns Promise resolving to formatted quantity string
35849
- *
35850
- * @example
35851
- * ```typescript
35852
- * const instance = new ExchangeInstance("binance");
35853
- * const formatted = await instance.formatQuantity("BTCUSDT", 0.001);
35854
- * console.log(formatted); // "0.00100000"
35855
- * ```
36157
+ * Handles error event.
36158
+ * Proxies call to the underlying notification adapter.
36159
+ * @param error - The error object
35856
36160
  */
35857
- this.formatQuantity = async (symbol, quantity) => {
35858
- bt.loggerService.info(EXCHANGE_METHOD_NAME_FORMAT_QUANTITY, {
35859
- exchangeName: this.exchangeName,
35860
- symbol,
35861
- quantity,
35862
- });
35863
- const isBacktest = await GET_BACKTEST_FN();
35864
- return await this._methods.formatQuantity(symbol, quantity, isBacktest);
36161
+ this.handleError = async (error) => {
36162
+ return await this._notificationLiveUtils.handleError(error);
35865
36163
  };
35866
36164
  /**
35867
- * Format price according to exchange precision rules.
35868
- *
35869
- * @param symbol - Trading pair symbol
35870
- * @param price - Raw price value
35871
- * @returns Promise resolving to formatted price string
35872
- *
35873
- * @example
35874
- * ```typescript
35875
- * const instance = new ExchangeInstance("binance");
35876
- * const formatted = await instance.formatPrice("BTCUSDT", 50000.123);
35877
- * console.log(formatted); // "50000.12"
35878
- * ```
36165
+ * Handles critical error event.
36166
+ * Proxies call to the underlying notification adapter.
36167
+ * @param error - The error object
35879
36168
  */
35880
- this.formatPrice = async (symbol, price) => {
35881
- bt.loggerService.info(EXCHANGE_METHOD_NAME_FORMAT_PRICE, {
35882
- exchangeName: this.exchangeName,
35883
- symbol,
35884
- price,
35885
- });
35886
- const isBacktest = await GET_BACKTEST_FN();
35887
- return await this._methods.formatPrice(symbol, price, isBacktest);
36169
+ this.handleCriticalError = async (error) => {
36170
+ return await this._notificationLiveUtils.handleCriticalError(error);
35888
36171
  };
35889
36172
  /**
35890
- * Fetch order book for a trading pair.
35891
- *
35892
- * Calculates time range using CC_ORDER_BOOK_TIME_OFFSET_MINUTES (default 10 minutes)
35893
- * and passes it to the exchange schema implementation. The implementation may use
35894
- * the time range (backtest) or ignore it (live trading).
35895
- *
35896
- * @param symbol - Trading pair symbol
35897
- * @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
35898
- * @returns Promise resolving to order book data
35899
- * @throws Error if getOrderBook is not implemented
35900
- *
35901
- * @example
35902
- * ```typescript
35903
- * const instance = new ExchangeInstance("binance");
35904
- * const orderBook = await instance.getOrderBook("BTCUSDT");
35905
- * console.log(orderBook.bids); // [{ price: "50000.00", quantity: "0.5" }, ...]
35906
- * const deepOrderBook = await instance.getOrderBook("BTCUSDT", 100);
35907
- * ```
36173
+ * Handles validation error event.
36174
+ * Proxies call to the underlying notification adapter.
36175
+ * @param error - The error object
35908
36176
  */
35909
- this.getOrderBook = async (symbol, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) => {
35910
- bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_ORDER_BOOK, {
35911
- exchangeName: this.exchangeName,
35912
- symbol,
35913
- depth,
35914
- });
35915
- const when = await GET_TIMESTAMP_FN();
35916
- const alignedTo = ALIGN_TO_INTERVAL_FN(when.getTime(), GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
35917
- const to = new Date(alignedTo);
35918
- const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$1);
35919
- const isBacktest = await GET_BACKTEST_FN();
35920
- return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
36177
+ this.handleValidationError = async (error) => {
36178
+ return await this._notificationLiveUtils.handleValidationError(error);
35921
36179
  };
35922
36180
  /**
35923
- * Fetches raw candles with flexible date/limit parameters.
35924
- *
35925
- * Uses Date.now() instead of execution context when for look-ahead bias protection.
35926
- *
35927
- * Parameter combinations:
35928
- * 1. sDate + eDate + limit: fetches with explicit parameters, validates eDate <= now
35929
- * 2. sDate + eDate: calculates limit from date range, validates eDate <= now
35930
- * 3. eDate + limit: calculates sDate backward, validates eDate <= now
35931
- * 4. sDate + limit: fetches forward, validates calculated endTimestamp <= now
35932
- * 5. Only limit: uses Date.now() as reference (backward)
35933
- *
35934
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
35935
- * @param interval - Candle interval (e.g., "1m", "1h")
35936
- * @param limit - Optional number of candles to fetch
35937
- * @param sDate - Optional start date in milliseconds
35938
- * @param eDate - Optional end date in milliseconds
35939
- * @returns Promise resolving to array of candle data
35940
- *
35941
- * @example
35942
- * ```typescript
35943
- * const instance = new ExchangeInstance("binance");
35944
- *
35945
- * // Fetch 100 candles backward from now
35946
- * const candles = await instance.getRawCandles("BTCUSDT", "1m", 100);
35947
- *
35948
- * // Fetch candles for specific date range
35949
- * const rangeCandles = await instance.getRawCandles("BTCUSDT", "1h", undefined, startMs, endMs);
35950
- * ```
36181
+ * Gets all stored notifications.
36182
+ * Proxies call to the underlying notification adapter.
36183
+ * @returns Array of all notification models
35951
36184
  */
35952
- this.getRawCandles = async (symbol, interval, limit, sDate, eDate) => {
35953
- bt.loggerService.info(EXCHANGE_METHOD_NAME_GET_RAW_CANDLES, {
35954
- exchangeName: this.exchangeName,
35955
- symbol,
35956
- interval,
35957
- limit,
35958
- sDate,
35959
- eDate,
35960
- });
35961
- const step = INTERVAL_MINUTES$1[interval];
35962
- if (!step) {
35963
- throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
35964
- }
35965
- const stepMs = step * MS_PER_MINUTE$1;
35966
- const when = await GET_TIMESTAMP_FN();
35967
- const nowTimestamp = when.getTime();
35968
- const alignedNow = ALIGN_TO_INTERVAL_FN(nowTimestamp, step);
35969
- let sinceTimestamp;
35970
- let calculatedLimit;
35971
- // Case 1: all three parameters provided
35972
- if (sDate !== undefined && eDate !== undefined && limit !== undefined) {
35973
- if (sDate >= eDate) {
35974
- throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
35975
- }
35976
- if (eDate > nowTimestamp) {
35977
- throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
35978
- }
35979
- // Align sDate down to interval boundary
35980
- sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
35981
- calculatedLimit = limit;
35982
- }
35983
- // Case 2: sDate + eDate (no limit) - calculate limit from date range
35984
- else if (sDate !== undefined && eDate !== undefined && limit === undefined) {
35985
- if (sDate >= eDate) {
35986
- throw new Error(`ExchangeInstance getRawCandles: sDate (${sDate}) must be < eDate (${eDate})`);
35987
- }
35988
- if (eDate > nowTimestamp) {
35989
- throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
35990
- }
35991
- // Align sDate down to interval boundary
35992
- sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
35993
- const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
35994
- calculatedLimit = Math.ceil((alignedEDate - sinceTimestamp) / stepMs);
35995
- if (calculatedLimit <= 0) {
35996
- throw new Error(`ExchangeInstance getRawCandles: calculated limit is ${calculatedLimit}, must be > 0`);
35997
- }
35998
- }
35999
- // Case 3: eDate + limit (no sDate) - calculate sDate backward from eDate
36000
- else if (sDate === undefined && eDate !== undefined && limit !== undefined) {
36001
- if (eDate > nowTimestamp) {
36002
- throw new Error(`ExchangeInstance getRawCandles: eDate (${eDate}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
36003
- }
36004
- // Align eDate down and calculate sinceTimestamp
36005
- const alignedEDate = ALIGN_TO_INTERVAL_FN(eDate, step);
36006
- sinceTimestamp = alignedEDate - limit * stepMs;
36007
- calculatedLimit = limit;
36008
- }
36009
- // Case 4: sDate + limit (no eDate) - calculate eDate forward from sDate
36010
- else if (sDate !== undefined && eDate === undefined && limit !== undefined) {
36011
- // Align sDate down to interval boundary
36012
- sinceTimestamp = ALIGN_TO_INTERVAL_FN(sDate, step);
36013
- const endTimestamp = sinceTimestamp + limit * stepMs;
36014
- if (endTimestamp > nowTimestamp) {
36015
- throw new Error(`ExchangeInstance getRawCandles: calculated endTimestamp (${endTimestamp}) exceeds current time (${nowTimestamp}). Look-ahead bias protection.`);
36016
- }
36017
- calculatedLimit = limit;
36018
- }
36019
- // Case 5: Only limit - use Date.now() as reference (backward)
36020
- else if (sDate === undefined && eDate === undefined && limit !== undefined) {
36021
- sinceTimestamp = alignedNow - limit * stepMs;
36022
- calculatedLimit = limit;
36023
- }
36024
- // Invalid: no parameters or only sDate or only eDate
36025
- else {
36026
- throw new Error(`ExchangeInstance getRawCandles: invalid parameter combination. ` +
36027
- `Provide one of: (sDate+eDate+limit), (sDate+eDate), (eDate+limit), (sDate+limit), or (limit only). ` +
36028
- `Got: sDate=${sDate}, eDate=${eDate}, limit=${limit}`);
36029
- }
36030
- // Try to read from cache first
36031
- const untilTimestamp = sinceTimestamp + calculatedLimit * stepMs;
36032
- const cachedCandles = await READ_CANDLES_CACHE_FN({ symbol, interval, limit: calculatedLimit }, sinceTimestamp, untilTimestamp, this.exchangeName);
36033
- if (cachedCandles !== null) {
36034
- return cachedCandles;
36035
- }
36036
- // Fetch candles
36037
- const since = new Date(sinceTimestamp);
36038
- let allData = [];
36039
- const isBacktest = await GET_BACKTEST_FN();
36040
- const getCandles = this._methods.getCandles;
36041
- if (calculatedLimit > GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST) {
36042
- let remaining = calculatedLimit;
36043
- let currentSince = new Date(since.getTime());
36044
- while (remaining > 0) {
36045
- const chunkLimit = Math.min(remaining, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
36046
- const chunkData = await getCandles(symbol, interval, currentSince, chunkLimit, isBacktest);
36047
- allData.push(...chunkData);
36048
- remaining -= chunkLimit;
36049
- if (remaining > 0) {
36050
- currentSince = new Date(currentSince.getTime() + chunkLimit * stepMs);
36051
- }
36052
- }
36053
- }
36054
- else {
36055
- allData = await getCandles(symbol, interval, since, calculatedLimit, isBacktest);
36056
- }
36057
- // Apply distinct by timestamp to remove duplicates
36058
- const uniqueData = Array.from(new Map(allData.map((candle) => [candle.timestamp, candle])).values());
36059
- if (allData.length !== uniqueData.length) {
36060
- bt.loggerService.warn(`ExchangeInstance getRawCandles: Removed ${allData.length - uniqueData.length} duplicate candles by timestamp`);
36061
- }
36062
- // Validate adapter returned data
36063
- if (uniqueData.length === 0) {
36064
- throw new Error(`ExchangeInstance getRawCandles: adapter returned empty array. ` +
36065
- `Expected ${calculatedLimit} candles starting from openTime=${sinceTimestamp}.`);
36066
- }
36067
- if (uniqueData[0].timestamp !== sinceTimestamp) {
36068
- throw new Error(`ExchangeInstance getRawCandles: first candle timestamp mismatch. ` +
36069
- `Expected openTime=${sinceTimestamp}, got=${uniqueData[0].timestamp}. ` +
36070
- `Adapter must return candles with timestamp=openTime, starting from aligned since.`);
36071
- }
36072
- if (uniqueData.length !== calculatedLimit) {
36073
- throw new Error(`ExchangeInstance getRawCandles: candle count mismatch. ` +
36074
- `Expected ${calculatedLimit} candles, got ${uniqueData.length}. ` +
36075
- `Adapter must return exact number of candles requested.`);
36076
- }
36077
- // Write to cache after successful fetch
36078
- await WRITE_CANDLES_CACHE_FN(uniqueData, { symbol, interval, limit: calculatedLimit }, this.exchangeName);
36079
- return uniqueData;
36185
+ this.getData = async () => {
36186
+ return await this._notificationLiveUtils.getData();
36080
36187
  };
36081
- const schema = bt.exchangeSchemaService.get(this.exchangeName);
36082
- this._methods = CREATE_EXCHANGE_INSTANCE_FN(schema);
36083
- }
36084
- }
36085
- /**
36086
- * Utility class for exchange operations.
36087
- *
36088
- * Provides simplified access to exchange schema methods with validation.
36089
- * Exported as singleton instance for convenient usage.
36090
- *
36091
- * @example
36092
- * ```typescript
36093
- * import { Exchange } from "./classes/Exchange";
36094
- *
36095
- * const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
36096
- * exchangeName: "binance"
36097
- * });
36098
- * const vwap = await Exchange.getAveragePrice("BTCUSDT", {
36099
- * exchangeName: "binance"
36100
- * });
36101
- * const formatted = await Exchange.formatQuantity("BTCUSDT", 0.001, {
36102
- * exchangeName: "binance"
36103
- * });
36104
- * ```
36105
- */
36106
- class ExchangeUtils {
36107
- constructor() {
36108
36188
  /**
36109
- * Memoized function to get or create ExchangeInstance for an exchange.
36110
- * Each exchange gets its own isolated instance.
36189
+ * Clears all stored notifications.
36190
+ * Proxies call to the underlying notification adapter.
36111
36191
  */
36112
- this._getInstance = memoize(([exchangeName]) => exchangeName, (exchangeName) => new ExchangeInstance(exchangeName));
36192
+ this.clear = async () => {
36193
+ return await this._notificationLiveUtils.clear();
36194
+ };
36113
36195
  /**
36114
- * Fetch candles from data source (API or database).
36115
- *
36116
- * Automatically calculates the start date based on Date.now() and the requested interval/limit.
36117
- * Uses the same logic as ClientExchange to ensure backwards compatibility.
36196
+ * Sets the notification adapter constructor.
36197
+ * All future notification operations will use this adapter.
36118
36198
  *
36119
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
36120
- * @param interval - Candle time interval (e.g., "1m", "1h")
36121
- * @param limit - Maximum number of candles to fetch
36122
- * @param context - Execution context with exchange name
36123
- * @returns Promise resolving to array of OHLCV candle data
36199
+ * @param Ctor - Constructor for notification adapter
36124
36200
  */
36125
- this.getCandles = async (symbol, interval, limit, context) => {
36126
- bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_CANDLES);
36127
- const instance = this._getInstance(context.exchangeName);
36128
- return await instance.getCandles(symbol, interval, limit);
36201
+ this.useNotificationAdapter = (Ctor) => {
36202
+ bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_ADAPTER);
36203
+ this._notificationLiveUtils = Reflect.construct(Ctor, []);
36129
36204
  };
36130
36205
  /**
36131
- * Calculates VWAP (Volume Weighted Average Price) from last N 1m candles.
36132
- *
36133
- * @param symbol - Trading pair symbol
36134
- * @param context - Execution context with exchange name
36135
- * @returns Promise resolving to VWAP price
36206
+ * Switches to dummy notification adapter.
36207
+ * All future notification writes will be no-ops.
36136
36208
  */
36137
- this.getAveragePrice = async (symbol, context) => {
36138
- bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_AVERAGE_PRICE);
36139
- const instance = this._getInstance(context.exchangeName);
36140
- return await instance.getAveragePrice(symbol);
36209
+ this.useDummy = () => {
36210
+ bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_DUMMY);
36211
+ this._notificationLiveUtils = new NotificationDummyLiveUtils();
36141
36212
  };
36142
36213
  /**
36143
- * Format quantity according to exchange precision rules.
36144
- *
36145
- * @param symbol - Trading pair symbol
36146
- * @param quantity - Raw quantity value
36147
- * @param context - Execution context with exchange name
36148
- * @returns Promise resolving to formatted quantity string
36214
+ * Switches to in-memory notification adapter (default).
36215
+ * Notifications will be stored in memory only.
36149
36216
  */
36150
- this.formatQuantity = async (symbol, quantity, context) => {
36151
- bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_FORMAT_QUANTITY);
36152
- const instance = this._getInstance(context.exchangeName);
36153
- return await instance.formatQuantity(symbol, quantity);
36217
+ this.useMemory = () => {
36218
+ bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_MEMORY);
36219
+ this._notificationLiveUtils = new NotificationMemoryLiveUtils();
36154
36220
  };
36155
36221
  /**
36156
- * Format price according to exchange precision rules.
36157
- *
36158
- * @param symbol - Trading pair symbol
36159
- * @param price - Raw price value
36160
- * @param context - Execution context with exchange name
36161
- * @returns Promise resolving to formatted price string
36222
+ * Switches to persistent notification adapter.
36223
+ * Notifications will be persisted to disk.
36162
36224
  */
36163
- this.formatPrice = async (symbol, price, context) => {
36164
- bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_FORMAT_PRICE);
36165
- const instance = this._getInstance(context.exchangeName);
36166
- return await instance.formatPrice(symbol, price);
36225
+ this.usePersist = () => {
36226
+ bt.loggerService.info(NOTIFICATION_LIVE_ADAPTER_METHOD_NAME_USE_PERSIST);
36227
+ this._notificationLiveUtils = new NotificationPersistLiveUtils();
36167
36228
  };
36229
+ }
36230
+ }
36231
+ /**
36232
+ * Main notification adapter that manages both backtest and live notification storage.
36233
+ *
36234
+ * Features:
36235
+ * - Subscribes to signal emitters for automatic notification updates
36236
+ * - Provides unified access to both backtest and live notifications
36237
+ * - Singleshot enable pattern prevents duplicate subscriptions
36238
+ * - Cleanup function for proper unsubscription
36239
+ */
36240
+ class NotificationAdapter {
36241
+ constructor() {
36168
36242
  /**
36169
- * Fetch order book for a trading pair.
36170
- *
36171
- * Delegates to ExchangeInstance which calculates time range and passes it
36172
- * to the exchange schema implementation. The from/to parameters may be used
36173
- * (backtest) or ignored (live) depending on the implementation.
36243
+ * Enables notification storage by subscribing to signal emitters.
36244
+ * Uses singleshot to ensure one-time subscription.
36174
36245
  *
36175
- * @param symbol - Trading pair symbol
36176
- * @param context - Execution context with exchange name
36177
- * @param depth - Maximum depth levels (default: CC_ORDER_BOOK_MAX_DEPTH_LEVELS)
36178
- * @returns Promise resolving to order book data
36246
+ * @returns Cleanup function that unsubscribes from all emitters
36179
36247
  */
36180
- this.getOrderBook = async (symbol, context, depth = GLOBAL_CONFIG.CC_ORDER_BOOK_MAX_DEPTH_LEVELS) => {
36181
- bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_ORDER_BOOK);
36182
- const instance = this._getInstance(context.exchangeName);
36183
- return await instance.getOrderBook(symbol, depth);
36248
+ this.enable = singleshot(() => {
36249
+ bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_ENABLE);
36250
+ let unLive;
36251
+ let unBacktest;
36252
+ {
36253
+ const unBacktestSignal = signalBacktestEmitter.subscribe((data) => NotificationBacktest.handleSignal(data));
36254
+ const unBacktestPartialProfit = partialProfitSubject
36255
+ .filter(({ backtest }) => backtest)
36256
+ .connect((data) => NotificationBacktest.handlePartialProfit(data));
36257
+ const unBacktestPartialLoss = partialLossSubject
36258
+ .filter(({ backtest }) => backtest)
36259
+ .connect((data) => NotificationBacktest.handlePartialLoss(data));
36260
+ const unBacktestBreakeven = breakevenSubject
36261
+ .filter(({ backtest }) => backtest)
36262
+ .connect((data) => NotificationBacktest.handleBreakeven(data));
36263
+ const unBacktestStrategyCommit = strategyCommitSubject
36264
+ .filter(({ backtest }) => backtest)
36265
+ .connect((data) => NotificationBacktest.handleStrategyCommit(data));
36266
+ const unBacktestRisk = riskSubject
36267
+ .filter(({ backtest }) => backtest)
36268
+ .connect((data) => NotificationBacktest.handleRisk(data));
36269
+ const unBacktestError = errorEmitter.subscribe((error) => NotificationBacktest.handleError(error));
36270
+ const unBacktestExit = exitEmitter.subscribe((error) => NotificationBacktest.handleCriticalError(error));
36271
+ const unBacktestValidation = validationSubject.subscribe((error) => NotificationBacktest.handleValidationError(error));
36272
+ unBacktest = compose(() => unBacktestSignal(), () => unBacktestPartialProfit(), () => unBacktestPartialLoss(), () => unBacktestBreakeven(), () => unBacktestStrategyCommit(), () => unBacktestRisk(), () => unBacktestError(), () => unBacktestExit(), () => unBacktestValidation());
36273
+ }
36274
+ {
36275
+ const unLiveSignal = signalLiveEmitter.subscribe((data) => NotificationLive.handleSignal(data));
36276
+ const unLivePartialProfit = partialProfitSubject
36277
+ .filter(({ backtest }) => !backtest)
36278
+ .connect((data) => NotificationLive.handlePartialProfit(data));
36279
+ const unLivePartialLoss = partialLossSubject
36280
+ .filter(({ backtest }) => !backtest)
36281
+ .connect((data) => NotificationLive.handlePartialLoss(data));
36282
+ const unLiveBreakeven = breakevenSubject
36283
+ .filter(({ backtest }) => !backtest)
36284
+ .connect((data) => NotificationLive.handleBreakeven(data));
36285
+ const unLiveStrategyCommit = strategyCommitSubject
36286
+ .filter(({ backtest }) => !backtest)
36287
+ .connect((data) => NotificationLive.handleStrategyCommit(data));
36288
+ const unLiveRisk = riskSubject
36289
+ .filter(({ backtest }) => !backtest)
36290
+ .connect((data) => NotificationLive.handleRisk(data));
36291
+ const unLiveError = errorEmitter.subscribe((error) => NotificationLive.handleError(error));
36292
+ const unLiveExit = exitEmitter.subscribe((error) => NotificationLive.handleCriticalError(error));
36293
+ const unLiveValidation = validationSubject.subscribe((error) => NotificationLive.handleValidationError(error));
36294
+ unLive = compose(() => unLiveSignal(), () => unLivePartialProfit(), () => unLivePartialLoss(), () => unLiveBreakeven(), () => unLiveStrategyCommit(), () => unLiveRisk(), () => unLiveError(), () => unLiveExit(), () => unLiveValidation());
36295
+ }
36296
+ return () => {
36297
+ unLive();
36298
+ unBacktest();
36299
+ this.enable.clear();
36300
+ };
36301
+ });
36302
+ /**
36303
+ * Disables notification storage by unsubscribing from all emitters.
36304
+ * Safe to call multiple times.
36305
+ */
36306
+ this.disable = () => {
36307
+ bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_DISABLE);
36308
+ if (this.enable.hasValue()) {
36309
+ const lastSubscription = this.enable();
36310
+ lastSubscription();
36311
+ }
36184
36312
  };
36185
36313
  /**
36186
- * Fetches raw candles with flexible date/limit parameters.
36314
+ * Gets all backtest/live notifications from storage.
36187
36315
  *
36188
- * Uses Date.now() instead of execution context when for look-ahead bias protection.
36316
+ * @returns Array of all backtest notification models
36317
+ * @throws Error if NotificationAdapter is not enabled
36318
+ */
36319
+ this.getData = async (isBacktest) => {
36320
+ bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_GET_DATA_BACKTEST, {
36321
+ backtest: isBacktest,
36322
+ });
36323
+ if (!this.enable.hasValue()) {
36324
+ throw new Error("NotificationAdapter is not enabled. Call enable() first.");
36325
+ }
36326
+ if (isBacktest) {
36327
+ return await NotificationBacktest.getData();
36328
+ }
36329
+ return await NotificationLive.getData();
36330
+ };
36331
+ /**
36332
+ * Clears all backtest/live notifications from storage.
36189
36333
  *
36190
- * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
36191
- * @param interval - Candle interval (e.g., "1m", "1h")
36192
- * @param context - Execution context with exchange name
36193
- * @param limit - Optional number of candles to fetch
36194
- * @param sDate - Optional start date in milliseconds
36195
- * @param eDate - Optional end date in milliseconds
36196
- * @returns Promise resolving to array of candle data
36334
+ * @throws Error if NotificationAdapter is not enabled
36197
36335
  */
36198
- this.getRawCandles = async (symbol, interval, context, limit, sDate, eDate) => {
36199
- bt.exchangeValidationService.validate(context.exchangeName, EXCHANGE_METHOD_NAME_GET_RAW_CANDLES);
36200
- const instance = this._getInstance(context.exchangeName);
36201
- return await instance.getRawCandles(symbol, interval, limit, sDate, eDate);
36336
+ this.clear = async (isBacktest) => {
36337
+ bt.loggerService.info(NOTIFICATION_ADAPTER_METHOD_NAME_CLEAR_LIVE, {
36338
+ backtest: isBacktest,
36339
+ });
36340
+ if (!this.enable.hasValue()) {
36341
+ throw new Error("NotificationAdapter is not enabled. Call enable() first.");
36342
+ }
36343
+ if (isBacktest) {
36344
+ return await NotificationBacktest.clear();
36345
+ }
36346
+ return await NotificationLive.clear();
36202
36347
  };
36203
36348
  }
36204
36349
  }
36205
36350
  /**
36206
- * Singleton instance of ExchangeUtils for convenient exchange operations.
36207
- *
36208
- * @example
36209
- * ```typescript
36210
- * import { Exchange } from "./classes/Exchange";
36211
- *
36212
- * // Using static-like API with context
36213
- * const candles = await Exchange.getCandles("BTCUSDT", "1m", 100, {
36214
- * exchangeName: "binance"
36215
- * });
36216
- * const vwap = await Exchange.getAveragePrice("BTCUSDT", {
36217
- * exchangeName: "binance"
36218
- * });
36219
- * const qty = await Exchange.formatQuantity("BTCUSDT", 0.001, {
36220
- * exchangeName: "binance"
36221
- * });
36222
- * const price = await Exchange.formatPrice("BTCUSDT", 50000.123, {
36223
- * exchangeName: "binance"
36224
- * });
36225
- *
36226
- * // Using instance API (no context needed, exchange set in constructor)
36227
- * const binance = new ExchangeInstance("binance");
36228
- * const candles2 = await binance.getCandles("BTCUSDT", "1m", 100);
36229
- * const vwap2 = await binance.getAveragePrice("BTCUSDT");
36230
- * ```
36351
+ * Global singleton instance of NotificationAdapter.
36352
+ * Provides unified notification management for backtest and live trading.
36231
36353
  */
36232
- const Exchange = new ExchangeUtils();
36354
+ const Notification = new NotificationAdapter();
36355
+ /**
36356
+ * Global singleton instance of NotificationLiveAdapter.
36357
+ * Provides live trading notification storage with pluggable backends.
36358
+ */
36359
+ const NotificationLive = new NotificationLiveAdapter();
36360
+ /**
36361
+ * Global singleton instance of NotificationBacktestAdapter.
36362
+ * Provides backtest notification storage with pluggable backends.
36363
+ */
36364
+ const NotificationBacktest = new NotificationBacktestAdapter();
36233
36365
 
36234
36366
  const CACHE_METHOD_NAME_FLUSH = "CacheUtils.flush";
36235
36367
  const CACHE_METHOD_NAME_CLEAR = "CacheInstance.clear";
@@ -37161,4 +37293,4 @@ const set = (object, path, value) => {
37161
37293
  }
37162
37294
  };
37163
37295
 
37164
- 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, commitActivateScheduled, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, 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 };
37296
+ 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, checkCandles, commitActivateScheduled, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, 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, warmCandles };