backtest-kit 3.0.9 → 3.0.11

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