backtest-kit 6.8.1 → 6.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -888,7 +888,7 @@ const LOGGER_SERVICE$7 = new LoggerService();
888
888
  /** Symbol key for the singleshot waitForInit function on PersistBase instances. */
889
889
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
890
890
  // Calculate step in milliseconds for candle close time validation
891
- const INTERVAL_MINUTES$8 = {
891
+ const INTERVAL_MINUTES$9 = {
892
892
  "1m": 1,
893
893
  "3m": 3,
894
894
  "5m": 5,
@@ -902,7 +902,7 @@ const INTERVAL_MINUTES$8 = {
902
902
  "1d": 1440,
903
903
  "1w": 10080,
904
904
  };
905
- const MS_PER_MINUTE$6 = 60000;
905
+ const MS_PER_MINUTE$7 = 60000;
906
906
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
907
907
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
908
908
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
@@ -961,8 +961,18 @@ const PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA = "PersistMeasureUtils.readMea
961
961
  const PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA = "PersistMeasureUtils.writeMeasureData";
962
962
  const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON = "PersistMeasureUtils.useJson";
963
963
  const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY = "PersistMeasureUtils.useDummy";
964
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA = "PersistMeasureUtils.removeMeasureData";
965
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA = "PersistMeasureUtils.listMeasureData";
964
966
  const PERSIST_MEASURE_UTILS_METHOD_NAME_CLEAR = "PersistMeasureUtils.clear";
965
967
  const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER = "PersistMeasureUtils.usePersistMeasureAdapter";
968
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA = "PersistIntervalUtils.readIntervalData";
969
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistIntervalUtils.writeIntervalData";
970
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON = "PersistIntervalUtils.useJson";
971
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY = "PersistIntervalUtils.useDummy";
972
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA = "PersistIntervalUtils.removeIntervalData";
973
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA = "PersistIntervalUtils.listIntervalData";
974
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR = "PersistIntervalUtils.clear";
975
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER = "PersistIntervalUtils.usePersistIntervalAdapter";
966
976
  const PERSIST_CANDLE_UTILS_METHOD_NAME_CLEAR = "PersistCandleUtils.clear";
967
977
  const PERSIST_MEMORY_UTILS_METHOD_NAME_USE_PERSIST_MEMORY_ADAPTER = "PersistMemoryUtils.usePersistMemoryAdapter";
968
978
  const PERSIST_MEMORY_UTILS_METHOD_NAME_READ_DATA = "PersistMemoryUtils.readMemoryData";
@@ -1875,7 +1885,7 @@ class PersistCandleUtils {
1875
1885
  const isInitial = !this.getCandlesStorage.has(key);
1876
1886
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1877
1887
  await stateStorage.waitForInit(isInitial);
1878
- const stepMs = INTERVAL_MINUTES$8[interval] * MS_PER_MINUTE$6;
1888
+ const stepMs = INTERVAL_MINUTES$9[interval] * MS_PER_MINUTE$7;
1879
1889
  // Calculate expected timestamps and fetch each candle directly
1880
1890
  const cachedCandles = [];
1881
1891
  for (let i = 0; i < limit; i++) {
@@ -1931,7 +1941,7 @@ class PersistCandleUtils {
1931
1941
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1932
1942
  await stateStorage.waitForInit(isInitial);
1933
1943
  // Calculate step in milliseconds to determine candle close time
1934
- const stepMs = INTERVAL_MINUTES$8[interval] * MS_PER_MINUTE$6;
1944
+ const stepMs = INTERVAL_MINUTES$9[interval] * MS_PER_MINUTE$7;
1935
1945
  const now = Date.now();
1936
1946
  // Write each candle as a separate file, skipping incomplete candles
1937
1947
  for (const candle of candles) {
@@ -2357,7 +2367,8 @@ class PersistMeasureUtils {
2357
2367
  const stateStorage = this.getMeasureStorage(bucket);
2358
2368
  await stateStorage.waitForInit(isInitial);
2359
2369
  if (await stateStorage.hasValue(key)) {
2360
- return await stateStorage.readValue(key);
2370
+ const data = await stateStorage.readValue(key);
2371
+ return data.removed ? null : data;
2361
2372
  }
2362
2373
  return null;
2363
2374
  };
@@ -2379,6 +2390,27 @@ class PersistMeasureUtils {
2379
2390
  await stateStorage.waitForInit(isInitial);
2380
2391
  await stateStorage.writeValue(key, data);
2381
2392
  };
2393
+ /**
2394
+ * Marks a cached entry as removed (soft delete — file is kept on disk).
2395
+ * After this call `readMeasureData` for the same key returns `null`.
2396
+ *
2397
+ * @param bucket - Storage bucket
2398
+ * @param key - Dynamic cache key within the bucket
2399
+ * @returns Promise that resolves when removal is complete
2400
+ */
2401
+ this.removeMeasureData = async (bucket, key) => {
2402
+ LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA, {
2403
+ bucket,
2404
+ key,
2405
+ });
2406
+ const isInitial = !this.getMeasureStorage.has(bucket);
2407
+ const stateStorage = this.getMeasureStorage(bucket);
2408
+ await stateStorage.waitForInit(isInitial);
2409
+ const data = await stateStorage.readValue(key);
2410
+ if (data) {
2411
+ await stateStorage.writeValue(key, Object.assign({}, data, { removed: true }));
2412
+ }
2413
+ };
2382
2414
  }
2383
2415
  /**
2384
2416
  * Registers a custom persistence adapter.
@@ -2389,6 +2421,27 @@ class PersistMeasureUtils {
2389
2421
  LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
2390
2422
  this.PersistMeasureFactory = Ctor;
2391
2423
  }
2424
+ /**
2425
+ * Async generator yielding all non-removed entity keys for a given bucket.
2426
+ * Used by `CacheFileInstance.clear()` to iterate and soft-delete all entries.
2427
+ *
2428
+ * @param bucket - Storage bucket
2429
+ * @returns AsyncGenerator yielding entity keys
2430
+ */
2431
+ async *listMeasureData(bucket) {
2432
+ LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA, { bucket });
2433
+ const isInitial = !this.getMeasureStorage.has(bucket);
2434
+ const stateStorage = this.getMeasureStorage(bucket);
2435
+ await stateStorage.waitForInit(isInitial);
2436
+ for await (const key of stateStorage.keys()) {
2437
+ const data = await stateStorage.readValue(String(key));
2438
+ if (data === null || data.removed) {
2439
+ continue;
2440
+ }
2441
+ yield String(key);
2442
+ }
2443
+ }
2444
+ ;
2392
2445
  /**
2393
2446
  * Clears the memoized storage cache.
2394
2447
  * Call this when process.cwd() changes between strategy iterations
@@ -2418,6 +2471,140 @@ class PersistMeasureUtils {
2418
2471
  * Used by Cache.file for persistent caching of external API responses.
2419
2472
  */
2420
2473
  const PersistMeasureAdapter = new PersistMeasureUtils();
2474
+ /**
2475
+ * Persistence layer for Interval.file once-per-interval signal firing.
2476
+ *
2477
+ * Stores fired-interval markers under `./dump/data/interval/`.
2478
+ * A record's presence means the interval has already fired for that bucket+key;
2479
+ * absence means the function has not yet fired (or returned null last time).
2480
+ */
2481
+ class PersistIntervalUtils {
2482
+ constructor() {
2483
+ this.PersistIntervalFactory = PersistBase;
2484
+ this.getIntervalStorage = memoize(([bucket]) => bucket, (bucket) => Reflect.construct(this.PersistIntervalFactory, [
2485
+ bucket,
2486
+ `./dump/data/interval/`,
2487
+ ]));
2488
+ /**
2489
+ * Reads interval data for a given bucket and key.
2490
+ *
2491
+ * @param bucket - Storage bucket (instance name + interval + index)
2492
+ * @param key - Entity key within the bucket (symbol + aligned timestamp)
2493
+ * @returns Promise resolving to stored value or null if not found
2494
+ */
2495
+ this.readIntervalData = async (bucket, key) => {
2496
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA, {
2497
+ bucket,
2498
+ key,
2499
+ });
2500
+ const isInitial = !this.getIntervalStorage.has(bucket);
2501
+ const stateStorage = this.getIntervalStorage(bucket);
2502
+ await stateStorage.waitForInit(isInitial);
2503
+ if (await stateStorage.hasValue(key)) {
2504
+ const data = await stateStorage.readValue(key);
2505
+ return data.removed ? null : data;
2506
+ }
2507
+ return null;
2508
+ };
2509
+ /**
2510
+ * Writes interval data to disk.
2511
+ *
2512
+ * @param data - Data to store
2513
+ * @param bucket - Storage bucket
2514
+ * @param key - Entity key within the bucket
2515
+ * @returns Promise that resolves when write is complete
2516
+ */
2517
+ this.writeIntervalData = async (data, bucket, key) => {
2518
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, {
2519
+ bucket,
2520
+ key,
2521
+ });
2522
+ const isInitial = !this.getIntervalStorage.has(bucket);
2523
+ const stateStorage = this.getIntervalStorage(bucket);
2524
+ await stateStorage.waitForInit(isInitial);
2525
+ await stateStorage.writeValue(key, data);
2526
+ };
2527
+ /**
2528
+ * Marks an interval entry as removed (soft delete — file is kept on disk).
2529
+ * After this call `readIntervalData` for the same key returns `null`,
2530
+ * so the function will fire again on the next `IntervalFileInstance.run` call.
2531
+ *
2532
+ * @param bucket - Storage bucket
2533
+ * @param key - Entity key within the bucket
2534
+ * @returns Promise that resolves when removal is complete
2535
+ */
2536
+ this.removeIntervalData = async (bucket, key) => {
2537
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA, {
2538
+ bucket,
2539
+ key,
2540
+ });
2541
+ const isInitial = !this.getIntervalStorage.has(bucket);
2542
+ const stateStorage = this.getIntervalStorage(bucket);
2543
+ await stateStorage.waitForInit(isInitial);
2544
+ const data = await stateStorage.readValue(key);
2545
+ if (data) {
2546
+ await stateStorage.writeValue(key, Object.assign({}, data, { removed: true }));
2547
+ }
2548
+ };
2549
+ }
2550
+ /**
2551
+ * Registers a custom persistence adapter.
2552
+ *
2553
+ * @param Ctor - Custom PersistBase constructor
2554
+ */
2555
+ usePersistIntervalAdapter(Ctor) {
2556
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER);
2557
+ this.PersistIntervalFactory = Ctor;
2558
+ }
2559
+ /**
2560
+ * Async generator yielding all non-removed entity keys for a given bucket.
2561
+ * Used by `IntervalFileInstance.clear()` to iterate and soft-delete all entries.
2562
+ *
2563
+ * @param bucket - Storage bucket
2564
+ * @returns AsyncGenerator yielding entity keys
2565
+ */
2566
+ async *listIntervalData(bucket) {
2567
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA, { bucket });
2568
+ const isInitial = !this.getIntervalStorage.has(bucket);
2569
+ const stateStorage = this.getIntervalStorage(bucket);
2570
+ await stateStorage.waitForInit(isInitial);
2571
+ for await (const key of stateStorage.keys()) {
2572
+ const data = await stateStorage.readValue(String(key));
2573
+ if (data === null || data.removed) {
2574
+ continue;
2575
+ }
2576
+ yield String(key);
2577
+ }
2578
+ }
2579
+ ;
2580
+ /**
2581
+ * Clears the memoized storage cache.
2582
+ * Call this when process.cwd() changes between strategy iterations.
2583
+ */
2584
+ clear() {
2585
+ LOGGER_SERVICE$7.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR);
2586
+ this.getIntervalStorage.clear();
2587
+ }
2588
+ /**
2589
+ * Switches to the default JSON persist adapter.
2590
+ */
2591
+ useJson() {
2592
+ LOGGER_SERVICE$7.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON);
2593
+ this.usePersistIntervalAdapter(PersistBase);
2594
+ }
2595
+ /**
2596
+ * Switches to a dummy persist adapter that discards all writes.
2597
+ */
2598
+ useDummy() {
2599
+ LOGGER_SERVICE$7.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY);
2600
+ this.usePersistIntervalAdapter(PersistDummy);
2601
+ }
2602
+ }
2603
+ /**
2604
+ * Global singleton instance of PersistIntervalUtils.
2605
+ * Used by Interval.file for persistent once-per-interval signal firing.
2606
+ */
2607
+ const PersistIntervalAdapter = new PersistIntervalUtils();
2421
2608
  /**
2422
2609
  * Utility class for managing memory entry persistence.
2423
2610
  *
@@ -2765,8 +2952,8 @@ class CandleUtils {
2765
2952
  }
2766
2953
  const Candle = new CandleUtils();
2767
2954
 
2768
- const MS_PER_MINUTE$5 = 60000;
2769
- const INTERVAL_MINUTES$7 = {
2955
+ const MS_PER_MINUTE$6 = 60000;
2956
+ const INTERVAL_MINUTES$8 = {
2770
2957
  "1m": 1,
2771
2958
  "3m": 3,
2772
2959
  "5m": 5,
@@ -2798,7 +2985,7 @@ const INTERVAL_MINUTES$7 = {
2798
2985
  * @returns Aligned timestamp rounded down to interval boundary
2799
2986
  */
2800
2987
  const ALIGN_TO_INTERVAL_FN$2 = (timestamp, intervalMinutes) => {
2801
- const intervalMs = intervalMinutes * MS_PER_MINUTE$5;
2988
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$6;
2802
2989
  return Math.floor(timestamp / intervalMs) * intervalMs;
2803
2990
  };
2804
2991
  /**
@@ -2952,9 +3139,9 @@ const WRITE_CANDLES_CACHE_FN$1 = trycatch(queued(async (candles, dto, self) => {
2952
3139
  * @returns Promise resolving to array of candle data
2953
3140
  */
2954
3141
  const GET_CANDLES_FN$1 = async (dto, since, self) => {
2955
- const step = INTERVAL_MINUTES$7[dto.interval];
3142
+ const step = INTERVAL_MINUTES$8[dto.interval];
2956
3143
  const sinceTimestamp = since.getTime();
2957
- const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$5;
3144
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$6;
2958
3145
  await Candle.acquireLock(`ClientExchange GET_CANDLES_FN symbol=${dto.symbol} interval=${dto.interval} limit=${dto.limit}`);
2959
3146
  try {
2960
3147
  // Try to read from cache first
@@ -3068,11 +3255,11 @@ class ClientExchange {
3068
3255
  interval,
3069
3256
  limit,
3070
3257
  });
3071
- const step = INTERVAL_MINUTES$7[interval];
3258
+ const step = INTERVAL_MINUTES$8[interval];
3072
3259
  if (!step) {
3073
3260
  throw new Error(`ClientExchange unknown interval=${interval}`);
3074
3261
  }
3075
- const stepMs = step * MS_PER_MINUTE$5;
3262
+ const stepMs = step * MS_PER_MINUTE$6;
3076
3263
  // Align when down to interval boundary
3077
3264
  const whenTimestamp = this.params.execution.context.when.getTime();
3078
3265
  const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
@@ -3149,11 +3336,11 @@ class ClientExchange {
3149
3336
  if (!this.params.execution.context.backtest) {
3150
3337
  throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
3151
3338
  }
3152
- const step = INTERVAL_MINUTES$7[interval];
3339
+ const step = INTERVAL_MINUTES$8[interval];
3153
3340
  if (!step) {
3154
3341
  throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
3155
3342
  }
3156
- const stepMs = step * MS_PER_MINUTE$5;
3343
+ const stepMs = step * MS_PER_MINUTE$6;
3157
3344
  const now = Date.now();
3158
3345
  // Align when down to interval boundary
3159
3346
  const whenTimestamp = this.params.execution.context.when.getTime();
@@ -3317,11 +3504,11 @@ class ClientExchange {
3317
3504
  sDate,
3318
3505
  eDate,
3319
3506
  });
3320
- const step = INTERVAL_MINUTES$7[interval];
3507
+ const step = INTERVAL_MINUTES$8[interval];
3321
3508
  if (!step) {
3322
3509
  throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
3323
3510
  }
3324
- const stepMs = step * MS_PER_MINUTE$5;
3511
+ const stepMs = step * MS_PER_MINUTE$6;
3325
3512
  const whenTimestamp = this.params.execution.context.when.getTime();
3326
3513
  const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
3327
3514
  let sinceTimestamp;
@@ -3450,7 +3637,7 @@ class ClientExchange {
3450
3637
  const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
3451
3638
  const to = new Date(alignedTo);
3452
3639
  const from = new Date(alignedTo -
3453
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$5);
3640
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$6);
3454
3641
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
3455
3642
  }
