backtest-kit 3.0.10 → 3.0.12

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