3456
3643
  /**
@@ -3479,7 +3666,7 @@ class ClientExchange {
3479
3666
  const whenTimestamp = this.params.execution.context.when.getTime();
3480
3667
  // Align to 1-minute boundary to prevent look-ahead bias
3481
3668
  const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, 1);
3482
- const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$5 - MS_PER_MINUTE$5;
3669
+ const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$6 - MS_PER_MINUTE$6;
3483
3670
  // No limit: fetch a single window and return as-is
3484
3671
  if (limit === undefined) {
3485
3672
  const to = new Date(alignedTo);
@@ -4458,7 +4645,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
4458
4645
  }
4459
4646
  };
4460
4647
 
4461
- const INTERVAL_MINUTES$6 = {
4648
+ const INTERVAL_MINUTES$7 = {
4462
4649
  "1m": 1,
4463
4650
  "3m": 3,
4464
4651
  "5m": 5,
@@ -4842,7 +5029,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
4842
5029
  }
4843
5030
  const currentTime = self.params.execution.context.when.getTime();
4844
5031
  {
4845
- const intervalMinutes = INTERVAL_MINUTES$6[self.params.interval];
5032
+ const intervalMinutes = INTERVAL_MINUTES$7[self.params.interval];
4846
5033
  const intervalMs = intervalMinutes * 60 * 1000;
4847
5034
  const alignedTime = Math.floor(currentTime / intervalMs) * intervalMs;
4848
5035
  // Проверяем что наступил новый интервал (по aligned timestamp)
@@ -7783,6 +7970,90 @@ class ClientStrategy {
7783
7970
  }
7784
7971
  return this._pendingSignal._fall.pnlCost;
7785
7972
  }
7973
+ /**
7974
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
7975
+ *
7976
+ * Measures how much PnL% the position has given back from its best point.
7977
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
7978
+ * Zero when called at the exact moment the peak was set, or when current PnL >= peak PnL.
7979
+ *
7980
+ * Returns null if no pending signal exists.
7981
+ *
7982
+ * @param symbol - Trading pair symbol
7983
+ * @param currentPrice - Current market price
7984
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
7985
+ */
7986
+ async getPositionHighestProfitDistancePnlPercentage(symbol, currentPrice) {
7987
+ this.params.logger.debug("ClientStrategy getPositionHighestProfitDistancePnlPercentage", { symbol, currentPrice });
7988
+ if (!this._pendingSignal) {
7989
+ return null;
7990
+ }
7991
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
7992
+ return Math.max(0, this._pendingSignal._peak.pnlPercentage - currentPnl.pnlPercentage);
7993
+ }
7994
+ /**
7995
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
7996
+ *
7997
+ * Measures how much PnL cost the position has given back from its best point.
7998
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
7999
+ * Zero when called at the exact moment the peak was set, or when current PnL >= peak PnL.
8000
+ *
8001
+ * Returns null if no pending signal exists.
8002
+ *
8003
+ * @param symbol - Trading pair symbol
8004
+ * @param currentPrice - Current market price
8005
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
8006
+ */
8007
+ async getPositionHighestProfitDistancePnlCost(symbol, currentPrice) {
8008
+ this.params.logger.debug("ClientStrategy getPositionHighestProfitDistancePnlCost", { symbol, currentPrice });
8009
+ if (!this._pendingSignal) {
8010
+ return null;
8011
+ }
8012
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
8013
+ return Math.max(0, this._pendingSignal._peak.pnlCost - currentPnl.pnlCost);
8014
+ }
8015
+ /**
8016
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
8017
+ *
8018
+ * Measures how much the position has recovered from its deepest loss point.
8019
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
8020
+ * Zero when called at the exact moment the trough was set, or when current PnL <= trough PnL.
8021
+ *
8022
+ * Returns null if no pending signal exists.
8023
+ *
8024
+ * @param symbol - Trading pair symbol
8025
+ * @param currentPrice - Current market price
8026
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
8027
+ */
8028
+ async getPositionHighestMaxDrawdownPnlPercentage(symbol, currentPrice) {
8029
+ this.params.logger.debug("ClientStrategy getPositionHighestMaxDrawdownPnlPercentage", { symbol, currentPrice });
8030
+ if (!this._pendingSignal) {
8031
+ return null;
8032
+ }
8033
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
8034
+ return Math.max(0, currentPnl.pnlPercentage - this._pendingSignal._fall.pnlPercentage);
8035
+ }
8036
+ /**
8037
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
8038
+ *
8039
+ * Measures how much the position has recovered from its deepest loss point.
8040
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
8041
+ * Zero when called at the exact moment the trough was set, or when current PnL <= trough PnL.
8042
+ *
8043
+ * Returns null if no pending signal exists.
8044
+ *
8045
+ * @param symbol - Trading pair symbol
8046
+ * @param currentPrice - Current market price
8047
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
8048
+ */
8049
+ async getPositionHighestMaxDrawdownPnlCost(symbol, currentPrice) {
8050
+ this.params.logger.debug("ClientStrategy getPositionHighestMaxDrawdownPnlCost", { symbol, currentPrice });
8051
+ if (!this._pendingSignal) {
8052
+ return null;
8053
+ }
8054
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
8055
+ return Math.max(0, currentPnl.pnlCost - this._pendingSignal._fall.pnlCost);
8056
+ }
7786
8057
  /**
7787
8058
  * Performs a single tick of strategy execution.
7788
8059
  *
@@ -9866,7 +10137,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
9866
10137
  * @param backtest - Whether running in backtest mode
9867
10138
  * @returns Unique string key for memoization
9868
10139
  */
9869
- const CREATE_KEY_FN$s = (symbol, strategyName, exchangeName, frameName, backtest) => {
10140
+ const CREATE_KEY_FN$t = (symbol, strategyName, exchangeName, frameName, backtest) => {
9870
10141
  const parts = [symbol, strategyName, exchangeName];
9871
10142
  if (frameName)
9872
10143
  parts.push(frameName);
@@ -10133,7 +10404,7 @@ class StrategyConnectionService {
10133
10404
  * @param backtest - Whether running in backtest mode
10134
10405
  * @returns Configured ClientStrategy instance
10135
10406
  */
10136
- this.getStrategy = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$s(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
10407
+ this.getStrategy = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$t(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
10137
10408
  const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
10138
10409
  return new ClientStrategy({
10139
10410
  symbol,
@@ -10890,6 +11161,90 @@ class StrategyConnectionService {
10890
11161
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
10891
11162
  return await strategy.getPositionMaxDrawdownPnlCost(symbol);
10892
11163
  };
11164
+ /**
11165
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
11166
+ *
11167
+ * Resolves current price via priceMetaService and delegates to
11168
+ * ClientStrategy.getPositionHighestProfitDistancePnlPercentage().
11169
+ * Returns null if no pending signal exists.
11170
+ *
11171
+ * @param backtest - Whether running in backtest mode
11172
+ * @param symbol - Trading pair symbol
11173
+ * @param context - Execution context with strategyName, exchangeName, frameName
11174
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
11175
+ */
11176
+ this.getPositionHighestProfitDistancePnlPercentage = async (backtest, symbol, context) => {
11177
+ this.loggerService.log("strategyConnectionService getPositionHighestProfitDistancePnlPercentage", {
11178
+ symbol,
11179
+ context,
11180
+ });
11181
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11182
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11183
+ return await strategy.getPositionHighestProfitDistancePnlPercentage(symbol, currentPrice);
11184
+ };
11185
+ /**
11186
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
11187
+ *
11188
+ * Resolves current price via priceMetaService and delegates to
11189
+ * ClientStrategy.getPositionHighestProfitDistancePnlCost().
11190
+ * Returns null if no pending signal exists.
11191
+ *
11192
+ * @param backtest - Whether running in backtest mode
11193
+ * @param symbol - Trading pair symbol
11194
+ * @param context - Execution context with strategyName, exchangeName, frameName
11195
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
11196
+ */
11197
+ this.getPositionHighestProfitDistancePnlCost = async (backtest, symbol, context) => {
11198
+ this.loggerService.log("strategyConnectionService getPositionHighestProfitDistancePnlCost", {
11199
+ symbol,
11200
+ context,
11201
+ });
11202
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11203
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11204
+ return await strategy.getPositionHighestProfitDistancePnlCost(symbol, currentPrice);
11205
+ };
11206
+ /**
11207
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
11208
+ *
11209
+ * Resolves current price via priceMetaService and delegates to
11210
+ * ClientStrategy.getPositionHighestMaxDrawdownPnlPercentage().
11211
+ * Returns null if no pending signal exists.
11212
+ *
11213
+ * @param backtest - Whether running in backtest mode
11214
+ * @param symbol - Trading pair symbol
11215
+ * @param context - Execution context with strategyName, exchangeName, frameName
11216
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
11217
+ */
11218
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (backtest, symbol, context) => {
11219
+ this.loggerService.log("strategyConnectionService getPositionHighestMaxDrawdownPnlPercentage", {
11220
+ symbol,
11221
+ context,
11222
+ });
11223
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11224
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11225
+ return await strategy.getPositionHighestMaxDrawdownPnlPercentage(symbol, currentPrice);
11226
+ };
11227
+ /**
11228
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
11229
+ *
11230
+ * Resolves current price via priceMetaService and delegates to
11231
+ * ClientStrategy.getPositionHighestMaxDrawdownPnlCost().
11232
+ * Returns null if no pending signal exists.
11233
+ *
11234
+ * @param backtest - Whether running in backtest mode
11235
+ * @param symbol - Trading pair symbol
11236
+ * @param context - Execution context with strategyName, exchangeName, frameName
11237
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
11238
+ */
11239
+ this.getPositionHighestMaxDrawdownPnlCost = async (backtest, symbol, context) => {
11240
+ this.loggerService.log("strategyConnectionService getPositionHighestMaxDrawdownPnlCost", {
11241
+ symbol,
11242
+ context,
11243
+ });
11244
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11245
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11246
+ return await strategy.getPositionHighestMaxDrawdownPnlCost(symbol, currentPrice);
11247
+ };
10893
11248
  /**
10894
11249
  * Disposes the ClientStrategy instance for the given context.
10895
11250
  *
@@ -10928,7 +11283,7 @@ class StrategyConnectionService {
10928
11283
  }
10929
11284
  return;
10930
11285
  }
10931
- const key = CREATE_KEY_FN$s(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
11286
+ const key = CREATE_KEY_FN$t(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
10932
11287
  if (!this.getStrategy.has(key)) {
10933
11288
  return;
10934
11289
  }
@@ -11330,7 +11685,7 @@ class StrategyConnectionService {
11330
11685
  * Maps FrameInterval to minutes for timestamp calculation.
11331
11686
  * Used to generate timeframe arrays with proper spacing.
11332
11687
  */
11333
- const INTERVAL_MINUTES$5 = {
11688
+ const INTERVAL_MINUTES$6 = {
11334
11689
  "1m": 1,
11335
11690
  "3m": 3,
11336
11691
  "5m": 5,
@@ -11386,7 +11741,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
11386
11741
  symbol,
11387
11742
  });
11388
11743
  const { interval, startDate, endDate } = self.params;
11389
- const intervalMinutes = INTERVAL_MINUTES$5[interval];
11744
+ const intervalMinutes = INTERVAL_MINUTES$6[interval];
11390
11745
  if (!intervalMinutes) {
11391
11746
  throw new Error(`ClientFrame unknown interval: ${interval}`);
11392
11747
  }
@@ -11751,8 +12106,8 @@ const get = (object, path) => {
11751
12106
  return pathArrayFlat.reduce((obj, key) => obj && obj[key], object);
11752
12107
  };
11753
12108
 
11754
- const MS_PER_MINUTE$4 = 60000;
11755
- const INTERVAL_MINUTES$4 = {
12109
+ const MS_PER_MINUTE$5 = 60000;
12110
+ const INTERVAL_MINUTES$5 = {
11756
12111
  "1m": 1,
11757
12112
  "3m": 3,
11758
12113
  "5m": 5,
@@ -11784,11 +12139,11 @@ const INTERVAL_MINUTES$4 = {
11784
12139
  * @returns New Date aligned down to interval boundary
11785
12140
  */
11786
12141
  const alignToInterval = (date, interval) => {
11787
- const minutes = INTERVAL_MINUTES$4[interval];
12142
+ const minutes = INTERVAL_MINUTES$5[interval];
11788
12143
  if (minutes === undefined) {
11789
12144
  throw new Error(`alignToInterval: unknown interval=${interval}`);
11790
12145
  }
11791
- const intervalMs = minutes * MS_PER_MINUTE$4;
12146
+ const intervalMs = minutes * MS_PER_MINUTE$5;
11792
12147
  return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
11793
12148
  };
11794
12149
 
@@ -12097,7 +12452,7 @@ class ClientRisk {
12097
12452
  * @param backtest - Whether running in backtest mode
12098
12453
  * @returns Unique string key for memoization
12099
12454
  */
12100
- const CREATE_KEY_FN$r = (riskName, exchangeName, frameName, backtest) => {
12455
+ const CREATE_KEY_FN$s = (riskName, exchangeName, frameName, backtest) => {
12101
12456
  const parts = [riskName, exchangeName];
12102
12457
  if (frameName)
12103
12458
  parts.push(frameName);
@@ -12197,7 +12552,7 @@ class RiskConnectionService {
12197
12552
  * @param backtest - True if backtest mode, false if live mode
12198
12553
  * @returns Configured ClientRisk instance
12199
12554
  */
12200
- this.getRisk = memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$r(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
12555
+ this.getRisk = memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$s(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
12201
12556
  const schema = this.riskSchemaService.get(riskName);
12202
12557
  return new ClientRisk({
12203
12558
  ...schema,
@@ -12266,7 +12621,7 @@ class RiskConnectionService {
12266
12621
  payload,
12267
12622
  });
12268
12623
  if (payload) {
12269
- const key = CREATE_KEY_FN$r(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
12624
+ const key = CREATE_KEY_FN$s(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
12270
12625
  this.getRisk.clear(key);
12271
12626
  }
12272
12627
  else {
@@ -13310,7 +13665,7 @@ class ClientAction {
13310
13665
  * @param backtest - Whether running in backtest mode
13311
13666
  * @returns Unique string key for memoization
13312
13667
  */
13313
- const CREATE_KEY_FN$q = (actionName, strategyName, exchangeName, frameName, backtest) => {
13668
+ const CREATE_KEY_FN$r = (actionName, strategyName, exchangeName, frameName, backtest) => {
13314
13669
  const parts = [actionName, strategyName, exchangeName];
13315
13670
  if (frameName)
13316
13671
  parts.push(frameName);
@@ -13362,7 +13717,7 @@ class ActionConnectionService {
13362
13717
  * @param backtest - True if backtest mode, false if live mode
13363
13718
  * @returns Configured ClientAction instance
13364
13719
  */
13365
- this.getAction = memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$q(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
13720
+ this.getAction = memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$r(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
13366
13721
  const schema = this.actionSchemaService.get(actionName);
13367
13722
  return new ClientAction({
13368
13723
  ...schema,
@@ -13573,7 +13928,7 @@ class ActionConnectionService {
13573
13928
  await Promise.all(actions.map(async (action) => await action.dispose()));
13574
13929
  return;
13575
13930
  }
13576
- const key = CREATE_KEY_FN$q(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13931
+ const key = CREATE_KEY_FN$r(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13577
13932
  if (!this.getAction.has(key)) {
13578
13933
  return;
13579
13934
  }
@@ -13591,7 +13946,7 @@ const METHOD_NAME_VALIDATE$2 = "exchangeCoreService validate";
13591
13946
  * @param exchangeName - Exchange name
13592
13947
  * @returns Unique string key for memoization
13593
13948
  */
13594
- const CREATE_KEY_FN$p = (exchangeName) => {
13949
+ const CREATE_KEY_FN$q = (exchangeName) => {
13595
13950
  return exchangeName;
13596
13951
  };
13597
13952
  /**
@@ -13615,7 +13970,7 @@ class ExchangeCoreService {
13615
13970
  * @param exchangeName - Name of the exchange to validate
13616
13971
  * @returns Promise that resolves when validation is complete
13617
13972
  */
13618
- this.validate = memoize(([exchangeName]) => CREATE_KEY_FN$p(exchangeName), async (exchangeName) => {
13973
+ this.validate = memoize(([exchangeName]) => CREATE_KEY_FN$q(exchangeName), async (exchangeName) => {
13619
13974
  this.loggerService.log(METHOD_NAME_VALIDATE$2, {
13620
13975
  exchangeName,
13621
13976
  });
@@ -13867,7 +14222,7 @@ const METHOD_NAME_VALIDATE$1 = "strategyCoreService validate";
13867
14222
  * @param context - Execution context with strategyName, exchangeName, frameName
13868
14223
  * @returns Unique string key for memoization
13869
14224
  */
13870
- const CREATE_KEY_FN$o = (context) => {
14225
+ const CREATE_KEY_FN$p = (context) => {
13871
14226
  const parts = [context.strategyName, context.exchangeName];
13872
14227
  if (context.frameName)
13873
14228
  parts.push(context.frameName);
@@ -13899,7 +14254,7 @@ class StrategyCoreService {
13899
14254
  * @param context - Execution context with strategyName, exchangeName, frameName
13900
14255
  * @returns Promise that resolves when validation is complete
13901
14256
  */
13902
- this.validate = memoize(([context]) => CREATE_KEY_FN$o(context), async (context) => {
14257
+ this.validate = memoize(([context]) => CREATE_KEY_FN$p(context), async (context) => {
13903
14258
  this.loggerService.log(METHOD_NAME_VALIDATE$1, {
13904
14259
  context,
13905
14260
  });
@@ -15051,6 +15406,82 @@ class StrategyCoreService {
15051
15406
  await this.validate(context);
15052
15407
  return await this.strategyConnectionService.getPositionMaxDrawdownPnlCost(backtest, symbol, context);
15053
15408
  };
15409
+ /**
15410
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
15411
+ *
15412
+ * Delegates to StrategyConnectionService.getPositionHighestProfitDistancePnlPercentage().
15413
+ * Returns null if no pending signal exists.
15414
+ *
15415
+ * @param backtest - Whether running in backtest mode
15416
+ * @param symbol - Trading pair symbol
15417
+ * @param context - Execution context with strategyName, exchangeName, frameName
15418
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
15419
+ */
15420
+ this.getPositionHighestProfitDistancePnlPercentage = async (backtest, symbol, context) => {
15421
+ this.loggerService.log("strategyCoreService getPositionHighestProfitDistancePnlPercentage", {
15422
+ symbol,
15423
+ context,
15424
+ });
15425
+ await this.validate(context);
15426
+ return await this.strategyConnectionService.getPositionHighestProfitDistancePnlPercentage(backtest, symbol, context);
15427
+ };
15428
+ /**
15429
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
15430
+ *
15431
+ * Delegates to StrategyConnectionService.getPositionHighestProfitDistancePnlCost().
15432
+ * Returns null if no pending signal exists.
15433
+ *
15434
+ * @param backtest - Whether running in backtest mode
15435
+ * @param symbol - Trading pair symbol
15436
+ * @param context - Execution context with strategyName, exchangeName, frameName
15437
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
15438
+ */
15439
+ this.getPositionHighestProfitDistancePnlCost = async (backtest, symbol, context) => {
15440
+ this.loggerService.log("strategyCoreService getPositionHighestProfitDistancePnlCost", {
15441
+ symbol,
15442
+ context,
15443
+ });
15444
+ await this.validate(context);
15445
+ return await this.strategyConnectionService.getPositionHighestProfitDistancePnlCost(backtest, symbol, context);
15446
+ };
15447
+ /**
15448
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
15449
+ *
15450
+ * Delegates to StrategyConnectionService.getPositionHighestMaxDrawdownPnlPercentage().
15451
+ * Returns null if no pending signal exists.
15452
+ *
15453
+ * @param backtest - Whether running in backtest mode
15454
+ * @param symbol - Trading pair symbol
15455
+ * @param context - Execution context with strategyName, exchangeName, frameName
15456
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
15457
+ */
15458
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (backtest, symbol, context) => {
15459
+ this.loggerService.log("strategyCoreService getPositionHighestMaxDrawdownPnlPercentage", {
15460
+ symbol,
15461
+ context,
15462
+ });
15463
+ await this.validate(context);
15464
+ return await this.strategyConnectionService.getPositionHighestMaxDrawdownPnlPercentage(backtest, symbol, context);
15465
+ };
15466
+ /**
15467
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
15468
+ *
15469
+ * Delegates to StrategyConnectionService.getPositionHighestMaxDrawdownPnlCost().
15470
+ * Returns null if no pending signal exists.
15471
+ *
15472
+ * @param backtest - Whether running in backtest mode
15473
+ * @param symbol - Trading pair symbol
15474
+ * @param context - Execution context with strategyName, exchangeName, frameName
15475
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
15476
+ */
15477
+ this.getPositionHighestMaxDrawdownPnlCost = async (backtest, symbol, context) => {
15478
+ this.loggerService.log("strategyCoreService getPositionHighestMaxDrawdownPnlCost", {
15479
+ symbol,
15480
+ context,
15481
+ });
15482
+ await this.validate(context);
15483
+ return await this.strategyConnectionService.getPositionHighestMaxDrawdownPnlCost(backtest, symbol, context);
15484
+ };
15054
15485
  }
15055
15486
  }
15056
15487
 
@@ -15123,7 +15554,7 @@ class SizingGlobalService {
15123
15554
  * @param context - Context with riskName, exchangeName, frameName
15124
15555
  * @returns Unique string key for memoization
15125
15556
  */
15126
- const CREATE_KEY_FN$n = (context) => {
15557
+ const CREATE_KEY_FN$o = (context) => {
15127
15558
  const parts = [context.riskName, context.exchangeName];
15128
15559
  if (context.frameName)
15129
15560
  parts.push(context.frameName);
@@ -15149,7 +15580,7 @@ class RiskGlobalService {
15149
15580
  * @param payload - Payload with riskName, exchangeName and frameName
15150
15581
  * @returns Promise that resolves when validation is complete
15151
15582
  */
15152
- this.validate = memoize(([context]) => CREATE_KEY_FN$n(context), async (context) => {
15583
+ this.validate = memoize(([context]) => CREATE_KEY_FN$o(context), async (context) => {
15153
15584
  this.loggerService.log("riskGlobalService validate", {
15154
15585
  context,
15155
15586
  });
@@ -15227,7 +15658,7 @@ const METHOD_NAME_VALIDATE = "actionCoreService validate";
15227
15658
  * @param context - Execution context with strategyName, exchangeName, frameName
15228
15659
  * @returns Unique string key for memoization
15229
15660
  */
15230
- const CREATE_KEY_FN$m = (context) => {
15661
+ const CREATE_KEY_FN$n = (context) => {
15231
15662
  const parts = [context.strategyName, context.exchangeName];
15232
15663
  if (context.frameName)
15233
15664
  parts.push(context.frameName);
@@ -15271,7 +15702,7 @@ class ActionCoreService {
15271
15702
  * @param context - Strategy execution context with strategyName, exchangeName and frameName
15272
15703
  * @returns Promise that resolves when all validations complete
15273
15704
  */
15274
- this.validate = memoize(([context]) => CREATE_KEY_FN$m(context), async (context) => {
15705
+ this.validate = memoize(([context]) => CREATE_KEY_FN$n(context), async (context) => {
15275
15706
  this.loggerService.log(METHOD_NAME_VALIDATE, {
15276
15707
  context,
15277
15708
  });
@@ -16866,8 +17297,8 @@ class BacktestLogicPrivateService {
16866
17297
  }
16867
17298
 
16868
17299
  const EMITTER_CHECK_INTERVAL = 5000;
16869
- const MS_PER_MINUTE$3 = 60000;
16870
- const INTERVAL_MINUTES$3 = {
17300
+ const MS_PER_MINUTE$4 = 60000;
17301
+ const INTERVAL_MINUTES$4 = {
16871
17302
  "1m": 1,
16872
17303
  "3m": 3,
16873
17304
  "5m": 5,
@@ -16883,7 +17314,7 @@ const INTERVAL_MINUTES$3 = {
16883
17314
  };
16884
17315
  const createEmitter = memoize(([interval]) => `${interval}`, (interval) => {
16885
17316
  const tickSubject = new Subject();
16886
- const intervalMs = INTERVAL_MINUTES$3[interval] * MS_PER_MINUTE$3;
17317
+ const intervalMs = INTERVAL_MINUTES$4[interval] * MS_PER_MINUTE$4;
16887
17318
  {
16888
17319
  let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
16889
17320
  Source.fromInterval(EMITTER_CHECK_INTERVAL)
@@ -20267,7 +20698,7 @@ const ReportWriter = new ReportWriterAdapter();
20267
20698
  * @param backtest - Whether running in backtest mode
20268
20699
  * @returns Unique string key for memoization
20269
20700
  */
20270
- const CREATE_KEY_FN$l = (symbol, strategyName, exchangeName, frameName, backtest) => {
20701
+ const CREATE_KEY_FN$m = (symbol, strategyName, exchangeName, frameName, backtest) => {
20271
20702
  const parts = [symbol, strategyName, exchangeName];
20272
20703
  if (frameName)
20273
20704
  parts.push(frameName);
@@ -20513,7 +20944,7 @@ class BacktestMarkdownService {
20513
20944
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
20514
20945
  * Each combination gets its own isolated storage instance.
20515
20946
  */
20516
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$l(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$a(symbol, strategyName, exchangeName, frameName));
20947
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$m(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$a(symbol, strategyName, exchangeName, frameName));
20517
20948
  /**
20518
20949
  * Processes tick events and accumulates closed signals.
20519
20950
  * Should be called from IStrategyCallbacks.onTick.
@@ -20670,7 +21101,7 @@ class BacktestMarkdownService {
20670
21101
  payload,
20671
21102
  });
20672
21103
  if (payload) {
20673
- const key = CREATE_KEY_FN$l(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21104
+ const key = CREATE_KEY_FN$m(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20674
21105
  this.getStorage.clear(key);
20675
21106
  }
20676
21107
  else {
@@ -20732,7 +21163,7 @@ class BacktestMarkdownService {
20732
21163
  * @param backtest - Whether running in backtest mode
20733
21164
  * @returns Unique string key for memoization
20734
21165
  */
20735
- const CREATE_KEY_FN$k = (symbol, strategyName, exchangeName, frameName, backtest) => {
21166
+ const CREATE_KEY_FN$l = (symbol, strategyName, exchangeName, frameName, backtest) => {
20736
21167
  const parts = [symbol, strategyName, exchangeName];
20737
21168
  if (frameName)
20738
21169
  parts.push(frameName);
@@ -21227,7 +21658,7 @@ class LiveMarkdownService {
21227
21658
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
21228
21659
  * Each combination gets its own isolated storage instance.
21229
21660
  */
21230
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$k(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$9(symbol, strategyName, exchangeName, frameName));
21661
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$l(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$9(symbol, strategyName, exchangeName, frameName));
21231
21662
  /**
21232
21663
  * Subscribes to live signal emitter to receive tick events.
21233
21664
  * Protected against multiple subscriptions.
@@ -21445,7 +21876,7 @@ class LiveMarkdownService {
21445
21876
  payload,
21446
21877
  });
21447
21878
  if (payload) {
21448
- const key = CREATE_KEY_FN$k(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21879
+ const key = CREATE_KEY_FN$l(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21449
21880
  this.getStorage.clear(key);
21450
21881
  }
21451
21882
  else {
@@ -21465,7 +21896,7 @@ class LiveMarkdownService {
21465
21896
  * @param backtest - Whether running in backtest mode
21466
21897
  * @returns Unique string key for memoization
21467
21898
  */
21468
- const CREATE_KEY_FN$j = (symbol, strategyName, exchangeName, frameName, backtest) => {
21899
+ const CREATE_KEY_FN$k = (symbol, strategyName, exchangeName, frameName, backtest) => {
21469
21900
  const parts = [symbol, strategyName, exchangeName];
21470
21901
  if (frameName)
21471
21902
  parts.push(frameName);
@@ -21754,7 +22185,7 @@ class ScheduleMarkdownService {
21754
22185
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
21755
22186
  * Each combination gets its own isolated storage instance.
21756
22187
  */
21757
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$j(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$8(symbol, strategyName, exchangeName, frameName));
22188
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$k(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$8(symbol, strategyName, exchangeName, frameName));
21758
22189
  /**
21759
22190
  * Subscribes to signal emitter to receive scheduled signal events.
21760
22191
  * Protected against multiple subscriptions.
@@ -21957,7 +22388,7 @@ class ScheduleMarkdownService {
21957
22388
  payload,
21958
22389
  });
21959
22390
  if (payload) {
21960
- const key = CREATE_KEY_FN$j(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22391
+ const key = CREATE_KEY_FN$k(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21961
22392
  this.getStorage.clear(key);
21962
22393
  }
21963
22394
  else {
@@ -21977,7 +22408,7 @@ class ScheduleMarkdownService {
21977
22408
  * @param backtest - Whether running in backtest mode
21978
22409
  * @returns Unique string key for memoization
21979
22410
  */
21980
- const CREATE_KEY_FN$i = (symbol, strategyName, exchangeName, frameName, backtest) => {
22411
+ const CREATE_KEY_FN$j = (symbol, strategyName, exchangeName, frameName, backtest) => {
21981
22412
  const parts = [symbol, strategyName, exchangeName];
21982
22413
  if (frameName)
21983
22414
  parts.push(frameName);
@@ -22222,7 +22653,7 @@ class PerformanceMarkdownService {
22222
22653
  * Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
22223
22654
  * Each combination gets its own isolated storage instance.
22224
22655
  */
22225
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$i(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
22656
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$j(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
22226
22657
  /**
22227
22658
  * Subscribes to performance emitter to receive performance events.
22228
22659
  * Protected against multiple subscriptions.
@@ -22389,7 +22820,7 @@ class PerformanceMarkdownService {
22389
22820
  payload,
22390
22821
  });
22391
22822
  if (payload) {
22392
- const key = CREATE_KEY_FN$i(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22823
+ const key = CREATE_KEY_FN$j(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22393
22824
  this.getStorage.clear(key);
22394
22825
  }
22395
22826
  else {
@@ -22868,7 +23299,7 @@ class WalkerMarkdownService {
22868
23299
  * @param backtest - Whether running in backtest mode
22869
23300
  * @returns Unique string key for memoization
22870
23301
  */
22871
- const CREATE_KEY_FN$h = (exchangeName, frameName, backtest) => {
23302
+ const CREATE_KEY_FN$i = (exchangeName, frameName, backtest) => {
22872
23303
  const parts = [exchangeName];
22873
23304
  if (frameName)
22874
23305
  parts.push(frameName);
@@ -23315,7 +23746,7 @@ class HeatMarkdownService {
23315
23746
  * Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
23316
23747
  * Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
23317
23748
  */
23318
- this.getStorage = memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$h(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
23749
+ this.getStorage = memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$i(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
23319
23750
  /**
23320
23751
  * Subscribes to signal emitter to receive tick events.
23321
23752
  * Protected against multiple subscriptions.
@@ -23533,7 +23964,7 @@ class HeatMarkdownService {
23533
23964
  payload,
23534
23965
  });
23535
23966
  if (payload) {
23536
- const key = CREATE_KEY_FN$h(payload.exchangeName, payload.frameName, payload.backtest);
23967
+ const key = CREATE_KEY_FN$i(payload.exchangeName, payload.frameName, payload.backtest);
23537
23968
  this.getStorage.clear(key);
23538
23969
  }
23539
23970
  else {
@@ -24564,7 +24995,7 @@ class ClientPartial {
24564
24995
  * @param backtest - Whether running in backtest mode
24565
24996
  * @returns Unique string key for memoization
24566
24997
  */
24567
- const CREATE_KEY_FN$g = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
24998
+ const CREATE_KEY_FN$h = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
24568
24999
  /**
24569
25000
  * Creates a callback function for emitting profit events to partialProfitSubject.
24570
25001
  *
@@ -24686,7 +25117,7 @@ class PartialConnectionService {
24686
25117
  * Key format: "signalId:backtest" or "signalId:live"
24687
25118
  * Value: ClientPartial instance with logger and event emitters
24688
25119
  */
24689
- this.getPartial = memoize(([signalId, backtest]) => CREATE_KEY_FN$g(signalId, backtest), (signalId, backtest) => {
25120
+ this.getPartial = memoize(([signalId, backtest]) => CREATE_KEY_FN$h(signalId, backtest), (signalId, backtest) => {
24690
25121
  return new ClientPartial({
24691
25122
  signalId,
24692
25123
  logger: this.loggerService,
@@ -24776,7 +25207,7 @@ class PartialConnectionService {
24776
25207
  const partial = this.getPartial(data.id, backtest);
24777
25208
  await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
24778
25209
  await partial.clear(symbol, data, priceClose, backtest);
24779
- const key = CREATE_KEY_FN$g(data.id, backtest);
25210
+ const key = CREATE_KEY_FN$h(data.id, backtest);
24780
25211
  this.getPartial.clear(key);
24781
25212
  };
24782
25213
  }
@@ -24792,7 +25223,7 @@ class PartialConnectionService {
24792
25223
  * @param backtest - Whether running in backtest mode
24793
25224
  * @returns Unique string key for memoization
24794
25225
  */
24795
- const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
25226
+ const CREATE_KEY_FN$g = (symbol, strategyName, exchangeName, frameName, backtest) => {
24796
25227
  const parts = [symbol, strategyName, exchangeName];
24797
25228
  if (frameName)
24798
25229
  parts.push(frameName);
@@ -25015,7 +25446,7 @@ class PartialMarkdownService {
25015
25446
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
25016
25447
  * Each combination gets its own isolated storage instance.
25017
25448
  */
25018
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$f(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$6(symbol, strategyName, exchangeName, frameName));
25449
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$g(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$6(symbol, strategyName, exchangeName, frameName));
25019
25450
  /**
25020
25451
  * Subscribes to partial profit/loss signal emitters to receive events.
25021
25452
  * Protected against multiple subscriptions.
@@ -25225,7 +25656,7 @@ class PartialMarkdownService {
25225
25656
  payload,
25226
25657
  });
25227
25658
  if (payload) {
25228
- const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25659
+ const key = CREATE_KEY_FN$g(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25229
25660
  this.getStorage.clear(key);
25230
25661
  }
25231
25662
  else {
@@ -25241,7 +25672,7 @@ class PartialMarkdownService {
25241
25672
  * @param context - Context with strategyName, exchangeName, frameName
25242
25673
  * @returns Unique string key for memoization
25243
25674
  */
25244
- const CREATE_KEY_FN$e = (context) => {
25675
+ const CREATE_KEY_FN$f = (context) => {
25245
25676
  const parts = [context.strategyName, context.exchangeName];
25246
25677
  if (context.frameName)
25247
25678
  parts.push(context.frameName);
@@ -25315,7 +25746,7 @@ class PartialGlobalService {
25315
25746
  * @param context - Context with strategyName, exchangeName and frameName
25316
25747
  * @param methodName - Name of the calling method for error tracking
25317
25748
  */
25318
- this.validate = memoize(([context]) => CREATE_KEY_FN$e(context), (context, methodName) => {
25749
+ this.validate = memoize(([context]) => CREATE_KEY_FN$f(context), (context, methodName) => {
25319
25750
  this.loggerService.log("partialGlobalService validate", {
25320
25751
  context,
25321
25752
  methodName,
@@ -25770,7 +26201,7 @@ class ClientBreakeven {
25770
26201
  * @param backtest - Whether running in backtest mode
25771
26202
  * @returns Unique string key for memoization
25772
26203
  */
25773
- const CREATE_KEY_FN$d = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
26204
+ const CREATE_KEY_FN$e = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
25774
26205
  /**
25775
26206
  * Creates a callback function for emitting breakeven events to breakevenSubject.
25776
26207
  *
@@ -25856,7 +26287,7 @@ class BreakevenConnectionService {
25856
26287
  * Key format: "signalId:backtest" or "signalId:live"
25857
26288
  * Value: ClientBreakeven instance with logger and event emitter
25858
26289
  */
25859
- this.getBreakeven = memoize(([signalId, backtest]) => CREATE_KEY_FN$d(signalId, backtest), (signalId, backtest) => {
26290
+ this.getBreakeven = memoize(([signalId, backtest]) => CREATE_KEY_FN$e(signalId, backtest), (signalId, backtest) => {
25860
26291
  return new ClientBreakeven({
25861
26292
  signalId,
25862
26293
  logger: this.loggerService,
@@ -25917,7 +26348,7 @@ class BreakevenConnectionService {
25917
26348
  const breakeven = this.getBreakeven(data.id, backtest);
25918
26349
  await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
25919
26350
  await breakeven.clear(symbol, data, priceClose, backtest);
25920
- const key = CREATE_KEY_FN$d(data.id, backtest);
26351
+ const key = CREATE_KEY_FN$e(data.id, backtest);
25921
26352
  this.getBreakeven.clear(key);
25922
26353
  };
25923
26354
  }
@@ -25933,7 +26364,7 @@ class BreakevenConnectionService {
25933
26364
  * @param backtest - Whether running in backtest mode
25934
26365
  * @returns Unique string key for memoization
25935
26366
  */
25936
- const CREATE_KEY_FN$c = (symbol, strategyName, exchangeName, frameName, backtest) => {
26367
+ const CREATE_KEY_FN$d = (symbol, strategyName, exchangeName, frameName, backtest) => {
25937
26368
  const parts = [symbol, strategyName, exchangeName];
25938
26369
  if (frameName)
25939
26370
  parts.push(frameName);
@@ -26108,7 +26539,7 @@ class BreakevenMarkdownService {
26108
26539
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
26109
26540
  * Each combination gets its own isolated storage instance.
26110
26541
  */
26111
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$c(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$5(symbol, strategyName, exchangeName, frameName));
26542
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$d(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$5(symbol, strategyName, exchangeName, frameName));
26112
26543
  /**
26113
26544
  * Subscribes to breakeven signal emitter to receive events.
26114
26545
  * Protected against multiple subscriptions.
@@ -26297,7 +26728,7 @@ class BreakevenMarkdownService {
26297
26728
  payload,
26298
26729
  });
26299
26730
  if (payload) {
26300
- const key = CREATE_KEY_FN$c(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26731
+ const key = CREATE_KEY_FN$d(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26301
26732
  this.getStorage.clear(key);
26302
26733
  }
26303
26734
  else {
@@ -26313,7 +26744,7 @@ class BreakevenMarkdownService {
26313
26744
  * @param context - Context with strategyName, exchangeName, frameName
26314
26745
  * @returns Unique string key for memoization
26315
26746
  */
26316
- const CREATE_KEY_FN$b = (context) => {
26747
+ const CREATE_KEY_FN$c = (context) => {
26317
26748
  const parts = [context.strategyName, context.exchangeName];
26318
26749
  if (context.frameName)
26319
26750
  parts.push(context.frameName);
@@ -26387,7 +26818,7 @@ class BreakevenGlobalService {
26387
26818
  * @param context - Context with strategyName, exchangeName and frameName
26388
26819
  * @param methodName - Name of the calling method for error tracking
26389
26820
  */
26390
- this.validate = memoize(([context]) => CREATE_KEY_FN$b(context), (context, methodName) => {
26821
+ this.validate = memoize(([context]) => CREATE_KEY_FN$c(context), (context, methodName) => {
26391
26822
  this.loggerService.log("breakevenGlobalService validate", {
26392
26823
  context,
26393
26824
  methodName,
@@ -26608,7 +27039,7 @@ class ConfigValidationService {
26608
27039
  * @param backtest - Whether running in backtest mode
26609
27040
  * @returns Unique string key for memoization
26610
27041
  */
26611
- const CREATE_KEY_FN$a = (symbol, strategyName, exchangeName, frameName, backtest) => {
27042
+ const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
26612
27043
  const parts = [symbol, strategyName, exchangeName];
26613
27044
  if (frameName)
26614
27045
  parts.push(frameName);
@@ -26775,7 +27206,7 @@ class RiskMarkdownService {
26775
27206
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
26776
27207
  * Each combination gets its own isolated storage instance.
26777
27208
  */
26778
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$a(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$4(symbol, strategyName, exchangeName, frameName));
27209
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$4(symbol, strategyName, exchangeName, frameName));
26779
27210
  /**
26780
27211
  * Subscribes to risk rejection emitter to receive rejection events.
26781
27212
  * Protected against multiple subscriptions.
@@ -26964,7 +27395,7 @@ class RiskMarkdownService {
26964
27395
  payload,
26965
27396
  });
26966
27397
  if (payload) {
26967
- const key = CREATE_KEY_FN$a(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
27398
+ const key = CREATE_KEY_FN$b(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26968
27399
  this.getStorage.clear(key);
26969
27400
  }
26970
27401
  else {
@@ -29343,7 +29774,7 @@ class HighestProfitReportService {
29343
29774
  * @returns Colon-separated key string for memoization
29344
29775
  * @internal
29345
29776
  */
29346
- const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
29777
+ const CREATE_KEY_FN$a = (symbol, strategyName, exchangeName, frameName, backtest) => {
29347
29778
  const parts = [symbol, strategyName, exchangeName];
29348
29779
  if (frameName)
29349
29780
  parts.push(frameName);
@@ -29585,7 +30016,7 @@ class StrategyMarkdownService {
29585
30016
  *
29586
30017
  * @internal
29587
30018
  */
29588
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$9(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$3(symbol, strategyName, exchangeName, frameName));
30019
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$a(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$3(symbol, strategyName, exchangeName, frameName));
29589
30020
  /**
29590
30021
  * Records a cancel-scheduled event when a scheduled signal is cancelled.
29591
30022
  *
@@ -30153,7 +30584,7 @@ class StrategyMarkdownService {
30153
30584
  this.clear = async (payload) => {
30154
30585
  this.loggerService.log("strategyMarkdownService clear", { payload });
30155
30586
  if (payload) {
30156
- const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
30587
+ const key = CREATE_KEY_FN$a(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
30157
30588
  this.getStorage.clear(key);
30158
30589
  }
30159
30590
  else {
@@ -30261,7 +30692,7 @@ class StrategyMarkdownService {
30261
30692
  * Creates a unique key for memoizing ReportStorage instances.
30262
30693
  * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
30263
30694
  */
30264
- const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
30695
+ const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
30265
30696
  const parts = [symbol, strategyName, exchangeName];
30266
30697
  if (frameName)
30267
30698
  parts.push(frameName);
@@ -30454,7 +30885,7 @@ let ReportStorage$2 = class ReportStorage {
30454
30885
  class SyncMarkdownService {
30455
30886
  constructor() {
30456
30887
  this.loggerService = inject(TYPES.loggerService);
30457
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$8(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName, backtest));
30888
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$9(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName, backtest));
30458
30889
  /**
30459
30890
  * Subscribes to `syncSubject` to start receiving `SignalSyncContract` events.
30460
30891
  * Protected against multiple subscriptions via `singleshot` — subsequent calls
@@ -30650,7 +31081,7 @@ class SyncMarkdownService {
30650
31081
  this.clear = async (payload) => {
30651
31082
  this.loggerService.log("syncMarkdownService clear", { payload });
30652
31083
  if (payload) {
30653
- const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31084
+ const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
30654
31085
  this.getStorage.clear(key);
30655
31086
  }
30656
31087
  else {
@@ -30663,7 +31094,7 @@ class SyncMarkdownService {
30663
31094
  /**
30664
31095
  * Creates a unique memoization key for a symbol-strategy-exchange-frame-backtest combination.
30665
31096
  */
30666
- const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31097
+ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
30667
31098
  const parts = [symbol, strategyName, exchangeName];
30668
31099
  if (frameName)
30669
31100
  parts.push(frameName);
@@ -30839,7 +31270,7 @@ let ReportStorage$1 = class ReportStorage {
30839
31270
  class HighestProfitMarkdownService {
30840
31271
  constructor() {
30841
31272
  this.loggerService = inject(TYPES.loggerService);
30842
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$7(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
31273
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$8(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
30843
31274
  /**
30844
31275
  * Subscribes to `highestProfitSubject` to start receiving `HighestProfitContract`
30845
31276
  * events. Protected against multiple subscriptions via `singleshot` — subsequent
@@ -31005,7 +31436,7 @@ class HighestProfitMarkdownService {
31005
31436
  this.clear = async (payload) => {
31006
31437
  this.loggerService.log("highestProfitMarkdownService clear", { payload });
31007
31438
  if (payload) {
31008
- const key = CREATE_KEY_FN$7(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31439
+ const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31009
31440
  this.getStorage.clear(key);
31010
31441
  }
31011
31442
  else {
@@ -31027,7 +31458,7 @@ const LISTEN_TIMEOUT$1 = 120000;
31027
31458
  * @param backtest - Whether running in backtest mode
31028
31459
  * @returns Unique string key for memoization
31029
31460
  */
31030
- const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31461
+ const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31031
31462
  const parts = [symbol, strategyName, exchangeName];
31032
31463
  if (frameName)
31033
31464
  parts.push(frameName);
@@ -31070,7 +31501,7 @@ class PriceMetaService {
31070
31501
  * Each subject holds the latest currentPrice emitted by the strategy iterator for that key.
31071
31502
  * Instances are cached until clear() is called.
31072
31503
  */
31073
- this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
31504
+ this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$7(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
31074
31505
  /**
31075
31506
  * Returns the current market price for the given symbol and context.
31076
31507
  *
@@ -31099,10 +31530,10 @@ class PriceMetaService {
31099
31530
  if (source.data) {
31100
31531
  return source.data;
31101
31532
  }
31102
- console.warn(`PriceMetaService: No currentPrice available for ${CREATE_KEY_FN$6(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
31533
+ console.warn(`PriceMetaService: No currentPrice available for ${CREATE_KEY_FN$7(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
31103
31534
  const currentPrice = await waitForNext(source, (data) => !!data, LISTEN_TIMEOUT$1);
31104
31535
  if (typeof currentPrice === "symbol") {
31105
- throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$6(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31536
+ throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$7(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31106
31537
  }
31107
31538
  return currentPrice;
31108
31539
  };
@@ -31144,7 +31575,7 @@ class PriceMetaService {
31144
31575
  this.getSource.clear();
31145
31576
  return;
31146
31577
  }
31147
- const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31578
+ const key = CREATE_KEY_FN$7(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31148
31579
  this.getSource.clear(key);
31149
31580
  };
31150
31581
  }
@@ -31162,7 +31593,7 @@ const LISTEN_TIMEOUT = 120000;
31162
31593
  * @param backtest - Whether running in backtest mode
31163
31594
  * @returns Unique string key for memoization
31164
31595
  */
31165
- const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31596
+ const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31166
31597
  const parts = [symbol, strategyName, exchangeName];
31167
31598
  if (frameName)
31168
31599
  parts.push(frameName);
@@ -31205,7 +31636,7 @@ class TimeMetaService {
31205
31636
  * Each subject holds the latest createdAt timestamp emitted by the strategy iterator for that key.
31206
31637
  * Instances are cached until clear() is called.
31207
31638
  */
31208
- this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
31639
+ this.getSource = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), () => new BehaviorSubject());
31209
31640
  /**
31210
31641
  * Returns the current candle timestamp (in milliseconds) for the given symbol and context.
31211
31642
  *
@@ -31233,10 +31664,10 @@ class TimeMetaService {
31233
31664
  if (source.data) {
31234
31665
  return source.data;
31235
31666
  }
31236
- console.warn(`TimeMetaService: No timestamp available for ${CREATE_KEY_FN$5(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
31667
+ console.warn(`TimeMetaService: No timestamp available for ${CREATE_KEY_FN$6(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
31237
31668
  const timestamp = await waitForNext(source, (data) => !!data, LISTEN_TIMEOUT);
31238
31669
  if (typeof timestamp === "symbol") {
31239
- throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$5(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31670
+ throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$6(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31240
31671
  }
31241
31672
  return timestamp;
31242
31673
  };
@@ -31278,7 +31709,7 @@ class TimeMetaService {
31278
31709
  this.getSource.clear();
31279
31710
  return;
31280
31711
  }
31281
- const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31712
+ const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31282
31713
  this.getSource.clear(key);
31283
31714
  };
31284
31715
  }
@@ -31374,7 +31805,7 @@ class MaxDrawdownReportService {
31374
31805
  /**
31375
31806
  * Creates a unique memoization key for a symbol-strategy-exchange-frame-backtest combination.
31376
31807
  */
31377
- const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31808
+ const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31378
31809
  const parts = [symbol, strategyName, exchangeName];
31379
31810
  if (frameName)
31380
31811
  parts.push(frameName);
@@ -31498,7 +31929,7 @@ class ReportStorage {
31498
31929
  class MaxDrawdownMarkdownService {
31499
31930
  constructor() {
31500
31931
  this.loggerService = inject(TYPES.loggerService);
31501
- this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$4(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
31932
+ this.getStorage = memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
31502
31933
  /**
31503
31934
  * Subscribes to `maxDrawdownSubject` to start receiving `MaxDrawdownContract`
31504
31935
  * events. Protected against multiple subscriptions via `singleshot`.
@@ -31577,7 +32008,7 @@ class MaxDrawdownMarkdownService {
31577
32008
  this.clear = async (payload) => {
31578
32009
  this.loggerService.log("maxDrawdownMarkdownService clear", { payload });
31579
32010
  if (payload) {
31580
- const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
32011
+ const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31581
32012
  this.getStorage.clear(key);
31582
32013
  }
31583
32014
  else {
@@ -31831,7 +32262,7 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
31831
32262
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
31832
32263
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
31833
32264
  const EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES = "ExchangeUtils.getAggregatedTrades";
31834
- const MS_PER_MINUTE$2 = 60000;
32265
+ const MS_PER_MINUTE$3 = 60000;
31835
32266
  /**
31836
32267
  * Gets current timestamp from execution context if available.
31837
32268
  * Returns current Date() if no execution context exists (non-trading GUI).
@@ -31898,7 +32329,7 @@ const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest)
31898
32329
  const DEFAULT_GET_AGGREGATED_TRADES_FN = async (_symbol, _from, _to, _backtest) => {
31899
32330
  throw new Error(`getAggregatedTrades is not implemented for this exchange`);
31900
32331
  };
31901
- const INTERVAL_MINUTES$2 = {
32332
+ const INTERVAL_MINUTES$3 = {
31902
32333
  "1m": 1,
31903
32334
  "3m": 3,
31904
32335
  "5m": 5,
@@ -31930,7 +32361,7 @@ const INTERVAL_MINUTES$2 = {
31930
32361
  * @returns Aligned timestamp rounded down to interval boundary
31931
32362
  */
31932
32363
  const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
31933
- const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
32364
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$3;
31934
32365
  return Math.floor(timestamp / intervalMs) * intervalMs;
31935
32366
  };
31936
32367
  /**
@@ -32070,11 +32501,11 @@ class ExchangeInstance {
32070
32501
  limit,
32071
32502
  });
32072
32503
  const getCandles = this._methods.getCandles;
32073
- const step = INTERVAL_MINUTES$2[interval];
32504
+ const step = INTERVAL_MINUTES$3[interval];
32074
32505
  if (!step) {
32075
32506
  throw new Error(`ExchangeInstance unknown interval=${interval}`);
32076
32507
  }
32077
- const stepMs = step * MS_PER_MINUTE$2;
32508
+ const stepMs = step * MS_PER_MINUTE$3;
32078
32509
  // Align when down to interval boundary
32079
32510
  const when = await GET_TIMESTAMP_FN();
32080
32511
  const whenTimestamp = when.getTime();
@@ -32259,7 +32690,7 @@ class ExchangeInstance {
32259
32690
  const when = await GET_TIMESTAMP_FN();
32260
32691
  const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
32261
32692
  const to = new Date(alignedTo);
32262
- const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$2);
32693
+ const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$3);
32263
32694
  const isBacktest = await GET_BACKTEST_FN();
32264
32695
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
32265
32696
  };
@@ -32291,7 +32722,7 @@ class ExchangeInstance {
32291
32722
  const when = await GET_TIMESTAMP_FN();
32292
32723
  // Align to 1-minute boundary to prevent look-ahead bias
32293
32724
  const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), 1);
32294
- const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$2 - MS_PER_MINUTE$2;
32725
+ const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$3 - MS_PER_MINUTE$3;
32295
32726
  const isBacktest = await GET_BACKTEST_FN();
32296
32727
  // No limit: fetch a single window and return as-is
32297
32728
  if (limit === undefined) {
@@ -32354,11 +32785,11 @@ class ExchangeInstance {
32354
32785
  sDate,
32355
32786
  eDate,
32356
32787
  });
32357
- const step = INTERVAL_MINUTES$2[interval];
32788
+ const step = INTERVAL_MINUTES$3[interval];
32358
32789
  if (!step) {
32359
32790
  throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
32360
32791
  }
32361
- const stepMs = step * MS_PER_MINUTE$2;
32792
+ const stepMs = step * MS_PER_MINUTE$3;
32362
32793
  const when = await GET_TIMESTAMP_FN();
32363
32794
  const nowTimestamp = when.getTime();
32364
32795
  const alignedNow = ALIGN_TO_INTERVAL_FN$1(nowTimestamp, step);
@@ -32648,8 +33079,8 @@ const Exchange = new ExchangeUtils();
32648
33079
 
32649
33080
  const WARM_CANDLES_METHOD_NAME = "cache.warmCandles";
32650
33081
  const CHECK_CANDLES_METHOD_NAME = "cache.checkCandles";
32651
- const MS_PER_MINUTE$1 = 60000;
32652
- const INTERVAL_MINUTES$1 = {
33082
+ const MS_PER_MINUTE$2 = 60000;
33083
+ const INTERVAL_MINUTES$2 = {
32653
33084
  "1m": 1,
32654
33085
  "3m": 3,
32655
33086
  "5m": 5,
@@ -32664,7 +33095,7 @@ const INTERVAL_MINUTES$1 = {
32664
33095
  "1w": 10080,
32665
33096
  };
32666
33097
  const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
32667
- const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
33098
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
32668
33099
  return Math.floor(timestamp / intervalMs) * intervalMs;
32669
33100
  };
32670
33101
  const BAR_LENGTH = 30;
@@ -32689,11 +33120,11 @@ const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
32689
33120
  async function checkCandles(params) {
32690
33121
  const { symbol, exchangeName, interval, from, to, baseDir = "./dump/data/candle" } = params;
32691
33122
  backtest.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
32692
- const step = INTERVAL_MINUTES$1[interval];
33123
+ const step = INTERVAL_MINUTES$2[interval];
32693
33124
  if (!step) {
32694
33125
  throw new Error(`checkCandles: unsupported interval=${interval}`);
32695
33126
  }
32696
- const stepMs = step * MS_PER_MINUTE$1;
33127
+ const stepMs = step * MS_PER_MINUTE$2;
32697
33128
  const dir = join(baseDir, exchangeName, symbol, interval);
32698
33129
  const fromTs = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
32699
33130
  const toTs = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
@@ -32763,11 +33194,11 @@ async function warmCandles(params) {
32763
33194
  from,
32764
33195
  to,
32765
33196
  });
32766
- const step = INTERVAL_MINUTES$1[interval];
33197
+ const step = INTERVAL_MINUTES$2[interval];
32767
33198
  if (!step) {
32768
33199
  throw new Error(`warmCandles: unsupported interval=${interval}`);
32769
33200
  }
32770
- const stepMs = step * MS_PER_MINUTE$1;
33201
+ const stepMs = step * MS_PER_MINUTE$2;
32771
33202
  const instance = new ExchangeInstance(exchangeName);
32772
33203
  const sinceTimestamp = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
32773
33204
  const untilTimestamp = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
@@ -34706,6 +35137,10 @@ const GET_POSITION_MAX_DRAWDOWN_PRICE_METHOD_NAME = "strategy.getPositionMaxDraw
34706
35137
  const GET_POSITION_MAX_DRAWDOWN_TIMESTAMP_METHOD_NAME = "strategy.getPositionMaxDrawdownTimestamp";
34707
35138
  const GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionMaxDrawdownPnlPercentage";
34708
35139
  const GET_POSITION_MAX_DRAWDOWN_PNL_COST_METHOD_NAME = "strategy.getPositionMaxDrawdownPnlCost";
35140
+ const GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionHighestProfitDistancePnlPercentage";
35141
+ const GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST_METHOD_NAME = "strategy.getPositionHighestProfitDistancePnlCost";
35142
+ const GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionHighestMaxDrawdownPnlPercentage";
35143
+ const GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST_METHOD_NAME = "strategy.getPositionHighestMaxDrawdownPnlCost";
34709
35144
  const GET_POSITION_ENTRY_OVERLAP_METHOD_NAME = "strategy.getPositionEntryOverlap";
34710
35145
  const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOverlap";
34711
35146
  const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
@@ -36340,6 +36775,122 @@ async function getPositionMaxDrawdownPnlCost(symbol) {
36340
36775
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36341
36776
  return await backtest.strategyCoreService.getPositionMaxDrawdownPnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
36342
36777
  }
36778
+ /**
36779
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
36780
+ *
36781
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
36782
+ * Returns null if no pending signal exists.
36783
+ *
36784
+ * @param symbol - Trading pair symbol
36785
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
36786
+ *
36787
+ * @example
36788
+ * ```typescript
36789
+ * import { getPositionHighestProfitDistancePnlPercentage } from "backtest-kit";
36790
+ *
36791
+ * const dist = await getPositionHighestProfitDistancePnlPercentage("BTCUSDT");
36792
+ * // e.g. 1.5 (gave back 1.5% from peak)
36793
+ * ```
36794
+ */
36795
+ async function getPositionHighestProfitDistancePnlPercentage(symbol) {
36796
+ backtest.loggerService.info(GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE_METHOD_NAME, { symbol });
36797
+ if (!ExecutionContextService.hasContext()) {
36798
+ throw new Error("getPositionHighestProfitDistancePnlPercentage requires an execution context");
36799
+ }
36800
+ if (!MethodContextService.hasContext()) {
36801
+ throw new Error("getPositionHighestProfitDistancePnlPercentage requires a method context");
36802
+ }
36803
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36804
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36805
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlPercentage(isBacktest, symbol, { exchangeName, frameName, strategyName });
36806
+ }
36807
+ /**
36808
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
36809
+ *
36810
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
36811
+ * Returns null if no pending signal exists.
36812
+ *
36813
+ * @param symbol - Trading pair symbol
36814
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
36815
+ *
36816
+ * @example
36817
+ * ```typescript
36818
+ * import { getPositionHighestProfitDistancePnlCost } from "backtest-kit";
36819
+ *
36820
+ * const dist = await getPositionHighestProfitDistancePnlCost("BTCUSDT");
36821
+ * // e.g. 3.2 (gave back $3.2 from peak)
36822
+ * ```
36823
+ */
36824
+ async function getPositionHighestProfitDistancePnlCost(symbol) {
36825
+ backtest.loggerService.info(GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST_METHOD_NAME, { symbol });
36826
+ if (!ExecutionContextService.hasContext()) {
36827
+ throw new Error("getPositionHighestProfitDistancePnlCost requires an execution context");
36828
+ }
36829
+ if (!MethodContextService.hasContext()) {
36830
+ throw new Error("getPositionHighestProfitDistancePnlCost requires a method context");
36831
+ }
36832
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36833
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36834
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
36835
+ }
36836
+ /**
36837
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
36838
+ *
36839
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
36840
+ * Returns null if no pending signal exists.
36841
+ *
36842
+ * @param symbol - Trading pair symbol
36843
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
36844
+ *
36845
+ * @example
36846
+ * ```typescript
36847
+ * import { getPositionHighestMaxDrawdownPnlPercentage } from "backtest-kit";
36848
+ *
36849
+ * const dist = await getPositionHighestMaxDrawdownPnlPercentage("BTCUSDT");
36850
+ * // e.g. 2.1 (recovered 2.1% from trough)
36851
+ * ```
36852
+ */
36853
+ async function getPositionHighestMaxDrawdownPnlPercentage(symbol) {
36854
+ backtest.loggerService.info(GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME, { symbol });
36855
+ if (!ExecutionContextService.hasContext()) {
36856
+ throw new Error("getPositionHighestMaxDrawdownPnlPercentage requires an execution context");
36857
+ }
36858
+ if (!MethodContextService.hasContext()) {
36859
+ throw new Error("getPositionHighestMaxDrawdownPnlPercentage requires a method context");
36860
+ }
36861
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36862
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36863
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlPercentage(isBacktest, symbol, { exchangeName, frameName, strategyName });
36864
+ }
36865
+ /**
36866
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
36867
+ *
36868
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
36869
+ * Returns null if no pending signal exists.
36870
+ *
36871
+ * @param symbol - Trading pair symbol
36872
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
36873
+ *
36874
+ * @example
36875
+ * ```typescript
36876
+ * import { getPositionHighestMaxDrawdownPnlCost } from "backtest-kit";
36877
+ *
36878
+ * const dist = await getPositionHighestMaxDrawdownPnlCost("BTCUSDT");
36879
+ * // e.g. 4.8 (recovered $4.8 from trough)
36880
+ * ```
36881
+ */
36882
+ async function getPositionHighestMaxDrawdownPnlCost(symbol) {
36883
+ backtest.loggerService.info(GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST_METHOD_NAME, { symbol });
36884
+ if (!ExecutionContextService.hasContext()) {
36885
+ throw new Error("getPositionHighestMaxDrawdownPnlCost requires an execution context");
36886
+ }
36887
+ if (!MethodContextService.hasContext()) {
36888
+ throw new Error("getPositionHighestMaxDrawdownPnlCost requires a method context");
36889
+ }
36890
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36891
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36892
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
36893
+ }
36343
36894
  /**
36344
36895
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
36345
36896
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -38009,6 +38560,10 @@ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE = "BacktestUtils.getP
38009
38560
  const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP = "BacktestUtils.getPositionMaxDrawdownTimestamp";
38010
38561
  const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE = "BacktestUtils.getPositionMaxDrawdownPnlPercentage";
38011
38562
  const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST = "BacktestUtils.getPositionMaxDrawdownPnlCost";
38563
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE = "BacktestUtils.getPositionHighestProfitDistancePnlPercentage";
38564
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST = "BacktestUtils.getPositionHighestProfitDistancePnlCost";
38565
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE = "BacktestUtils.getPositionHighestMaxDrawdownPnlPercentage";
38566
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST = "BacktestUtils.getPositionHighestMaxDrawdownPnlCost";
38012
38567
  const BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "BacktestUtils.getPositionEntryOverlap";
38013
38568
  const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPositionPartialOverlap";
38014
38569
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
@@ -39278,6 +39833,118 @@ class BacktestUtils {
39278
39833
  }
39279
39834
  return await backtest.strategyCoreService.getPositionMaxDrawdownPnlCost(true, symbol, context);
39280
39835
  };
39836
+ /**
39837
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
39838
+ *
39839
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
39840
+ * Returns null if no pending signal exists.
39841
+ *
39842
+ * @param symbol - Trading pair symbol
39843
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39844
+ * @returns drawdown distance in PnL% (≥ 0) or null if no active position
39845
+ */
39846
+ this.getPositionHighestProfitDistancePnlPercentage = async (symbol, context) => {
39847
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE, {
39848
+ symbol,
39849
+ context,
39850
+ });
39851
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
39852
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
39853
+ {
39854
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39855
+ riskName &&
39856
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
39857
+ riskList &&
39858
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
39859
+ actions &&
39860
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
39861
+ }
39862
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlPercentage(true, symbol, context);
39863
+ };
39864
+ /**
39865
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
39866
+ *
39867
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
39868
+ * Returns null if no pending signal exists.
39869
+ *
39870
+ * @param symbol - Trading pair symbol
39871
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39872
+ * @returns drawdown distance in PnL cost (≥ 0) or null if no active position
39873
+ */
39874
+ this.getPositionHighestProfitDistancePnlCost = async (symbol, context) => {
39875
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST, {
39876
+ symbol,
39877
+ context,
39878
+ });
39879
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
39880
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
39881
+ {
39882
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39883
+ riskName &&
39884
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
39885
+ riskList &&
39886
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
39887
+ actions &&
39888
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
39889
+ }
39890
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlCost(true, symbol, context);
39891
+ };
39892
+ /**
39893
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
39894
+ *
39895
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
39896
+ * Returns null if no pending signal exists.
39897
+ *
39898
+ * @param symbol - Trading pair symbol
39899
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39900
+ * @returns recovery distance in PnL% (≥ 0) or null if no active position
39901
+ */
39902
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (symbol, context) => {
39903
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE, {
39904
+ symbol,
39905
+ context,
39906
+ });
39907
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
39908
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
39909
+ {
39910
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39911
+ riskName &&
39912
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
39913
+ riskList &&
39914
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
39915
+ actions &&
39916
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
39917
+ }
39918
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlPercentage(true, symbol, context);
39919
+ };
39920
+ /**
39921
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
39922
+ *
39923
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
39924
+ * Returns null if no pending signal exists.
39925
+ *
39926
+ * @param symbol - Trading pair symbol
39927
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39928
+ * @returns recovery distance in PnL cost (≥ 0) or null if no active position
39929
+ */
39930
+ this.getPositionHighestMaxDrawdownPnlCost = async (symbol, context) => {
39931
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST, {
39932
+ symbol,
39933
+ context,
39934
+ });
39935
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
39936
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
39937
+ {
39938
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39939
+ riskName &&
39940
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
39941
+ riskList &&
39942
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
39943
+ actions &&
39944
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
39945
+ }
39946
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlCost(true, symbol, context);
39947
+ };
39281
39948
  /**
39282
39949
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
39283
39950
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -40403,6 +41070,10 @@ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE = "LiveUtils.getPositionM
40403
41070
  const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP = "LiveUtils.getPositionMaxDrawdownTimestamp";
40404
41071
  const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE = "LiveUtils.getPositionMaxDrawdownPnlPercentage";
40405
41072
  const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST = "LiveUtils.getPositionMaxDrawdownPnlCost";
41073
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE = "LiveUtils.getPositionHighestProfitDistancePnlPercentage";
41074
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST = "LiveUtils.getPositionHighestProfitDistancePnlCost";
41075
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE = "LiveUtils.getPositionHighestMaxDrawdownPnlPercentage";
41076
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST = "LiveUtils.getPositionHighestMaxDrawdownPnlCost";
40406
41077
  const LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "LiveUtils.getPositionEntryOverlap";
40407
41078
  const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPartialOverlap";
40408
41079
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
@@ -41799,6 +42470,134 @@ class LiveUtils {
41799
42470
  frameName: "",
41800
42471
  });
41801
42472
  };
42473
+ /**
42474
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
42475
+ *
42476
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
42477
+ * Returns null if no pending signal exists.
42478
+ *
42479
+ * @param symbol - Trading pair symbol
42480
+ * @param context - Execution context with strategyName and exchangeName
42481
+ * @returns drawdown distance in PnL% (≥ 0) or null if no active position
42482
+ */
42483
+ this.getPositionHighestProfitDistancePnlPercentage = async (symbol, context) => {
42484
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE, {
42485
+ symbol,
42486
+ context,
42487
+ });
42488
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
42489
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
42490
+ {
42491
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42492
+ riskName &&
42493
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
42494
+ riskList &&
42495
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
42496
+ actions &&
42497
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
42498
+ }
42499
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlPercentage(false, symbol, {
42500
+ strategyName: context.strategyName,
42501
+ exchangeName: context.exchangeName,
42502
+ frameName: "",
42503
+ });
42504
+ };
42505
+ /**
42506
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
42507
+ *
42508
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
42509
+ * Returns null if no pending signal exists.
42510
+ *
42511
+ * @param symbol - Trading pair symbol
42512
+ * @param context - Execution context with strategyName and exchangeName
42513
+ * @returns drawdown distance in PnL cost (≥ 0) or null if no active position
42514
+ */
42515
+ this.getPositionHighestProfitDistancePnlCost = async (symbol, context) => {
42516
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST, {
42517
+ symbol,
42518
+ context,
42519
+ });
42520
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
42521
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
42522
+ {
42523
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42524
+ riskName &&
42525
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
42526
+ riskList &&
42527
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
42528
+ actions &&
42529
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
42530
+ }
42531
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlCost(false, symbol, {
42532
+ strategyName: context.strategyName,
42533
+ exchangeName: context.exchangeName,
42534
+ frameName: "",
42535
+ });
42536
+ };
42537
+ /**
42538
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
42539
+ *
42540
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
42541
+ * Returns null if no pending signal exists.
42542
+ *
42543
+ * @param symbol - Trading pair symbol
42544
+ * @param context - Execution context with strategyName and exchangeName
42545
+ * @returns recovery distance in PnL% (≥ 0) or null if no active position
42546
+ */
42547
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (symbol, context) => {
42548
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE, {
42549
+ symbol,
42550
+ context,
42551
+ });
42552
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
42553
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
42554
+ {
42555
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42556
+ riskName &&
42557
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
42558
+ riskList &&
42559
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
42560
+ actions &&
42561
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
42562
+ }
42563
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlPercentage(false, symbol, {
42564
+ strategyName: context.strategyName,
42565
+ exchangeName: context.exchangeName,
42566
+ frameName: "",
42567
+ });
42568
+ };
42569
+ /**
42570
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
42571
+ *
42572
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
42573
+ * Returns null if no pending signal exists.
42574
+ *
42575
+ * @param symbol - Trading pair symbol
42576
+ * @param context - Execution context with strategyName and exchangeName
42577
+ * @returns recovery distance in PnL cost (≥ 0) or null if no active position
42578
+ */
42579
+ this.getPositionHighestMaxDrawdownPnlCost = async (symbol, context) => {
42580
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST, {
42581
+ symbol,
42582
+ context,
42583
+ });
42584
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
42585
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
42586
+ {
42587
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42588
+ riskName &&
42589
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
42590
+ riskList &&
42591
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
42592
+ actions &&
42593
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
42594
+ }
42595
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlCost(false, symbol, {
42596
+ strategyName: context.strategyName,
42597
+ exchangeName: context.exchangeName,
42598
+ frameName: "",
42599
+ });
42600
+ };
41802
42601
  /**
41803
42602
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
41804
42603
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -44176,7 +44975,7 @@ const createSearchIndex = () => {
44176
44975
  return { upsert, remove, list, search, read };
44177
44976
  };
44178
44977
 
44179
- const CREATE_KEY_FN$3 = (signalId, bucketName) => `${signalId}-${bucketName}`;
44978
+ const CREATE_KEY_FN$4 = (signalId, bucketName) => `${signalId}-${bucketName}`;
44180
44979
  const LIST_MEMORY_FN = ({ id, content }) => ({
44181
44980
  memoryId: id,
44182
44981
  content: content,
@@ -44498,7 +45297,7 @@ class MemoryDummyInstance {
44498
45297
  class MemoryAdapter {
44499
45298
  constructor() {
44500
45299
  this.MemoryFactory = MemoryPersistInstance;
44501
- this.getInstance = memoize(([signalId, bucketName]) => CREATE_KEY_FN$3(signalId, bucketName), (signalId, bucketName) => Reflect.construct(this.MemoryFactory, [signalId, bucketName]));
45300
+ this.getInstance = memoize(([signalId, bucketName]) => CREATE_KEY_FN$4(signalId, bucketName), (signalId, bucketName) => Reflect.construct(this.MemoryFactory, [signalId, bucketName]));
44502
45301
  /**
44503
45302
  * Activates the adapter by subscribing to signal lifecycle events.
44504
45303
  * Clears memoized instances for a signalId when it is cancelled or closed,
@@ -44509,7 +45308,7 @@ class MemoryAdapter {
44509
45308
  this.enable = singleshot(() => {
44510
45309
  backtest.loggerService.info(MEMORY_ADAPTER_METHOD_NAME_ENABLE);
44511
45310
  const handleDispose = (signalId) => {
44512
- const prefix = CREATE_KEY_FN$3(signalId, "");
45311
+ const prefix = CREATE_KEY_FN$4(signalId, "");
44513
45312
  for (const key of this.getInstance.keys()) {
44514
45313
  if (key.startsWith(prefix)) {
44515
45314
  const instance = this.getInstance.get(key);
@@ -44554,7 +45353,7 @@ class MemoryAdapter {
44554
45353
  bucketName: dto.bucketName,
44555
45354
  memoryId: dto.memoryId,
44556
45355
  });
44557
- const key = CREATE_KEY_FN$3(dto.signalId, dto.bucketName);
45356
+ const key = CREATE_KEY_FN$4(dto.signalId, dto.bucketName);
44558
45357
  const isInitial = !this.getInstance.has(key);
44559
45358
  const instance = this.getInstance(dto.signalId, dto.bucketName);
44560
45359
  await instance.waitForInit(isInitial);
@@ -44576,7 +45375,7 @@ class MemoryAdapter {
44576
45375
  bucketName: dto.bucketName,
44577
45376
  query: dto.query,
44578
45377
  });
44579
- const key = CREATE_KEY_FN$3(dto.signalId, dto.bucketName);
45378
+ const key = CREATE_KEY_FN$4(dto.signalId, dto.bucketName);
44580
45379
  const isInitial = !this.getInstance.has(key);
44581
45380
  const instance = this.getInstance(dto.signalId, dto.bucketName);
44582
45381
  await instance.waitForInit(isInitial);
@@ -44596,7 +45395,7 @@ class MemoryAdapter {
44596
45395
  signalId: dto.signalId,
44597
45396
  bucketName: dto.bucketName,
44598
45397
  });
44599
- const key = CREATE_KEY_FN$3(dto.signalId, dto.bucketName);
45398
+ const key = CREATE_KEY_FN$4(dto.signalId, dto.bucketName);
44600
45399
  const isInitial = !this.getInstance.has(key);
44601
45400
  const instance = this.getInstance(dto.signalId, dto.bucketName);
44602
45401
  await instance.waitForInit(isInitial);
@@ -44617,7 +45416,7 @@ class MemoryAdapter {
44617
45416
  bucketName: dto.bucketName,
44618
45417
  memoryId: dto.memoryId,
44619
45418
  });
44620
- const key = CREATE_KEY_FN$3(dto.signalId, dto.bucketName);
45419
+ const key = CREATE_KEY_FN$4(dto.signalId, dto.bucketName);
44621
45420
  const isInitial = !this.getInstance.has(key);
44622
45421
  const instance = this.getInstance(dto.signalId, dto.bucketName);
44623
45422
  await instance.waitForInit(isInitial);
@@ -44640,7 +45439,7 @@ class MemoryAdapter {
44640
45439
  bucketName: dto.bucketName,
44641
45440
  memoryId: dto.memoryId,
44642
45441
  });
44643
- const key = CREATE_KEY_FN$3(dto.signalId, dto.bucketName);
45442
+ const key = CREATE_KEY_FN$4(dto.signalId, dto.bucketName);
44644
45443
  const isInitial = !this.getInstance.has(key);
44645
45444
  const instance = this.getInstance(dto.signalId, dto.bucketName);
44646
45445
  await instance.waitForInit(isInitial);
@@ -44932,7 +45731,7 @@ async function removeMemory(dto) {
44932
45731
  });
44933
45732
  }
44934
45733
 
44935
- const CREATE_KEY_FN$2 = (signalId, bucketName) => `${signalId}-${bucketName}`;
45734
+ const CREATE_KEY_FN$3 = (signalId, bucketName) => `${signalId}-${bucketName}`;
44936
45735
  const DUMP_MEMORY_INSTANCE_METHOD_NAME_AGENT = "DumpMemoryInstance.dumpAgentAnswer";
44937
45736
  const DUMP_MEMORY_INSTANCE_METHOD_NAME_RECORD = "DumpMemoryInstance.dumpRecord";
44938
45737
  const DUMP_MEMORY_INSTANCE_METHOD_NAME_TABLE = "DumpMemoryInstance.dumpTable";
@@ -45552,7 +46351,7 @@ class DumpDummyInstance {
45552
46351
  class DumpAdapter {
45553
46352
  constructor() {
45554
46353
  this.DumpFactory = DumpMarkdownInstance;
45555
- this.getInstance = memoize(([signalId, bucketName]) => CREATE_KEY_FN$2(signalId, bucketName), (signalId, bucketName) => Reflect.construct(this.DumpFactory, [signalId, bucketName]));
46354
+ this.getInstance = memoize(([signalId, bucketName]) => CREATE_KEY_FN$3(signalId, bucketName), (signalId, bucketName) => Reflect.construct(this.DumpFactory, [signalId, bucketName]));
45556
46355
  /**
45557
46356
  * Activates the adapter by subscribing to signal lifecycle events.
45558
46357
  * Clears memoized instances for a signalId when it is cancelled or closed,
@@ -45563,7 +46362,7 @@ class DumpAdapter {
45563
46362
  this.enable = singleshot(() => {
45564
46363
  backtest.loggerService.info(DUMP_ADAPTER_METHOD_NAME_ENABLE);
45565
46364
  const handleDispose = (signalId) => {
45566
- const prefix = CREATE_KEY_FN$2(signalId, "");
46365
+ const prefix = CREATE_KEY_FN$3(signalId, "");
45567
46366
  for (const key of this.getInstance.keys()) {
45568
46367
  if (key.startsWith(prefix)) {
45569
46368
  const instance = this.getInstance.get(key);
@@ -48497,6 +49296,49 @@ PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, cont
48497
49296
  };
48498
49297
  const PositionSize = PositionSizeUtils;
48499
49298
 
49299
+ const METHOD_NAME_MOONBAG = "Position.moonbag";
49300
+ const METHOD_NAME_BRACKET = "Position.bracket";
49301
+ /**
49302
+ * Utilities for calculating take profit and stop loss price levels.
49303
+ * Automatically inverts direction based on position type (long/short).
49304
+ */
49305
+ class Position {
49306
+ }
49307
+ /**
49308
+ * Calculates levels for the "moonbag" strategy — fixed TP at 50% from the current price.
49309
+ * @param dto.position - position type: "long" or "short"
49310
+ * @param dto.currentPrice - current asset price
49311
+ * @param dto.percentStopLoss - stop loss percentage from 0 to 100
49312
+ * @returns priceTakeProfit and priceStopLoss in fiat
49313
+ */
49314
+ Position.moonbag = (dto) => {
49315
+ backtest.loggerService.log(METHOD_NAME_MOONBAG, { dto });
49316
+ const percentTakeProfit = 50;
49317
+ const sign = dto.position === "long" ? 1 : -1;
49318
+ return {
49319
+ position: dto.position,
49320
+ priceTakeProfit: dto.currentPrice * (1 + sign * percentTakeProfit / 100),
49321
+ priceStopLoss: dto.currentPrice * (1 - sign * dto.percentStopLoss / 100),
49322
+ };
49323
+ };
49324
+ /**
49325
+ * Calculates levels for a bracket order with custom TP and SL.
49326
+ * @param dto.position - position type: "long" or "short"
49327
+ * @param dto.currentPrice - current asset price
49328
+ * @param dto.percentStopLoss - stop loss percentage from 0 to 100
49329
+ * @param dto.percentTakeProfit - take profit percentage from 0 to 100
49330
+ * @returns priceTakeProfit and priceStopLoss in fiat
49331
+ */
49332
+ Position.bracket = (dto) => {
49333
+ backtest.loggerService.log(METHOD_NAME_BRACKET, { dto });
49334
+ const sign = dto.position === "long" ? 1 : -1;
49335
+ return {
49336
+ position: dto.position,
49337
+ priceTakeProfit: dto.currentPrice * (1 + sign * dto.percentTakeProfit / 100),
49338
+ priceStopLoss: dto.currentPrice * (1 - sign * dto.percentStopLoss / 100),
49339
+ };
49340
+ };
49341
+
48500
49342
  const PARTIAL_METHOD_NAME_GET_DATA = "PartialUtils.getData";
48501
49343
  const PARTIAL_METHOD_NAME_GET_REPORT = "PartialUtils.getReport";
48502
49344
  const PARTIAL_METHOD_NAME_DUMP = "PartialUtils.dump";
@@ -50699,7 +51541,7 @@ const StorageBacktest = new StorageBacktestAdapter();
50699
51541
  * Generates a unique key for notification identification.
50700
51542
  * @returns Random string identifier
50701
51543
  */
50702
- const CREATE_KEY_FN$1 = () => randomString();
51544
+ const CREATE_KEY_FN$2 = () => randomString();
50703
51545
  /**
50704
51546
  * Creates a notification model from signal tick result.
50705
51547
  * Handles opened, closed, scheduled, and cancelled signal actions.
@@ -50710,7 +51552,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
50710
51552
  if (data.action === "opened") {
50711
51553
  return {
50712
51554
  type: "signal.opened",
50713
- id: CREATE_KEY_FN$1(),
51555
+ id: CREATE_KEY_FN$2(),
50714
51556
  timestamp: data.signal.pendingAt,
50715
51557
  backtest: data.backtest,
50716
51558
  symbol: data.symbol,
@@ -50744,7 +51586,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
50744
51586
  const durationMin = Math.round(durationMs / 60000);
50745
51587
  return {
50746
51588
  type: "signal.closed",
50747
- id: CREATE_KEY_FN$1(),
51589
+ id: CREATE_KEY_FN$2(),
50748
51590
  timestamp: data.closeTimestamp,
50749
51591
  backtest: data.backtest,
50750
51592
  symbol: data.symbol,
@@ -50778,7 +51620,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
50778
51620
  if (data.action === "scheduled") {
50779
51621
  return {
50780
51622
  type: "signal.scheduled",
50781
- id: CREATE_KEY_FN$1(),
51623
+ id: CREATE_KEY_FN$2(),
50782
51624
  timestamp: data.signal.scheduledAt,
50783
51625
  backtest: data.backtest,
50784
51626
  symbol: data.symbol,
@@ -50811,7 +51653,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
50811
51653
  const durationMin = Math.round(durationMs / 60000);
50812
51654
  return {
50813
51655
  type: "signal.cancelled",
50814
- id: CREATE_KEY_FN$1(),
51656
+ id: CREATE_KEY_FN$2(),
50815
51657
  timestamp: data.closeTimestamp,
50816
51658
  backtest: data.backtest,
50817
51659
  symbol: data.symbol,
@@ -50844,7 +51686,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
50844
51686
  */
50845
51687
  const CREATE_PARTIAL_PROFIT_NOTIFICATION_FN = (data) => ({
50846
51688
  type: "partial_profit.available",
50847
- id: CREATE_KEY_FN$1(),
51689
+ id: CREATE_KEY_FN$2(),
50848
51690
  timestamp: data.timestamp,
50849
51691
  backtest: data.backtest,
50850
51692
  symbol: data.symbol,
@@ -50879,7 +51721,7 @@ const CREATE_PARTIAL_PROFIT_NOTIFICATION_FN = (data) => ({
50879
51721
  */
50880
51722
  const CREATE_PARTIAL_LOSS_NOTIFICATION_FN = (data) => ({
50881
51723
  type: "partial_loss.available",
50882
- id: CREATE_KEY_FN$1(),
51724
+ id: CREATE_KEY_FN$2(),
50883
51725
  timestamp: data.timestamp,
50884
51726
  backtest: data.backtest,
50885
51727
  symbol: data.symbol,
@@ -50914,7 +51756,7 @@ const CREATE_PARTIAL_LOSS_NOTIFICATION_FN = (data) => ({
50914
51756
  */
50915
51757
  const CREATE_BREAKEVEN_NOTIFICATION_FN = (data) => ({
50916
51758
  type: "breakeven.available",
50917
- id: CREATE_KEY_FN$1(),
51759
+ id: CREATE_KEY_FN$2(),
50918
51760
  timestamp: data.timestamp,
50919
51761
  backtest: data.backtest,
50920
51762
  symbol: data.symbol,
@@ -50952,7 +51794,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
50952
51794
  if (data.action === "partial-profit") {
50953
51795
  return {
50954
51796
  type: "partial_profit.commit",
50955
- id: CREATE_KEY_FN$1(),
51797
+ id: CREATE_KEY_FN$2(),
50956
51798
  timestamp: data.timestamp,
50957
51799
  backtest: data.backtest,
50958
51800
  symbol: data.symbol,
@@ -50984,7 +51826,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
50984
51826
  if (data.action === "partial-loss") {
50985
51827
  return {
50986
51828
  type: "partial_loss.commit",
50987
- id: CREATE_KEY_FN$1(),
51829
+ id: CREATE_KEY_FN$2(),
50988
51830
  timestamp: data.timestamp,
50989
51831
  backtest: data.backtest,
50990
51832
  symbol: data.symbol,
@@ -51016,7 +51858,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51016
51858
  if (data.action === "breakeven") {
51017
51859
  return {
51018
51860
  type: "breakeven.commit",
51019
- id: CREATE_KEY_FN$1(),
51861
+ id: CREATE_KEY_FN$2(),
51020
51862
  timestamp: data.timestamp,
51021
51863
  backtest: data.backtest,
51022
51864
  symbol: data.symbol,
@@ -51047,7 +51889,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51047
51889
  if (data.action === "trailing-stop") {
51048
51890
  return {
51049
51891
  type: "trailing_stop.commit",
51050
- id: CREATE_KEY_FN$1(),
51892
+ id: CREATE_KEY_FN$2(),
51051
51893
  timestamp: data.timestamp,
51052
51894
  backtest: data.backtest,
51053
51895
  symbol: data.symbol,
@@ -51079,7 +51921,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51079
51921
  if (data.action === "trailing-take") {
51080
51922
  return {
51081
51923
  type: "trailing_take.commit",
51082
- id: CREATE_KEY_FN$1(),
51924
+ id: CREATE_KEY_FN$2(),
51083
51925
  timestamp: data.timestamp,
51084
51926
  backtest: data.backtest,
51085
51927
  symbol: data.symbol,
@@ -51111,7 +51953,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51111
51953
  if (data.action === "activate-scheduled") {
51112
51954
  return {
51113
51955
  type: "activate_scheduled.commit",
51114
- id: CREATE_KEY_FN$1(),
51956
+ id: CREATE_KEY_FN$2(),
51115
51957
  timestamp: data.timestamp,
51116
51958
  backtest: data.backtest,
51117
51959
  symbol: data.symbol,
@@ -51143,7 +51985,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51143
51985
  if (data.action === "average-buy") {
51144
51986
  return {
51145
51987
  type: "average_buy.commit",
51146
- id: CREATE_KEY_FN$1(),
51988
+ id: CREATE_KEY_FN$2(),
51147
51989
  timestamp: data.timestamp,
51148
51990
  backtest: data.backtest,
51149
51991
  symbol: data.symbol,
@@ -51176,7 +52018,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51176
52018
  if (data.action === "cancel-scheduled") {
51177
52019
  return {
51178
52020
  type: "cancel_scheduled.commit",
51179
- id: CREATE_KEY_FN$1(),
52021
+ id: CREATE_KEY_FN$2(),
51180
52022
  timestamp: data.timestamp,
51181
52023
  backtest: data.backtest,
51182
52024
  symbol: data.symbol,
@@ -51199,7 +52041,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51199
52041
  if (data.action === "close-pending") {
51200
52042
  return {
51201
52043
  type: "close_pending.commit",
51202
- id: CREATE_KEY_FN$1(),
52044
+ id: CREATE_KEY_FN$2(),
51203
52045
  timestamp: data.timestamp,
51204
52046
  backtest: data.backtest,
51205
52047
  symbol: data.symbol,
@@ -51231,7 +52073,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
51231
52073
  if (data.action === "signal-open") {
51232
52074
  return {
51233
52075
  type: "signal_sync.open",
51234
- id: CREATE_KEY_FN$1(),
52076
+ id: CREATE_KEY_FN$2(),
51235
52077
  timestamp: data.timestamp,
51236
52078
  backtest: data.backtest,
51237
52079
  symbol: data.symbol,
@@ -51263,7 +52105,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
51263
52105
  if (data.action === "signal-close") {
51264
52106
  return {
51265
52107
  type: "signal_sync.close",
51266
- id: CREATE_KEY_FN$1(),
52108
+ id: CREATE_KEY_FN$2(),
51267
52109
  timestamp: data.timestamp,
51268
52110
  backtest: data.backtest,
51269
52111
  symbol: data.symbol,
@@ -51301,7 +52143,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
51301
52143
  */
51302
52144
  const CREATE_RISK_NOTIFICATION_FN = (data) => ({
51303
52145
  type: "risk.rejection",
51304
- id: CREATE_KEY_FN$1(),
52146
+ id: CREATE_KEY_FN$2(),
51305
52147
  timestamp: data.timestamp,
51306
52148
  backtest: data.backtest,
51307
52149
  symbol: data.symbol,
@@ -51327,7 +52169,7 @@ const CREATE_RISK_NOTIFICATION_FN = (data) => ({
51327
52169
  */
51328
52170
  const CREATE_ERROR_NOTIFICATION_FN = (error) => ({
51329
52171
  type: "error.info",
51330
- id: CREATE_KEY_FN$1(),
52172
+ id: CREATE_KEY_FN$2(),
51331
52173
  error: errorData(error),
51332
52174
  message: getErrorMessage(error),
51333
52175
  backtest: false,
@@ -51339,7 +52181,7 @@ const CREATE_ERROR_NOTIFICATION_FN = (error) => ({
51339
52181
  */
51340
52182
  const CREATE_CRITICAL_ERROR_NOTIFICATION_FN = (error) => ({
51341
52183
  type: "error.critical",
51342
- id: CREATE_KEY_FN$1(),
52184
+ id: CREATE_KEY_FN$2(),
51343
52185
  error: errorData(error),
51344
52186
  message: getErrorMessage(error),
51345
52187
  backtest: false,
@@ -51351,7 +52193,7 @@ const CREATE_CRITICAL_ERROR_NOTIFICATION_FN = (error) => ({
51351
52193
  */
51352
52194
  const CREATE_VALIDATION_ERROR_NOTIFICATION_FN = (error) => ({
51353
52195
  type: "error.validation",
51354
- id: CREATE_KEY_FN$1(),
52196
+ id: CREATE_KEY_FN$2(),
51355
52197
  error: errorData(error),
51356
52198
  message: getErrorMessage(error),
51357
52199
  backtest: false,
@@ -52780,7 +53622,7 @@ const NotificationLive = new NotificationLiveAdapter();
52780
53622
  */
52781
53623
  const NotificationBacktest = new NotificationBacktestAdapter();
52782
53624
 
52783
- const CACHE_METHOD_NAME_RUN = "CacheInstance.run";
53625
+ const CACHE_METHOD_NAME_RUN = "CacheFnInstance.run";
52784
53626
  const CACHE_METHOD_NAME_FN = "CacheUtils.fn";
52785
53627
  const CACHE_METHOD_NAME_FN_CLEAR = "CacheUtils.fn.clear";
52786
53628
  const CACHE_METHOD_NAME_FN_GC = "CacheUtils.fn.gc";
@@ -52789,8 +53631,8 @@ const CACHE_METHOD_NAME_FILE_CLEAR = "CacheUtils.file.clear";
52789
53631
  const CACHE_METHOD_NAME_DISPOSE = "CacheUtils.dispose";
52790
53632
  const CACHE_METHOD_NAME_CLEAR = "CacheUtils.clear";
52791
53633
  const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
52792
- const MS_PER_MINUTE = 60000;
52793
- const INTERVAL_MINUTES = {
53634
+ const MS_PER_MINUTE$1 = 60000;
53635
+ const INTERVAL_MINUTES$1 = {
52794
53636
  "1m": 1,
52795
53637
  "3m": 3,
52796
53638
  "5m": 5,
@@ -52824,12 +53666,12 @@ const INTERVAL_MINUTES = {
52824
53666
  * // Returns timestamp for 2025-10-01T01:00:00Z
52825
53667
  * ```
52826
53668
  */
52827
- const align = (timestamp, interval) => {
52828
- const intervalMinutes = INTERVAL_MINUTES[interval];
53669
+ const align$1 = (timestamp, interval) => {
53670
+ const intervalMinutes = INTERVAL_MINUTES$1[interval];
52829
53671
  if (!intervalMinutes) {
52830
53672
  throw new Error(`align: unknown interval=${interval}`);
52831
53673
  }
52832
- const intervalMs = intervalMinutes * MS_PER_MINUTE;
53674
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
52833
53675
  return Math.floor(timestamp / intervalMs) * intervalMs;
52834
53676
  };
52835
53677
  /**
@@ -52840,7 +53682,7 @@ const align = (timestamp, interval) => {
52840
53682
  * @param backtest - Whether running in backtest mode
52841
53683
  * @returns Cache key string
52842
53684
  */
52843
- const CREATE_KEY_FN = (strategyName, exchangeName, frameName, backtest) => {
53685
+ const CREATE_KEY_FN$1 = (strategyName, exchangeName, frameName, backtest) => {
52844
53686
  const parts = [strategyName, exchangeName];
52845
53687
  if (frameName)
52846
53688
  parts.push(frameName);
@@ -52864,16 +53706,16 @@ const NEVER_VALUE = Symbol("never");
52864
53706
  *
52865
53707
  * @example
52866
53708
  * ```typescript
52867
- * const instance = new CacheInstance(myExpensiveFunction, "1h");
53709
+ * const instance = new CacheFnInstance(myExpensiveFunction, "1h");
52868
53710
  * const result = instance.run(arg1, arg2); // Computed
52869
53711
  * const result2 = instance.run(arg1, arg2); // Cached (within same hour)
52870
53712
  * // After 1 hour passes
52871
53713
  * const result3 = instance.run(arg1, arg2); // Recomputed
52872
53714
  * ```
52873
53715
  */
52874
- class CacheInstance {
53716
+ class CacheFnInstance {
52875
53717
  /**
52876
- * Creates a new CacheInstance for a specific function and interval.
53718
+ * Creates a new CacheFnInstance for a specific function and interval.
52877
53719
  *
52878
53720
  * @param fn - Function to cache
52879
53721
  * @param interval - Candle interval for cache invalidation (e.g., "1m", "1h")
@@ -52910,7 +53752,7 @@ class CacheInstance {
52910
53752
  *
52911
53753
  * @example
52912
53754
  * ```typescript
52913
- * const instance = new CacheInstance(calculateIndicator, "15m");
53755
+ * const instance = new CacheFnInstance(calculateIndicator, "15m");
52914
53756
  * const result = instance.run("BTCUSDT", 100);
52915
53757
  * console.log(result.value); // Calculated value
52916
53758
  * console.log(result.when); // Cache timestamp
@@ -52918,26 +53760,26 @@ class CacheInstance {
52918
53760
  */
52919
53761
  this.run = (...args) => {
52920
53762
  backtest.loggerService.debug(CACHE_METHOD_NAME_RUN, { args });
52921
- const step = INTERVAL_MINUTES[this.interval];
53763
+ const step = INTERVAL_MINUTES$1[this.interval];
52922
53764
  {
52923
53765
  if (!MethodContextService.hasContext()) {
52924
- throw new Error("CacheInstance run requires method context");
53766
+ throw new Error("CacheFnInstance run requires method context");
52925
53767
  }
52926
53768
  if (!ExecutionContextService.hasContext()) {
52927
- throw new Error("CacheInstance run requires execution context");
53769
+ throw new Error("CacheFnInstance run requires execution context");
52928
53770
  }
52929
53771
  if (!step) {
52930
- throw new Error(`CacheInstance unknown cache ttl interval=${this.interval}`);
53772
+ throw new Error(`CacheFnInstance unknown cache ttl interval=${this.interval}`);
52931
53773
  }
52932
53774
  }
52933
- const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
53775
+ const contextKey = CREATE_KEY_FN$1(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
52934
53776
  const argKey = String(this.key(args));
52935
53777
  const key = `${contextKey}:${argKey}`;
52936
53778
  const currentWhen = backtest.executionContextService.context.when;
52937
53779
  const cached = this._cacheMap.get(key);
52938
53780
  if (cached) {
52939
- const currentAligned = align(currentWhen.getTime(), this.interval);
52940
- const cachedAligned = align(cached.when.getTime(), this.interval);
53781
+ const currentAligned = align$1(currentWhen.getTime(), this.interval);
53782
+ const cachedAligned = align$1(cached.when.getTime(), this.interval);
52941
53783
  if (currentAligned === cachedAligned) {
52942
53784
  return cached;
52943
53785
  }
@@ -52960,7 +53802,7 @@ class CacheInstance {
52960
53802
  *
52961
53803
  * @example
52962
53804
  * ```typescript
52963
- * const instance = new CacheInstance(calculateIndicator, "1h");
53805
+ * const instance = new CacheFnInstance(calculateIndicator, "1h");
52964
53806
  * const result1 = instance.run("BTCUSDT", 14); // Computed
52965
53807
  * const result2 = instance.run("BTCUSDT", 14); // Cached
52966
53808
  *
@@ -52970,7 +53812,7 @@ class CacheInstance {
52970
53812
  * ```
52971
53813
  */
52972
53814
  this.clear = () => {
52973
- const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
53815
+ const contextKey = CREATE_KEY_FN$1(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
52974
53816
  const prefix = `${contextKey}:`;
52975
53817
  for (const key of this._cacheMap.keys()) {
52976
53818
  if (key.startsWith(prefix)) {
@@ -52990,7 +53832,7 @@ class CacheInstance {
52990
53832
  *
52991
53833
  * @example
52992
53834
  * ```typescript
52993
- * const instance = new CacheInstance(calculateIndicator, "1h");
53835
+ * const instance = new CacheFnInstance(calculateIndicator, "1h");
52994
53836
  * instance.run("BTCUSDT", 14); // Cached at 10:00
52995
53837
  * instance.run("ETHUSDT", 14); // Cached at 10:00
52996
53838
  * // Time passes to 11:00
@@ -52999,10 +53841,10 @@ class CacheInstance {
52999
53841
  */
53000
53842
  this.gc = () => {
53001
53843
  const currentWhen = backtest.executionContextService.context.when;
53002
- const currentAligned = align(currentWhen.getTime(), this.interval);
53844
+ const currentAligned = align$1(currentWhen.getTime(), this.interval);
53003
53845
  let removed = 0;
53004
53846
  for (const [key, cached] of this._cacheMap.entries()) {
53005
- const cachedAligned = align(cached.when.getTime(), this.interval);
53847
+ const cachedAligned = align$1(cached.when.getTime(), this.interval);
53006
53848
  if (currentAligned !== cachedAligned) {
53007
53849
  this._cacheMap.delete(key);
53008
53850
  removed++;
@@ -53077,7 +53919,7 @@ class CacheFileInstance {
53077
53919
  */
53078
53920
  this.run = async (...args) => {
53079
53921
  backtest.loggerService.debug(CACHE_FILE_INSTANCE_METHOD_NAME_RUN, { args });
53080
- const step = INTERVAL_MINUTES[this.interval];
53922
+ const step = INTERVAL_MINUTES$1[this.interval];
53081
53923
  {
53082
53924
  if (!MethodContextService.hasContext()) {
53083
53925
  throw new Error("CacheFileInstance run requires method context");
@@ -53091,7 +53933,7 @@ class CacheFileInstance {
53091
53933
  }
53092
53934
  const [symbol, ...rest] = args;
53093
53935
  const { when } = backtest.executionContextService.context;
53094
- const alignedTs = align(when.getTime(), this.interval);
53936
+ const alignedTs = align$1(when.getTime(), this.interval);
53095
53937
  const bucket = `${this.name}_${this.interval}_${this.index}`;
53096
53938
  const entityKey = this.key([symbol, alignedTs, ...rest]);
53097
53939
  const cached = await PersistMeasureAdapter.readMeasureData(bucket, entityKey);
@@ -53099,9 +53941,19 @@ class CacheFileInstance {
53099
53941
  return cached.data;
53100
53942
  }
53101
53943
  const result = await this.fn.call(null, ...args);
53102
- await PersistMeasureAdapter.writeMeasureData({ id: entityKey, data: result }, bucket, entityKey);
53944
+ await PersistMeasureAdapter.writeMeasureData({ id: entityKey, data: result, removed: false }, bucket, entityKey);
53103
53945
  return result;
53104
53946
  };
53947
+ /**
53948
+ * Soft-delete all persisted records for this instance's bucket.
53949
+ * After this call the next `run()` will recompute and re-cache the value.
53950
+ */
53951
+ this.clear = async () => {
53952
+ const bucket = `${this.name}_${this.interval}_${this.index}`;
53953
+ for await (const key of PersistMeasureAdapter.listMeasureData(bucket)) {
53954
+ await PersistMeasureAdapter.removeMeasureData(bucket, key);
53955
+ }
53956
+ };
53105
53957
  this.index = CacheFileInstance.createIndex();
53106
53958
  }
53107
53959
  }
@@ -53125,10 +53977,10 @@ CacheFileInstance._indexCounter = 0;
53125
53977
  class CacheUtils {
53126
53978
  constructor() {
53127
53979
  /**
53128
- * Memoized function to get or create CacheInstance for a function.
53980
+ * Memoized function to get or create CacheFnInstance for a function.
53129
53981
  * Each function gets its own isolated cache instance.
53130
53982
  */
53131
- this._getFnInstance = memoize(([run]) => run, (run, interval, key) => new CacheInstance(run, interval, key));
53983
+ this._getFnInstance = memoize(([run]) => run, (run, interval, key) => new CacheFnInstance(run, interval, key));
53132
53984
  /**
53133
53985
  * Memoized function to get or create CacheFileInstance for an async function.
53134
53986
  * Each function gets its own isolated file-cache instance.
@@ -53237,31 +54089,34 @@ class CacheUtils {
53237
54089
  */
53238
54090
  this.file = (run, context) => {
53239
54091
  backtest.loggerService.info(CACHE_METHOD_NAME_FILE, { context });
54092
+ {
54093
+ this._getFileInstance(run, context.interval, context.name, context.key);
54094
+ }
53240
54095
  const wrappedFn = (...args) => {
53241
54096
  const instance = this._getFileInstance(run, context.interval, context.name, context.key);
53242
54097
  return instance.run(...args);
53243
54098
  };
53244
- wrappedFn.clear = () => {
54099
+ wrappedFn.clear = async () => {
53245
54100
  backtest.loggerService.info(CACHE_METHOD_NAME_FILE_CLEAR);
53246
- this._getFileInstance.clear(run);
54101
+ await this._getFileInstance.get(run)?.clear();
53247
54102
  };
53248
54103
  return wrappedFn;
53249
54104
  };
53250
54105
  /**
53251
- * Dispose (remove) the memoized CacheInstance for a specific function.
54106
+ * Dispose (remove) the memoized CacheFnInstance for a specific function.
53252
54107
  *
53253
- * Removes the CacheInstance from the internal memoization cache, discarding all cached
54108
+ * Removes the CacheFnInstance from the internal memoization cache, discarding all cached
53254
54109
  * results across all contexts (all strategy/exchange/mode combinations) for that function.
53255
- * The next call to the wrapped function will create a fresh CacheInstance.
54110
+ * The next call to the wrapped function will create a fresh CacheFnInstance.
53256
54111
  *
53257
54112
  * @template T - Function type
53258
- * @param run - Function whose CacheInstance should be disposed.
54113
+ * @param run - Function whose CacheFnInstance should be disposed.
53259
54114
  *
53260
54115
  * @example
53261
54116
  * ```typescript
53262
54117
  * const cachedFn = Cache.fn(calculateIndicator, { interval: "1h" });
53263
54118
  *
53264
- * // Dispose CacheInstance for a specific function
54119
+ * // Dispose CacheFnInstance for a specific function
53265
54120
  * Cache.dispose(calculateIndicator);
53266
54121
  * ```
53267
54122
  */
@@ -53275,7 +54130,7 @@ class CacheUtils {
53275
54130
  }
53276
54131
  };
53277
54132
  /**
53278
- * Clears all memoized CacheInstance and CacheFileInstance objects.
54133
+ * Clears all memoized CacheFnInstance and CacheFileInstance objects.
53279
54134
  * Call this when process.cwd() changes between strategy iterations
53280
54135
  * so new instances are created with the updated base path.
53281
54136
  */
@@ -53305,6 +54160,425 @@ class CacheUtils {
53305
54160
  */
53306
54161
  const Cache = new CacheUtils();
53307
54162
 
54163
+ const INTERVAL_METHOD_NAME_RUN = "IntervalFnInstance.run";
54164
+ const INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN = "IntervalFileInstance.run";
54165
+ const INTERVAL_METHOD_NAME_FN = "IntervalUtils.fn";
54166
+ const INTERVAL_METHOD_NAME_FN_CLEAR = "IntervalUtils.fn.clear";
54167
+ const INTERVAL_METHOD_NAME_FILE = "IntervalUtils.file";
54168
+ const INTERVAL_METHOD_NAME_FILE_CLEAR = "IntervalUtils.file.clear";
54169
+ const INTERVAL_METHOD_NAME_DISPOSE = "IntervalUtils.dispose";
54170
+ const INTERVAL_METHOD_NAME_CLEAR = "IntervalUtils.clear";
54171
+ const MS_PER_MINUTE = 60000;
54172
+ const INTERVAL_MINUTES = {
54173
+ "1m": 1,
54174
+ "3m": 3,
54175
+ "5m": 5,
54176
+ "15m": 15,
54177
+ "30m": 30,
54178
+ "1h": 60,
54179
+ "2h": 120,
54180
+ "4h": 240,
54181
+ "6h": 360,
54182
+ "8h": 480,
54183
+ "1d": 1440,
54184
+ "1w": 10080,
54185
+ };
54186
+ /**
54187
+ * Aligns timestamp down to the nearest interval boundary.
54188
+ * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
54189
+ *
54190
+ * @param timestamp - Timestamp in milliseconds
54191
+ * @param interval - Candle interval
54192
+ * @returns Aligned timestamp rounded down to interval boundary
54193
+ * @throws Error if interval is unknown
54194
+ *
54195
+ * @example
54196
+ * ```typescript
54197
+ * // Align to 15-minute boundary
54198
+ * const aligned = align(new Date("2025-10-01T00:35:00Z").getTime(), "15m");
54199
+ * // Returns timestamp for 2025-10-01T00:30:00Z
54200
+ * ```
54201
+ */
54202
+ const align = (timestamp, interval) => {
54203
+ const intervalMinutes = INTERVAL_MINUTES[interval];
54204
+ if (!intervalMinutes) {
54205
+ throw new Error(`align: unknown interval=${interval}`);
54206
+ }
54207
+ const intervalMs = intervalMinutes * MS_PER_MINUTE;
54208
+ return Math.floor(timestamp / intervalMs) * intervalMs;
54209
+ };
54210
+ /**
54211
+ * Build a context key string from strategy name, exchange name, frame name, and execution mode.
54212
+ *
54213
+ * @param strategyName - Name of the strategy
54214
+ * @param exchangeName - Name of the exchange
54215
+ * @param frameName - Name of the backtest frame (omitted in live mode)
54216
+ * @param isBacktest - Whether running in backtest mode
54217
+ * @returns Context key string
54218
+ */
54219
+ const CREATE_KEY_FN = (strategyName, exchangeName, frameName, isBacktest) => {
54220
+ const parts = [strategyName, exchangeName];
54221
+ if (frameName)
54222
+ parts.push(frameName);
54223
+ parts.push(isBacktest ? "backtest" : "live");
54224
+ return parts.join(":");
54225
+ };
54226
+ /**
54227
+ * Instance class for firing a function exactly once per interval boundary.
54228
+ *
54229
+ * On the first call within a new interval the wrapped function is invoked and
54230
+ * its result is returned. Every subsequent call within the same interval returns
54231
+ * `null` without invoking the function again.
54232
+ * If the function itself returns `null`, the interval countdown does not start —
54233
+ * the next call will retry the function.
54234
+ *
54235
+ * State is kept in memory; use `IntervalFileInstance` for persistence across restarts.
54236
+ *
54237
+ * @example
54238
+ * ```typescript
54239
+ * const instance = new IntervalFnInstance(mySignalFn, "1h");
54240
+ * await instance.run("BTCUSDT"); // → T | null (fn called)
54241
+ * await instance.run("BTCUSDT"); // → null (skipped, same interval)
54242
+ * // After 1 hour passes:
54243
+ * await instance.run("BTCUSDT"); // → T | null (fn called again)
54244
+ * ```
54245
+ */
54246
+ class IntervalFnInstance {
54247
+ /**
54248
+ * Creates a new IntervalFnInstance.
54249
+ *
54250
+ * @param fn - Function to fire once per interval
54251
+ * @param interval - Candle interval that controls the firing boundary
54252
+ */
54253
+ constructor(fn, interval) {
54254
+ this.fn = fn;
54255
+ this.interval = interval;
54256
+ /** Stores the last aligned timestamp per context+symbol key. */
54257
+ this._stateMap = new Map();
54258
+ /**
54259
+ * Execute the signal function with once-per-interval enforcement.
54260
+ *
54261
+ * Algorithm:
54262
+ * 1. Align the current execution context `when` to the interval boundary.
54263
+ * 2. If the stored aligned timestamp for this context+symbol equals the current one → return `null`.
54264
+ * 3. Otherwise call `fn`. If it returns a non-null signal, record the aligned timestamp and return
54265
+ * the signal. If it returns `null`, leave state unchanged so the next call retries.
54266
+ *
54267
+ * Requires active method context and execution context.
54268
+ *
54269
+ * @param symbol - Trading pair symbol (e.g. "BTCUSDT")
54270
+ * @returns The value returned by `fn` on the first non-null fire, `null` on all subsequent calls
54271
+ * within the same interval or when `fn` itself returned `null`
54272
+ * @throws Error if method context, execution context, or interval is missing
54273
+ */
54274
+ this.run = async (symbol) => {
54275
+ backtest.loggerService.debug(INTERVAL_METHOD_NAME_RUN, { symbol });
54276
+ const step = INTERVAL_MINUTES[this.interval];
54277
+ {
54278
+ if (!MethodContextService.hasContext()) {
54279
+ throw new Error("IntervalFnInstance run requires method context");
54280
+ }
54281
+ if (!ExecutionContextService.hasContext()) {
54282
+ throw new Error("IntervalFnInstance run requires execution context");
54283
+ }
54284
+ if (!step) {
54285
+ throw new Error(`IntervalFnInstance unknown interval=${this.interval}`);
54286
+ }
54287
+ }
54288
+ const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
54289
+ const key = `${contextKey}:${symbol}`;
54290
+ const currentWhen = backtest.executionContextService.context.when;
54291
+ const currentAligned = align(currentWhen.getTime(), this.interval);
54292
+ if (this._stateMap.get(key) === currentAligned) {
54293
+ return null;
54294
+ }
54295
+ const result = await this.fn(symbol, currentWhen);
54296
+ if (result !== null) {
54297
+ this._stateMap.set(key, currentAligned);
54298
+ }
54299
+ return result;
54300
+ };
54301
+ /**
54302
+ * Clear fired-interval state for the current execution context.
54303
+ *
54304
+ * Removes all entries for the current strategy/exchange/frame/mode combination
54305
+ * from this instance's state map. The next `run()` call will invoke the function again.
54306
+ *
54307
+ * Requires active method context and execution context.
54308
+ */
54309
+ this.clear = () => {
54310
+ const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
54311
+ const prefix = `${contextKey}:`;
54312
+ for (const key of this._stateMap.keys()) {
54313
+ if (key.startsWith(prefix)) {
54314
+ this._stateMap.delete(key);
54315
+ }
54316
+ }
54317
+ };
54318
+ }
54319
+ }
54320
+ /**
54321
+ * Instance class for firing an async function exactly once per interval boundary,
54322
+ * with the fired state persisted to disk via `PersistIntervalAdapter`.
54323
+ *
54324
+ * On the first call within a new interval the wrapped function is invoked.
54325
+ * If it returns a non-null signal, that result is written to disk and returned.
54326
+ * Every subsequent call within the same interval returns `null` (record exists on disk).
54327
+ * If the function returns `null`, nothing is written and the next call retries.
54328
+ *
54329
+ * Fired state survives process restarts — unlike `IntervalFnInstance` which is in-memory only.
54330
+ *
54331
+ * @template T - Async function type: `(symbol: string, ...args) => Promise<R | null>`
54332
+ *
54333
+ * @example
54334
+ * ```typescript
54335
+ * const instance = new IntervalFileInstance(fetchSignal, "1h", "mySignal");
54336
+ * await instance.run("BTCUSDT"); // → R | null (fn called, result written to disk)
54337
+ * await instance.run("BTCUSDT"); // → null (record exists, already fired)
54338
+ * ```
54339
+ */
54340
+ class IntervalFileInstance {
54341
+ /**
54342
+ * Allocates a new unique index. Called once in the constructor to give each
54343
+ * IntervalFileInstance its own namespace in the persistent key space.
54344
+ */
54345
+ static createIndex() {
54346
+ return IntervalFileInstance._indexCounter++;
54347
+ }
54348
+ /**
54349
+ * Resets the index counter to zero.
54350
+ * Call this when clearing all instances (e.g. on `IntervalUtils.clear()`).
54351
+ */
54352
+ static clearCounter() {
54353
+ IntervalFileInstance._indexCounter = 0;
54354
+ }
54355
+ /**
54356
+ * Creates a new IntervalFileInstance.
54357
+ *
54358
+ * @param fn - Async signal function to fire once per interval
54359
+ * @param interval - Candle interval that controls the firing boundary
54360
+ * @param name - Human-readable bucket name used as the directory prefix
54361
+ */
54362
+ constructor(fn, interval, name) {
54363
+ this.fn = fn;
54364
+ this.interval = interval;
54365
+ this.name = name;
54366
+ /**
54367
+ * Execute the async function with persistent once-per-interval enforcement.
54368
+ *
54369
+ * Algorithm:
54370
+ * 1. Build bucket = `${name}_${interval}_${index}` — fixed per instance, used as directory name.
54371
+ * 2. Align execution context `when` to interval boundary → `alignedTs`.
54372
+ * 3. Build entity key = `${symbol}_${alignedTs}`.
54373
+ * 4. Try to read from `PersistIntervalAdapter` using (bucket, entityKey).
54374
+ * 5. On hit — return `null` (interval already fired).
54375
+ * 6. On miss — call `fn`. If non-null, write to disk and return result. If null, skip write and return null.
54376
+ *
54377
+ * Requires active method context and execution context.
54378
+ *
54379
+ * @param symbol - Trading pair symbol (e.g. "BTCUSDT")
54380
+ * @returns The value on the first non-null fire, `null` if already fired this interval
54381
+ * or if `fn` itself returned `null`
54382
+ * @throws Error if method context, execution context, or interval is missing
54383
+ */
54384
+ this.run = async (symbol) => {
54385
+ backtest.loggerService.debug(INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN, { symbol });
54386
+ const step = INTERVAL_MINUTES[this.interval];
54387
+ {
54388
+ if (!MethodContextService.hasContext()) {
54389
+ throw new Error("IntervalFileInstance run requires method context");
54390
+ }
54391
+ if (!ExecutionContextService.hasContext()) {
54392
+ throw new Error("IntervalFileInstance run requires execution context");
54393
+ }
54394
+ if (!step) {
54395
+ throw new Error(`IntervalFileInstance unknown interval=${this.interval}`);
54396
+ }
54397
+ }
54398
+ const { when } = backtest.executionContextService.context;
54399
+ const alignedTs = align(when.getTime(), this.interval);
54400
+ const bucket = `${this.name}_${this.interval}_${this.index}`;
54401
+ const entityKey = `${symbol}_${alignedTs}`;
54402
+ const cached = await PersistIntervalAdapter.readIntervalData(bucket, entityKey);
54403
+ if (cached !== null) {
54404
+ return null;
54405
+ }
54406
+ const result = await this.fn(symbol, when);
54407
+ if (result !== null) {
54408
+ await PersistIntervalAdapter.writeIntervalData({ id: entityKey, data: result, removed: false }, bucket, entityKey);
54409
+ }
54410
+ return result;
54411
+ };
54412
+ /**
54413
+ * Soft-delete all persisted records for this instance's bucket.
54414
+ * After this call the function will fire again on the next `run()`.
54415
+ */
54416
+ this.clear = async () => {
54417
+ const bucket = `${this.name}_${this.interval}_${this.index}`;
54418
+ for await (const key of PersistIntervalAdapter.listIntervalData(bucket)) {
54419
+ await PersistIntervalAdapter.removeIntervalData(bucket, key);
54420
+ }
54421
+ };
54422
+ this.index = IntervalFileInstance.createIndex();
54423
+ }
54424
+ }
54425
+ /** Global counter — incremented once per IntervalFileInstance construction. */
54426
+ IntervalFileInstance._indexCounter = 0;
54427
+ /**
54428
+ * Utility class for wrapping signal functions with once-per-interval firing.
54429
+ * Provides two modes: in-memory (`fn`) and persistent file-based (`file`).
54430
+ * Exported as singleton instance `Interval` for convenient usage.
54431
+ *
54432
+ * @example
54433
+ * ```typescript
54434
+ * import { Interval } from "./classes/Interval";
54435
+ *
54436
+ * const fireOncePerHour = Interval.fn(mySignalFn, { interval: "1h" });
54437
+ * await fireOncePerHour("BTCUSDT"); // fn called — returns its result
54438
+ * await fireOncePerHour("BTCUSDT"); // returns null (same interval)
54439
+ * ```
54440
+ */
54441
+ class IntervalUtils {
54442
+ constructor() {
54443
+ /**
54444
+ * Memoized factory to get or create an `IntervalFnInstance` for a function.
54445
+ * Each function reference gets its own isolated instance.
54446
+ */
54447
+ this._getInstance = memoize(([run]) => run, (run, interval) => new IntervalFnInstance(run, interval));
54448
+ /**
54449
+ * Memoized factory to get or create an `IntervalFileInstance` for an async function.
54450
+ * Each function reference gets its own isolated persistent instance.
54451
+ */
54452
+ this._getFileInstance = memoize(([run]) => run, (run, interval, name) => new IntervalFileInstance(run, interval, name));
54453
+ /**
54454
+ * Wrap a signal function with in-memory once-per-interval firing.
54455
+ *
54456
+ * Returns a wrapped version of the function that fires at most once per interval boundary.
54457
+ * If the function returns `null`, the countdown does not start and the next call retries.
54458
+ *
54459
+ * The `run` function reference is used as the memoization key for the underlying
54460
+ * `IntervalFnInstance`, so each unique function reference gets its own isolated instance.
54461
+ *
54462
+ * @param run - Signal function to wrap
54463
+ * @param context.interval - Candle interval that controls the firing boundary
54464
+ * @returns Wrapped function with the same signature as `TIntervalFn<T>`, plus a `clear()` method
54465
+ *
54466
+ * @example
54467
+ * ```typescript
54468
+ * const fireOnce = Interval.fn(mySignalFn, { interval: "15m" });
54469
+ *
54470
+ * await fireOnce("BTCUSDT"); // → T or null (fn called)
54471
+ * await fireOnce("BTCUSDT"); // → null (same interval, skipped)
54472
+ * ```
54473
+ */
54474
+ this.fn = (run, context) => {
54475
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_FN, { context });
54476
+ const wrappedFn = (symbol) => {
54477
+ const instance = this._getInstance(run, context.interval);
54478
+ return instance.run(symbol);
54479
+ };
54480
+ wrappedFn.clear = () => {
54481
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_FN_CLEAR);
54482
+ if (!MethodContextService.hasContext()) {
54483
+ backtest.loggerService.warn(`${INTERVAL_METHOD_NAME_FN_CLEAR} called without method context, skipping`);
54484
+ return;
54485
+ }
54486
+ if (!ExecutionContextService.hasContext()) {
54487
+ backtest.loggerService.warn(`${INTERVAL_METHOD_NAME_FN_CLEAR} called without execution context, skipping`);
54488
+ return;
54489
+ }
54490
+ this._getInstance.get(run)?.clear();
54491
+ };
54492
+ return wrappedFn;
54493
+ };
54494
+ /**
54495
+ * Wrap an async signal function with persistent file-based once-per-interval firing.
54496
+ *
54497
+ * Returns a wrapped version of the function that reads from disk on hit (returns `null`)
54498
+ * and writes the fired signal to disk on the first successful fire.
54499
+ * Fired state survives process restarts.
54500
+ *
54501
+ * The `run` function reference is used as the memoization key for the underlying
54502
+ * `IntervalFileInstance`, so each unique function reference gets its own isolated instance.
54503
+ *
54504
+ * @template T - Async function type to wrap
54505
+ * @param run - Async signal function to wrap with persistent once-per-interval firing
54506
+ * @param context.interval - Candle interval that controls the firing boundary
54507
+ * @param context.name - Human-readable bucket name; becomes the directory prefix
54508
+ * @returns Wrapped function with the same signature as `T`, plus an async `clear()` method
54509
+ * that deletes persisted records from disk and disposes the memoized instance
54510
+ *
54511
+ * @example
54512
+ * ```typescript
54513
+ * const fetchSignal = async (symbol: string, when: Date) => { ... };
54514
+ * const fireOnce = Interval.file(fetchSignal, { interval: "1h", name: "fetchSignal" });
54515
+ * await fireOnce.clear(); // delete disk records so the function fires again next call
54516
+ * ```
54517
+ */
54518
+ this.file = (run, context) => {
54519
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_FILE, { context });
54520
+ {
54521
+ this._getFileInstance(run, context.interval, context.name);
54522
+ }
54523
+ const wrappedFn = (symbol) => {
54524
+ const instance = this._getFileInstance(run, context.interval, context.name);
54525
+ return instance.run(symbol);
54526
+ };
54527
+ wrappedFn.clear = async () => {
54528
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_FILE_CLEAR);
54529
+ await this._getFileInstance.get(run)?.clear();
54530
+ };
54531
+ return wrappedFn;
54532
+ };
54533
+ /**
54534
+ * Dispose (remove) the memoized `IntervalFnInstance` for a specific function.
54535
+ *
54536
+ * Removes the instance from the internal memoization cache, discarding all in-memory
54537
+ * fired-interval state across all contexts for that function.
54538
+ * The next call to the wrapped function will create a fresh `IntervalFnInstance`.
54539
+ *
54540
+ * @param run - Function whose `IntervalFnInstance` should be disposed
54541
+ *
54542
+ * @example
54543
+ * ```typescript
54544
+ * const fireOnce = Interval.fn(mySignalFn, { interval: "1h" });
54545
+ * Interval.dispose(mySignalFn);
54546
+ * ```
54547
+ */
54548
+ this.dispose = (run) => {
54549
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_DISPOSE, { run });
54550
+ this._getInstance.clear(run);
54551
+ };
54552
+ /**
54553
+ * Clears all memoized `IntervalFnInstance` and `IntervalFileInstance` objects and
54554
+ * resets the `IntervalFileInstance` index counter.
54555
+ * Call this when `process.cwd()` changes between strategy iterations
54556
+ * so new instances are created with the updated base path.
54557
+ */
54558
+ this.clear = () => {
54559
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_CLEAR);
54560
+ this._getInstance.clear();
54561
+ this._getFileInstance.clear();
54562
+ IntervalFileInstance.clearCounter();
54563
+ };
54564
+ }
54565
+ }
54566
+ /**
54567
+ * Singleton instance of `IntervalUtils` for convenient once-per-interval signal firing.
54568
+ *
54569
+ * @example
54570
+ * ```typescript
54571
+ * import { Interval } from "./classes/Interval";
54572
+ *
54573
+ * // In-memory: fires once per hour, resets on process restart
54574
+ * const fireOnce = Interval.fn(mySignalFn, { interval: "1h" });
54575
+ *
54576
+ * // Persistent: fired state survives restarts
54577
+ * const fireOncePersist = Interval.file(mySignalFn, { interval: "1h", name: "mySignal" });
54578
+ * ```
54579
+ */
54580
+ const Interval = new IntervalUtils();
54581
+
53308
54582
  const BREAKEVEN_METHOD_NAME_GET_DATA = "BreakevenUtils.getData";
53309
54583
  const BREAKEVEN_METHOD_NAME_GET_REPORT = "BreakevenUtils.getReport";
53310
54584
  const BREAKEVEN_METHOD_NAME_DUMP = "BreakevenUtils.dump";
@@ -54419,4 +55693,4 @@ const validateSignal = (signal, currentPrice) => {
54419
55693
  return !errors.length;
54420
55694
  };
54421
55695
 
54422
- export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistMemoryAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, ReportWriter, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Sync, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, warmCandles, writeMemory };
55696
+ export { ActionBase, Backtest, Breakeven, Broker, BrokerBase, Cache, Constant, Dump, Exchange, ExecutionContextService, Heat, HighestProfit, Interval, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MarkdownWriter, MaxDrawdown, Memory, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistIntervalAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistMemoryAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, Position, PositionSize, Report, ReportBase, ReportWriter, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Sync, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialLossCost, commitPartialProfit, commitPartialProfitCost, commitTrailingStop, commitTrailingStopCost, commitTrailingTake, commitTrailingTakeCost, dumpAgentAnswer, dumpError, dumpJson, dumpRecord, dumpTable, dumpText, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getPositionCountdownMinutes, getPositionDrawdownMinutes, getPositionEffectivePrice, getPositionEntries, getPositionEntryOverlap, getPositionEstimateMinutes, getPositionHighestMaxDrawdownPnlCost, getPositionHighestMaxDrawdownPnlPercentage, getPositionHighestPnlCost, getPositionHighestPnlPercentage, getPositionHighestProfitBreakeven, getPositionHighestProfitDistancePnlCost, getPositionHighestProfitDistancePnlPercentage, getPositionHighestProfitMinutes, getPositionHighestProfitPrice, getPositionHighestProfitTimestamp, getPositionInvestedCost, getPositionInvestedCount, getPositionLevels, getPositionMaxDrawdownMinutes, getPositionMaxDrawdownPnlCost, getPositionMaxDrawdownPnlPercentage, getPositionMaxDrawdownPrice, getPositionMaxDrawdownTimestamp, getPositionPartialOverlap, getPositionPartials, getPositionPnlCost, getPositionPnlPercent, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasNoPendingSignal, hasNoScheduledSignal, hasTradeContext, investedCostToPercent, backtest as lib, listExchangeSchema, listFrameSchema, listMemory, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenHighestProfit, listenHighestProfitOnce, listenMaxDrawdown, listenMaxDrawdownOnce, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenSync, listenSyncOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, percentDiff, percentToCloseCost, percentValue, readMemory, removeMemory, roundTicks, runInMockContext, searchMemory, set, setColumns, setConfig, setLogger, shutdown, slPercentShiftToPrice, slPriceToPercentShift, stopStrategy, toProfitLossDto, tpPercentShiftToPrice, tpPriceToPercentShift, validate, validateCommonSignal, validatePendingSignal, validateScheduledSignal, validateSignal, waitForCandle, warmCandles, writeMemory };