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.cjs CHANGED
@@ -908,7 +908,7 @@ const LOGGER_SERVICE$7 = new LoggerService();
908
908
  /** Symbol key for the singleshot waitForInit function on PersistBase instances. */
909
909
  const BASE_WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
910
910
  // Calculate step in milliseconds for candle close time validation
911
- const INTERVAL_MINUTES$8 = {
911
+ const INTERVAL_MINUTES$9 = {
912
912
  "1m": 1,
913
913
  "3m": 3,
914
914
  "5m": 5,
@@ -922,7 +922,7 @@ const INTERVAL_MINUTES$8 = {
922
922
  "1d": 1440,
923
923
  "1w": 10080,
924
924
  };
925
- const MS_PER_MINUTE$6 = 60000;
925
+ const MS_PER_MINUTE$7 = 60000;
926
926
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_USE_PERSIST_SIGNAL_ADAPTER = "PersistSignalUtils.usePersistSignalAdapter";
927
927
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_READ_DATA = "PersistSignalUtils.readSignalData";
928
928
  const PERSIST_SIGNAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistSignalUtils.writeSignalData";
@@ -981,8 +981,18 @@ const PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA = "PersistMeasureUtils.readMea
981
981
  const PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA = "PersistMeasureUtils.writeMeasureData";
982
982
  const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON = "PersistMeasureUtils.useJson";
983
983
  const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY = "PersistMeasureUtils.useDummy";
984
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA = "PersistMeasureUtils.removeMeasureData";
985
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA = "PersistMeasureUtils.listMeasureData";
984
986
  const PERSIST_MEASURE_UTILS_METHOD_NAME_CLEAR = "PersistMeasureUtils.clear";
985
987
  const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER = "PersistMeasureUtils.usePersistMeasureAdapter";
988
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA = "PersistIntervalUtils.readIntervalData";
989
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA = "PersistIntervalUtils.writeIntervalData";
990
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON = "PersistIntervalUtils.useJson";
991
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY = "PersistIntervalUtils.useDummy";
992
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA = "PersistIntervalUtils.removeIntervalData";
993
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA = "PersistIntervalUtils.listIntervalData";
994
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR = "PersistIntervalUtils.clear";
995
+ const PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER = "PersistIntervalUtils.usePersistIntervalAdapter";
986
996
  const PERSIST_CANDLE_UTILS_METHOD_NAME_CLEAR = "PersistCandleUtils.clear";
987
997
  const PERSIST_MEMORY_UTILS_METHOD_NAME_USE_PERSIST_MEMORY_ADAPTER = "PersistMemoryUtils.usePersistMemoryAdapter";
988
998
  const PERSIST_MEMORY_UTILS_METHOD_NAME_READ_DATA = "PersistMemoryUtils.readMemoryData";
@@ -1895,7 +1905,7 @@ class PersistCandleUtils {
1895
1905
  const isInitial = !this.getCandlesStorage.has(key);
1896
1906
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1897
1907
  await stateStorage.waitForInit(isInitial);
1898
- const stepMs = INTERVAL_MINUTES$8[interval] * MS_PER_MINUTE$6;
1908
+ const stepMs = INTERVAL_MINUTES$9[interval] * MS_PER_MINUTE$7;
1899
1909
  // Calculate expected timestamps and fetch each candle directly
1900
1910
  const cachedCandles = [];
1901
1911
  for (let i = 0; i < limit; i++) {
@@ -1951,7 +1961,7 @@ class PersistCandleUtils {
1951
1961
  const stateStorage = this.getCandlesStorage(symbol, interval, exchangeName);
1952
1962
  await stateStorage.waitForInit(isInitial);
1953
1963
  // Calculate step in milliseconds to determine candle close time
1954
- const stepMs = INTERVAL_MINUTES$8[interval] * MS_PER_MINUTE$6;
1964
+ const stepMs = INTERVAL_MINUTES$9[interval] * MS_PER_MINUTE$7;
1955
1965
  const now = Date.now();
1956
1966
  // Write each candle as a separate file, skipping incomplete candles
1957
1967
  for (const candle of candles) {
@@ -2377,7 +2387,8 @@ class PersistMeasureUtils {
2377
2387
  const stateStorage = this.getMeasureStorage(bucket);
2378
2388
  await stateStorage.waitForInit(isInitial);
2379
2389
  if (await stateStorage.hasValue(key)) {
2380
- return await stateStorage.readValue(key);
2390
+ const data = await stateStorage.readValue(key);
2391
+ return data.removed ? null : data;
2381
2392
  }
2382
2393
  return null;
2383
2394
  };
@@ -2399,6 +2410,27 @@ class PersistMeasureUtils {
2399
2410
  await stateStorage.waitForInit(isInitial);
2400
2411
  await stateStorage.writeValue(key, data);
2401
2412
  };
2413
+ /**
2414
+ * Marks a cached entry as removed (soft delete — file is kept on disk).
2415
+ * After this call `readMeasureData` for the same key returns `null`.
2416
+ *
2417
+ * @param bucket - Storage bucket
2418
+ * @param key - Dynamic cache key within the bucket
2419
+ * @returns Promise that resolves when removal is complete
2420
+ */
2421
+ this.removeMeasureData = async (bucket, key) => {
2422
+ LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_REMOVE_DATA, {
2423
+ bucket,
2424
+ key,
2425
+ });
2426
+ const isInitial = !this.getMeasureStorage.has(bucket);
2427
+ const stateStorage = this.getMeasureStorage(bucket);
2428
+ await stateStorage.waitForInit(isInitial);
2429
+ const data = await stateStorage.readValue(key);
2430
+ if (data) {
2431
+ await stateStorage.writeValue(key, Object.assign({}, data, { removed: true }));
2432
+ }
2433
+ };
2402
2434
  }
2403
2435
  /**
2404
2436
  * Registers a custom persistence adapter.
@@ -2409,6 +2441,27 @@ class PersistMeasureUtils {
2409
2441
  LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
2410
2442
  this.PersistMeasureFactory = Ctor;
2411
2443
  }
2444
+ /**
2445
+ * Async generator yielding all non-removed entity keys for a given bucket.
2446
+ * Used by `CacheFileInstance.clear()` to iterate and soft-delete all entries.
2447
+ *
2448
+ * @param bucket - Storage bucket
2449
+ * @returns AsyncGenerator yielding entity keys
2450
+ */
2451
+ async *listMeasureData(bucket) {
2452
+ LOGGER_SERVICE$7.info(PERSIST_MEASURE_UTILS_METHOD_NAME_LIST_DATA, { bucket });
2453
+ const isInitial = !this.getMeasureStorage.has(bucket);
2454
+ const stateStorage = this.getMeasureStorage(bucket);
2455
+ await stateStorage.waitForInit(isInitial);
2456
+ for await (const key of stateStorage.keys()) {
2457
+ const data = await stateStorage.readValue(String(key));
2458
+ if (data === null || data.removed) {
2459
+ continue;
2460
+ }
2461
+ yield String(key);
2462
+ }
2463
+ }
2464
+ ;
2412
2465
  /**
2413
2466
  * Clears the memoized storage cache.
2414
2467
  * Call this when process.cwd() changes between strategy iterations
@@ -2438,6 +2491,140 @@ class PersistMeasureUtils {
2438
2491
  * Used by Cache.file for persistent caching of external API responses.
2439
2492
  */
2440
2493
  const PersistMeasureAdapter = new PersistMeasureUtils();
2494
+ /**
2495
+ * Persistence layer for Interval.file once-per-interval signal firing.
2496
+ *
2497
+ * Stores fired-interval markers under `./dump/data/interval/`.
2498
+ * A record's presence means the interval has already fired for that bucket+key;
2499
+ * absence means the function has not yet fired (or returned null last time).
2500
+ */
2501
+ class PersistIntervalUtils {
2502
+ constructor() {
2503
+ this.PersistIntervalFactory = PersistBase;
2504
+ this.getIntervalStorage = functoolsKit.memoize(([bucket]) => bucket, (bucket) => Reflect.construct(this.PersistIntervalFactory, [
2505
+ bucket,
2506
+ `./dump/data/interval/`,
2507
+ ]));
2508
+ /**
2509
+ * Reads interval data for a given bucket and key.
2510
+ *
2511
+ * @param bucket - Storage bucket (instance name + interval + index)
2512
+ * @param key - Entity key within the bucket (symbol + aligned timestamp)
2513
+ * @returns Promise resolving to stored value or null if not found
2514
+ */
2515
+ this.readIntervalData = async (bucket, key) => {
2516
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_READ_DATA, {
2517
+ bucket,
2518
+ key,
2519
+ });
2520
+ const isInitial = !this.getIntervalStorage.has(bucket);
2521
+ const stateStorage = this.getIntervalStorage(bucket);
2522
+ await stateStorage.waitForInit(isInitial);
2523
+ if (await stateStorage.hasValue(key)) {
2524
+ const data = await stateStorage.readValue(key);
2525
+ return data.removed ? null : data;
2526
+ }
2527
+ return null;
2528
+ };
2529
+ /**
2530
+ * Writes interval data to disk.
2531
+ *
2532
+ * @param data - Data to store
2533
+ * @param bucket - Storage bucket
2534
+ * @param key - Entity key within the bucket
2535
+ * @returns Promise that resolves when write is complete
2536
+ */
2537
+ this.writeIntervalData = async (data, bucket, key) => {
2538
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_WRITE_DATA, {
2539
+ bucket,
2540
+ key,
2541
+ });
2542
+ const isInitial = !this.getIntervalStorage.has(bucket);
2543
+ const stateStorage = this.getIntervalStorage(bucket);
2544
+ await stateStorage.waitForInit(isInitial);
2545
+ await stateStorage.writeValue(key, data);
2546
+ };
2547
+ /**
2548
+ * Marks an interval entry as removed (soft delete — file is kept on disk).
2549
+ * After this call `readIntervalData` for the same key returns `null`,
2550
+ * so the function will fire again on the next `IntervalFileInstance.run` call.
2551
+ *
2552
+ * @param bucket - Storage bucket
2553
+ * @param key - Entity key within the bucket
2554
+ * @returns Promise that resolves when removal is complete
2555
+ */
2556
+ this.removeIntervalData = async (bucket, key) => {
2557
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_REMOVE_DATA, {
2558
+ bucket,
2559
+ key,
2560
+ });
2561
+ const isInitial = !this.getIntervalStorage.has(bucket);
2562
+ const stateStorage = this.getIntervalStorage(bucket);
2563
+ await stateStorage.waitForInit(isInitial);
2564
+ const data = await stateStorage.readValue(key);
2565
+ if (data) {
2566
+ await stateStorage.writeValue(key, Object.assign({}, data, { removed: true }));
2567
+ }
2568
+ };
2569
+ }
2570
+ /**
2571
+ * Registers a custom persistence adapter.
2572
+ *
2573
+ * @param Ctor - Custom PersistBase constructor
2574
+ */
2575
+ usePersistIntervalAdapter(Ctor) {
2576
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_PERSIST_INTERVAL_ADAPTER);
2577
+ this.PersistIntervalFactory = Ctor;
2578
+ }
2579
+ /**
2580
+ * Async generator yielding all non-removed entity keys for a given bucket.
2581
+ * Used by `IntervalFileInstance.clear()` to iterate and soft-delete all entries.
2582
+ *
2583
+ * @param bucket - Storage bucket
2584
+ * @returns AsyncGenerator yielding entity keys
2585
+ */
2586
+ async *listIntervalData(bucket) {
2587
+ LOGGER_SERVICE$7.info(PERSIST_INTERVAL_UTILS_METHOD_NAME_LIST_DATA, { bucket });
2588
+ const isInitial = !this.getIntervalStorage.has(bucket);
2589
+ const stateStorage = this.getIntervalStorage(bucket);
2590
+ await stateStorage.waitForInit(isInitial);
2591
+ for await (const key of stateStorage.keys()) {
2592
+ const data = await stateStorage.readValue(String(key));
2593
+ if (data === null || data.removed) {
2594
+ continue;
2595
+ }
2596
+ yield String(key);
2597
+ }
2598
+ }
2599
+ ;
2600
+ /**
2601
+ * Clears the memoized storage cache.
2602
+ * Call this when process.cwd() changes between strategy iterations.
2603
+ */
2604
+ clear() {
2605
+ LOGGER_SERVICE$7.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_CLEAR);
2606
+ this.getIntervalStorage.clear();
2607
+ }
2608
+ /**
2609
+ * Switches to the default JSON persist adapter.
2610
+ */
2611
+ useJson() {
2612
+ LOGGER_SERVICE$7.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_JSON);
2613
+ this.usePersistIntervalAdapter(PersistBase);
2614
+ }
2615
+ /**
2616
+ * Switches to a dummy persist adapter that discards all writes.
2617
+ */
2618
+ useDummy() {
2619
+ LOGGER_SERVICE$7.log(PERSIST_INTERVAL_UTILS_METHOD_NAME_USE_DUMMY);
2620
+ this.usePersistIntervalAdapter(PersistDummy);
2621
+ }
2622
+ }
2623
+ /**
2624
+ * Global singleton instance of PersistIntervalUtils.
2625
+ * Used by Interval.file for persistent once-per-interval signal firing.
2626
+ */
2627
+ const PersistIntervalAdapter = new PersistIntervalUtils();
2441
2628
  /**
2442
2629
  * Utility class for managing memory entry persistence.
2443
2630
  *
@@ -2785,8 +2972,8 @@ class CandleUtils {
2785
2972
  }
2786
2973
  const Candle = new CandleUtils();
2787
2974
 
2788
- const MS_PER_MINUTE$5 = 60000;
2789
- const INTERVAL_MINUTES$7 = {
2975
+ const MS_PER_MINUTE$6 = 60000;
2976
+ const INTERVAL_MINUTES$8 = {
2790
2977
  "1m": 1,
2791
2978
  "3m": 3,
2792
2979
  "5m": 5,
@@ -2818,7 +3005,7 @@ const INTERVAL_MINUTES$7 = {
2818
3005
  * @returns Aligned timestamp rounded down to interval boundary
2819
3006
  */
2820
3007
  const ALIGN_TO_INTERVAL_FN$2 = (timestamp, intervalMinutes) => {
2821
- const intervalMs = intervalMinutes * MS_PER_MINUTE$5;
3008
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$6;
2822
3009
  return Math.floor(timestamp / intervalMs) * intervalMs;
2823
3010
  };
2824
3011
  /**
@@ -2972,9 +3159,9 @@ const WRITE_CANDLES_CACHE_FN$1 = functoolsKit.trycatch(functoolsKit.queued(async
2972
3159
  * @returns Promise resolving to array of candle data
2973
3160
  */
2974
3161
  const GET_CANDLES_FN$1 = async (dto, since, self) => {
2975
- const step = INTERVAL_MINUTES$7[dto.interval];
3162
+ const step = INTERVAL_MINUTES$8[dto.interval];
2976
3163
  const sinceTimestamp = since.getTime();
2977
- const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$5;
3164
+ const untilTimestamp = sinceTimestamp + dto.limit * step * MS_PER_MINUTE$6;
2978
3165
  await Candle.acquireLock(`ClientExchange GET_CANDLES_FN symbol=${dto.symbol} interval=${dto.interval} limit=${dto.limit}`);
2979
3166
  try {
2980
3167
  // Try to read from cache first
@@ -3088,11 +3275,11 @@ class ClientExchange {
3088
3275
  interval,
3089
3276
  limit,
3090
3277
  });
3091
- const step = INTERVAL_MINUTES$7[interval];
3278
+ const step = INTERVAL_MINUTES$8[interval];
3092
3279
  if (!step) {
3093
3280
  throw new Error(`ClientExchange unknown interval=${interval}`);
3094
3281
  }
3095
- const stepMs = step * MS_PER_MINUTE$5;
3282
+ const stepMs = step * MS_PER_MINUTE$6;
3096
3283
  // Align when down to interval boundary
3097
3284
  const whenTimestamp = this.params.execution.context.when.getTime();
3098
3285
  const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
@@ -3169,11 +3356,11 @@ class ClientExchange {
3169
3356
  if (!this.params.execution.context.backtest) {
3170
3357
  throw new Error(`ClientExchange getNextCandles: cannot fetch future candles in live mode`);
3171
3358
  }
3172
- const step = INTERVAL_MINUTES$7[interval];
3359
+ const step = INTERVAL_MINUTES$8[interval];
3173
3360
  if (!step) {
3174
3361
  throw new Error(`ClientExchange getNextCandles: unknown interval=${interval}`);
3175
3362
  }
3176
- const stepMs = step * MS_PER_MINUTE$5;
3363
+ const stepMs = step * MS_PER_MINUTE$6;
3177
3364
  const now = Date.now();
3178
3365
  // Align when down to interval boundary
3179
3366
  const whenTimestamp = this.params.execution.context.when.getTime();
@@ -3337,11 +3524,11 @@ class ClientExchange {
3337
3524
  sDate,
3338
3525
  eDate,
3339
3526
  });
3340
- const step = INTERVAL_MINUTES$7[interval];
3527
+ const step = INTERVAL_MINUTES$8[interval];
3341
3528
  if (!step) {
3342
3529
  throw new Error(`ClientExchange getRawCandles: unknown interval=${interval}`);
3343
3530
  }
3344
- const stepMs = step * MS_PER_MINUTE$5;
3531
+ const stepMs = step * MS_PER_MINUTE$6;
3345
3532
  const whenTimestamp = this.params.execution.context.when.getTime();
3346
3533
  const alignedWhen = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, step);
3347
3534
  let sinceTimestamp;
@@ -3470,7 +3657,7 @@ class ClientExchange {
3470
3657
  const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
3471
3658
  const to = new Date(alignedTo);
3472
3659
  const from = new Date(alignedTo -
3473
- GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$5);
3660
+ GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$6);
3474
3661
  return await this.params.getOrderBook(symbol, depth, from, to, this.params.execution.context.backtest);
3475
3662
  }
3476
3663
  /**
@@ -3499,7 +3686,7 @@ class ClientExchange {
3499
3686
  const whenTimestamp = this.params.execution.context.when.getTime();
3500
3687
  // Align to 1-minute boundary to prevent look-ahead bias
3501
3688
  const alignedTo = ALIGN_TO_INTERVAL_FN$2(whenTimestamp, 1);
3502
- const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$5 - MS_PER_MINUTE$5;
3689
+ const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$6 - MS_PER_MINUTE$6;
3503
3690
  // No limit: fetch a single window and return as-is
3504
3691
  if (limit === undefined) {
3505
3692
  const to = new Date(alignedTo);
@@ -4478,7 +4665,7 @@ const validateScheduledSignal = (signal, currentPrice) => {
4478
4665
  }
4479
4666
  };
4480
4667
 
4481
- const INTERVAL_MINUTES$6 = {
4668
+ const INTERVAL_MINUTES$7 = {
4482
4669
  "1m": 1,
4483
4670
  "3m": 3,
4484
4671
  "5m": 5,
@@ -4862,7 +5049,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4862
5049
  }
4863
5050
  const currentTime = self.params.execution.context.when.getTime();
4864
5051
  {
4865
- const intervalMinutes = INTERVAL_MINUTES$6[self.params.interval];
5052
+ const intervalMinutes = INTERVAL_MINUTES$7[self.params.interval];
4866
5053
  const intervalMs = intervalMinutes * 60 * 1000;
4867
5054
  const alignedTime = Math.floor(currentTime / intervalMs) * intervalMs;
4868
5055
  // Проверяем что наступил новый интервал (по aligned timestamp)
@@ -7803,6 +7990,90 @@ class ClientStrategy {
7803
7990
  }
7804
7991
  return this._pendingSignal._fall.pnlCost;
7805
7992
  }
7993
+ /**
7994
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
7995
+ *
7996
+ * Measures how much PnL% the position has given back from its best point.
7997
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
7998
+ * Zero when called at the exact moment the peak was set, or when current PnL >= peak PnL.
7999
+ *
8000
+ * Returns null if no pending signal exists.
8001
+ *
8002
+ * @param symbol - Trading pair symbol
8003
+ * @param currentPrice - Current market price
8004
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
8005
+ */
8006
+ async getPositionHighestProfitDistancePnlPercentage(symbol, currentPrice) {
8007
+ this.params.logger.debug("ClientStrategy getPositionHighestProfitDistancePnlPercentage", { symbol, currentPrice });
8008
+ if (!this._pendingSignal) {
8009
+ return null;
8010
+ }
8011
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
8012
+ return Math.max(0, this._pendingSignal._peak.pnlPercentage - currentPnl.pnlPercentage);
8013
+ }
8014
+ /**
8015
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
8016
+ *
8017
+ * Measures how much PnL cost the position has given back from its best point.
8018
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
8019
+ * Zero when called at the exact moment the peak was set, or when current PnL >= peak PnL.
8020
+ *
8021
+ * Returns null if no pending signal exists.
8022
+ *
8023
+ * @param symbol - Trading pair symbol
8024
+ * @param currentPrice - Current market price
8025
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
8026
+ */
8027
+ async getPositionHighestProfitDistancePnlCost(symbol, currentPrice) {
8028
+ this.params.logger.debug("ClientStrategy getPositionHighestProfitDistancePnlCost", { symbol, currentPrice });
8029
+ if (!this._pendingSignal) {
8030
+ return null;
8031
+ }
8032
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
8033
+ return Math.max(0, this._pendingSignal._peak.pnlCost - currentPnl.pnlCost);
8034
+ }
8035
+ /**
8036
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
8037
+ *
8038
+ * Measures how much the position has recovered from its deepest loss point.
8039
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
8040
+ * Zero when called at the exact moment the trough was set, or when current PnL <= trough PnL.
8041
+ *
8042
+ * Returns null if no pending signal exists.
8043
+ *
8044
+ * @param symbol - Trading pair symbol
8045
+ * @param currentPrice - Current market price
8046
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
8047
+ */
8048
+ async getPositionHighestMaxDrawdownPnlPercentage(symbol, currentPrice) {
8049
+ this.params.logger.debug("ClientStrategy getPositionHighestMaxDrawdownPnlPercentage", { symbol, currentPrice });
8050
+ if (!this._pendingSignal) {
8051
+ return null;
8052
+ }
8053
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
8054
+ return Math.max(0, currentPnl.pnlPercentage - this._pendingSignal._fall.pnlPercentage);
8055
+ }
8056
+ /**
8057
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
8058
+ *
8059
+ * Measures how much the position has recovered from its deepest loss point.
8060
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
8061
+ * Zero when called at the exact moment the trough was set, or when current PnL <= trough PnL.
8062
+ *
8063
+ * Returns null if no pending signal exists.
8064
+ *
8065
+ * @param symbol - Trading pair symbol
8066
+ * @param currentPrice - Current market price
8067
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
8068
+ */
8069
+ async getPositionHighestMaxDrawdownPnlCost(symbol, currentPrice) {
8070
+ this.params.logger.debug("ClientStrategy getPositionHighestMaxDrawdownPnlCost", { symbol, currentPrice });
8071
+ if (!this._pendingSignal) {
8072
+ return null;
8073
+ }
8074
+ const currentPnl = toProfitLossDto(this._pendingSignal, currentPrice);
8075
+ return Math.max(0, currentPnl.pnlCost - this._pendingSignal._fall.pnlCost);
8076
+ }
7806
8077
  /**
7807
8078
  * Performs a single tick of strategy execution.
7808
8079
  *
@@ -9886,7 +10157,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
9886
10157
  * @param backtest - Whether running in backtest mode
9887
10158
  * @returns Unique string key for memoization
9888
10159
  */
9889
- const CREATE_KEY_FN$s = (symbol, strategyName, exchangeName, frameName, backtest) => {
10160
+ const CREATE_KEY_FN$t = (symbol, strategyName, exchangeName, frameName, backtest) => {
9890
10161
  const parts = [symbol, strategyName, exchangeName];
9891
10162
  if (frameName)
9892
10163
  parts.push(frameName);
@@ -10153,7 +10424,7 @@ class StrategyConnectionService {
10153
10424
  * @param backtest - Whether running in backtest mode
10154
10425
  * @returns Configured ClientStrategy instance
10155
10426
  */
10156
- this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$s(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
10427
+ this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$t(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
10157
10428
  const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
10158
10429
  return new ClientStrategy({
10159
10430
  symbol,
@@ -10910,6 +11181,90 @@ class StrategyConnectionService {
10910
11181
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
10911
11182
  return await strategy.getPositionMaxDrawdownPnlCost(symbol);
10912
11183
  };
11184
+ /**
11185
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
11186
+ *
11187
+ * Resolves current price via priceMetaService and delegates to
11188
+ * ClientStrategy.getPositionHighestProfitDistancePnlPercentage().
11189
+ * Returns null if no pending signal exists.
11190
+ *
11191
+ * @param backtest - Whether running in backtest mode
11192
+ * @param symbol - Trading pair symbol
11193
+ * @param context - Execution context with strategyName, exchangeName, frameName
11194
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
11195
+ */
11196
+ this.getPositionHighestProfitDistancePnlPercentage = async (backtest, symbol, context) => {
11197
+ this.loggerService.log("strategyConnectionService getPositionHighestProfitDistancePnlPercentage", {
11198
+ symbol,
11199
+ context,
11200
+ });
11201
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11202
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11203
+ return await strategy.getPositionHighestProfitDistancePnlPercentage(symbol, currentPrice);
11204
+ };
11205
+ /**
11206
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
11207
+ *
11208
+ * Resolves current price via priceMetaService and delegates to
11209
+ * ClientStrategy.getPositionHighestProfitDistancePnlCost().
11210
+ * Returns null if no pending signal exists.
11211
+ *
11212
+ * @param backtest - Whether running in backtest mode
11213
+ * @param symbol - Trading pair symbol
11214
+ * @param context - Execution context with strategyName, exchangeName, frameName
11215
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
11216
+ */
11217
+ this.getPositionHighestProfitDistancePnlCost = async (backtest, symbol, context) => {
11218
+ this.loggerService.log("strategyConnectionService getPositionHighestProfitDistancePnlCost", {
11219
+ symbol,
11220
+ context,
11221
+ });
11222
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11223
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11224
+ return await strategy.getPositionHighestProfitDistancePnlCost(symbol, currentPrice);
11225
+ };
11226
+ /**
11227
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
11228
+ *
11229
+ * Resolves current price via priceMetaService and delegates to
11230
+ * ClientStrategy.getPositionHighestMaxDrawdownPnlPercentage().
11231
+ * Returns null if no pending signal exists.
11232
+ *
11233
+ * @param backtest - Whether running in backtest mode
11234
+ * @param symbol - Trading pair symbol
11235
+ * @param context - Execution context with strategyName, exchangeName, frameName
11236
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
11237
+ */
11238
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (backtest, symbol, context) => {
11239
+ this.loggerService.log("strategyConnectionService getPositionHighestMaxDrawdownPnlPercentage", {
11240
+ symbol,
11241
+ context,
11242
+ });
11243
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11244
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11245
+ return await strategy.getPositionHighestMaxDrawdownPnlPercentage(symbol, currentPrice);
11246
+ };
11247
+ /**
11248
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
11249
+ *
11250
+ * Resolves current price via priceMetaService and delegates to
11251
+ * ClientStrategy.getPositionHighestMaxDrawdownPnlCost().
11252
+ * Returns null if no pending signal exists.
11253
+ *
11254
+ * @param backtest - Whether running in backtest mode
11255
+ * @param symbol - Trading pair symbol
11256
+ * @param context - Execution context with strategyName, exchangeName, frameName
11257
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
11258
+ */
11259
+ this.getPositionHighestMaxDrawdownPnlCost = async (backtest, symbol, context) => {
11260
+ this.loggerService.log("strategyConnectionService getPositionHighestMaxDrawdownPnlCost", {
11261
+ symbol,
11262
+ context,
11263
+ });
11264
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
11265
+ const currentPrice = await this.priceMetaService.getCurrentPrice(symbol, context, backtest);
11266
+ return await strategy.getPositionHighestMaxDrawdownPnlCost(symbol, currentPrice);
11267
+ };
10913
11268
  /**
10914
11269
  * Disposes the ClientStrategy instance for the given context.
10915
11270
  *
@@ -10948,7 +11303,7 @@ class StrategyConnectionService {
10948
11303
  }
10949
11304
  return;
10950
11305
  }
10951
- const key = CREATE_KEY_FN$s(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
11306
+ const key = CREATE_KEY_FN$t(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
10952
11307
  if (!this.getStrategy.has(key)) {
10953
11308
  return;
10954
11309
  }
@@ -11350,7 +11705,7 @@ class StrategyConnectionService {
11350
11705
  * Maps FrameInterval to minutes for timestamp calculation.
11351
11706
  * Used to generate timeframe arrays with proper spacing.
11352
11707
  */
11353
- const INTERVAL_MINUTES$5 = {
11708
+ const INTERVAL_MINUTES$6 = {
11354
11709
  "1m": 1,
11355
11710
  "3m": 3,
11356
11711
  "5m": 5,
@@ -11406,7 +11761,7 @@ const GET_TIMEFRAME_FN = async (symbol, self) => {
11406
11761
  symbol,
11407
11762
  });
11408
11763
  const { interval, startDate, endDate } = self.params;
11409
- const intervalMinutes = INTERVAL_MINUTES$5[interval];
11764
+ const intervalMinutes = INTERVAL_MINUTES$6[interval];
11410
11765
  if (!intervalMinutes) {
11411
11766
  throw new Error(`ClientFrame unknown interval: ${interval}`);
11412
11767
  }
@@ -11771,8 +12126,8 @@ const get = (object, path) => {
11771
12126
  return pathArrayFlat.reduce((obj, key) => obj && obj[key], object);
11772
12127
  };
11773
12128
 
11774
- const MS_PER_MINUTE$4 = 60000;
11775
- const INTERVAL_MINUTES$4 = {
12129
+ const MS_PER_MINUTE$5 = 60000;
12130
+ const INTERVAL_MINUTES$5 = {
11776
12131
  "1m": 1,
11777
12132
  "3m": 3,
11778
12133
  "5m": 5,
@@ -11804,11 +12159,11 @@ const INTERVAL_MINUTES$4 = {
11804
12159
  * @returns New Date aligned down to interval boundary
11805
12160
  */
11806
12161
  const alignToInterval = (date, interval) => {
11807
- const minutes = INTERVAL_MINUTES$4[interval];
12162
+ const minutes = INTERVAL_MINUTES$5[interval];
11808
12163
  if (minutes === undefined) {
11809
12164
  throw new Error(`alignToInterval: unknown interval=${interval}`);
11810
12165
  }
11811
- const intervalMs = minutes * MS_PER_MINUTE$4;
12166
+ const intervalMs = minutes * MS_PER_MINUTE$5;
11812
12167
  return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
11813
12168
  };
11814
12169
 
@@ -12117,7 +12472,7 @@ class ClientRisk {
12117
12472
  * @param backtest - Whether running in backtest mode
12118
12473
  * @returns Unique string key for memoization
12119
12474
  */
12120
- const CREATE_KEY_FN$r = (riskName, exchangeName, frameName, backtest) => {
12475
+ const CREATE_KEY_FN$s = (riskName, exchangeName, frameName, backtest) => {
12121
12476
  const parts = [riskName, exchangeName];
12122
12477
  if (frameName)
12123
12478
  parts.push(frameName);
@@ -12217,7 +12572,7 @@ class RiskConnectionService {
12217
12572
  * @param backtest - True if backtest mode, false if live mode
12218
12573
  * @returns Configured ClientRisk instance
12219
12574
  */
12220
- this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$r(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
12575
+ this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$s(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
12221
12576
  const schema = this.riskSchemaService.get(riskName);
12222
12577
  return new ClientRisk({
12223
12578
  ...schema,
@@ -12286,7 +12641,7 @@ class RiskConnectionService {
12286
12641
  payload,
12287
12642
  });
12288
12643
  if (payload) {
12289
- const key = CREATE_KEY_FN$r(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
12644
+ const key = CREATE_KEY_FN$s(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
12290
12645
  this.getRisk.clear(key);
12291
12646
  }
12292
12647
  else {
@@ -13330,7 +13685,7 @@ class ClientAction {
13330
13685
  * @param backtest - Whether running in backtest mode
13331
13686
  * @returns Unique string key for memoization
13332
13687
  */
13333
- const CREATE_KEY_FN$q = (actionName, strategyName, exchangeName, frameName, backtest) => {
13688
+ const CREATE_KEY_FN$r = (actionName, strategyName, exchangeName, frameName, backtest) => {
13334
13689
  const parts = [actionName, strategyName, exchangeName];
13335
13690
  if (frameName)
13336
13691
  parts.push(frameName);
@@ -13382,7 +13737,7 @@ class ActionConnectionService {
13382
13737
  * @param backtest - True if backtest mode, false if live mode
13383
13738
  * @returns Configured ClientAction instance
13384
13739
  */
13385
- this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$q(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
13740
+ this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$r(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
13386
13741
  const schema = this.actionSchemaService.get(actionName);
13387
13742
  return new ClientAction({
13388
13743
  ...schema,
@@ -13593,7 +13948,7 @@ class ActionConnectionService {
13593
13948
  await Promise.all(actions.map(async (action) => await action.dispose()));
13594
13949
  return;
13595
13950
  }
13596
- const key = CREATE_KEY_FN$q(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13951
+ const key = CREATE_KEY_FN$r(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
13597
13952
  if (!this.getAction.has(key)) {
13598
13953
  return;
13599
13954
  }
@@ -13611,7 +13966,7 @@ const METHOD_NAME_VALIDATE$2 = "exchangeCoreService validate";
13611
13966
  * @param exchangeName - Exchange name
13612
13967
  * @returns Unique string key for memoization
13613
13968
  */
13614
- const CREATE_KEY_FN$p = (exchangeName) => {
13969
+ const CREATE_KEY_FN$q = (exchangeName) => {
13615
13970
  return exchangeName;
13616
13971
  };
13617
13972
  /**
@@ -13635,7 +13990,7 @@ class ExchangeCoreService {
13635
13990
  * @param exchangeName - Name of the exchange to validate
13636
13991
  * @returns Promise that resolves when validation is complete
13637
13992
  */
13638
- this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$p(exchangeName), async (exchangeName) => {
13993
+ this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$q(exchangeName), async (exchangeName) => {
13639
13994
  this.loggerService.log(METHOD_NAME_VALIDATE$2, {
13640
13995
  exchangeName,
13641
13996
  });
@@ -13887,7 +14242,7 @@ const METHOD_NAME_VALIDATE$1 = "strategyCoreService validate";
13887
14242
  * @param context - Execution context with strategyName, exchangeName, frameName
13888
14243
  * @returns Unique string key for memoization
13889
14244
  */
13890
- const CREATE_KEY_FN$o = (context) => {
14245
+ const CREATE_KEY_FN$p = (context) => {
13891
14246
  const parts = [context.strategyName, context.exchangeName];
13892
14247
  if (context.frameName)
13893
14248
  parts.push(context.frameName);
@@ -13919,7 +14274,7 @@ class StrategyCoreService {
13919
14274
  * @param context - Execution context with strategyName, exchangeName, frameName
13920
14275
  * @returns Promise that resolves when validation is complete
13921
14276
  */
13922
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$o(context), async (context) => {
14277
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$p(context), async (context) => {
13923
14278
  this.loggerService.log(METHOD_NAME_VALIDATE$1, {
13924
14279
  context,
13925
14280
  });
@@ -15071,6 +15426,82 @@ class StrategyCoreService {
15071
15426
  await this.validate(context);
15072
15427
  return await this.strategyConnectionService.getPositionMaxDrawdownPnlCost(backtest, symbol, context);
15073
15428
  };
15429
+ /**
15430
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
15431
+ *
15432
+ * Delegates to StrategyConnectionService.getPositionHighestProfitDistancePnlPercentage().
15433
+ * Returns null if no pending signal exists.
15434
+ *
15435
+ * @param backtest - Whether running in backtest mode
15436
+ * @param symbol - Trading pair symbol
15437
+ * @param context - Execution context with strategyName, exchangeName, frameName
15438
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
15439
+ */
15440
+ this.getPositionHighestProfitDistancePnlPercentage = async (backtest, symbol, context) => {
15441
+ this.loggerService.log("strategyCoreService getPositionHighestProfitDistancePnlPercentage", {
15442
+ symbol,
15443
+ context,
15444
+ });
15445
+ await this.validate(context);
15446
+ return await this.strategyConnectionService.getPositionHighestProfitDistancePnlPercentage(backtest, symbol, context);
15447
+ };
15448
+ /**
15449
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
15450
+ *
15451
+ * Delegates to StrategyConnectionService.getPositionHighestProfitDistancePnlCost().
15452
+ * Returns null if no pending signal exists.
15453
+ *
15454
+ * @param backtest - Whether running in backtest mode
15455
+ * @param symbol - Trading pair symbol
15456
+ * @param context - Execution context with strategyName, exchangeName, frameName
15457
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
15458
+ */
15459
+ this.getPositionHighestProfitDistancePnlCost = async (backtest, symbol, context) => {
15460
+ this.loggerService.log("strategyCoreService getPositionHighestProfitDistancePnlCost", {
15461
+ symbol,
15462
+ context,
15463
+ });
15464
+ await this.validate(context);
15465
+ return await this.strategyConnectionService.getPositionHighestProfitDistancePnlCost(backtest, symbol, context);
15466
+ };
15467
+ /**
15468
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
15469
+ *
15470
+ * Delegates to StrategyConnectionService.getPositionHighestMaxDrawdownPnlPercentage().
15471
+ * Returns null if no pending signal exists.
15472
+ *
15473
+ * @param backtest - Whether running in backtest mode
15474
+ * @param symbol - Trading pair symbol
15475
+ * @param context - Execution context with strategyName, exchangeName, frameName
15476
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
15477
+ */
15478
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (backtest, symbol, context) => {
15479
+ this.loggerService.log("strategyCoreService getPositionHighestMaxDrawdownPnlPercentage", {
15480
+ symbol,
15481
+ context,
15482
+ });
15483
+ await this.validate(context);
15484
+ return await this.strategyConnectionService.getPositionHighestMaxDrawdownPnlPercentage(backtest, symbol, context);
15485
+ };
15486
+ /**
15487
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
15488
+ *
15489
+ * Delegates to StrategyConnectionService.getPositionHighestMaxDrawdownPnlCost().
15490
+ * Returns null if no pending signal exists.
15491
+ *
15492
+ * @param backtest - Whether running in backtest mode
15493
+ * @param symbol - Trading pair symbol
15494
+ * @param context - Execution context with strategyName, exchangeName, frameName
15495
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
15496
+ */
15497
+ this.getPositionHighestMaxDrawdownPnlCost = async (backtest, symbol, context) => {
15498
+ this.loggerService.log("strategyCoreService getPositionHighestMaxDrawdownPnlCost", {
15499
+ symbol,
15500
+ context,
15501
+ });
15502
+ await this.validate(context);
15503
+ return await this.strategyConnectionService.getPositionHighestMaxDrawdownPnlCost(backtest, symbol, context);
15504
+ };
15074
15505
  }
15075
15506
  }
15076
15507
 
@@ -15143,7 +15574,7 @@ class SizingGlobalService {
15143
15574
  * @param context - Context with riskName, exchangeName, frameName
15144
15575
  * @returns Unique string key for memoization
15145
15576
  */
15146
- const CREATE_KEY_FN$n = (context) => {
15577
+ const CREATE_KEY_FN$o = (context) => {
15147
15578
  const parts = [context.riskName, context.exchangeName];
15148
15579
  if (context.frameName)
15149
15580
  parts.push(context.frameName);
@@ -15169,7 +15600,7 @@ class RiskGlobalService {
15169
15600
  * @param payload - Payload with riskName, exchangeName and frameName
15170
15601
  * @returns Promise that resolves when validation is complete
15171
15602
  */
15172
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$n(context), async (context) => {
15603
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$o(context), async (context) => {
15173
15604
  this.loggerService.log("riskGlobalService validate", {
15174
15605
  context,
15175
15606
  });
@@ -15247,7 +15678,7 @@ const METHOD_NAME_VALIDATE = "actionCoreService validate";
15247
15678
  * @param context - Execution context with strategyName, exchangeName, frameName
15248
15679
  * @returns Unique string key for memoization
15249
15680
  */
15250
- const CREATE_KEY_FN$m = (context) => {
15681
+ const CREATE_KEY_FN$n = (context) => {
15251
15682
  const parts = [context.strategyName, context.exchangeName];
15252
15683
  if (context.frameName)
15253
15684
  parts.push(context.frameName);
@@ -15291,7 +15722,7 @@ class ActionCoreService {
15291
15722
  * @param context - Strategy execution context with strategyName, exchangeName and frameName
15292
15723
  * @returns Promise that resolves when all validations complete
15293
15724
  */
15294
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$m(context), async (context) => {
15725
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$n(context), async (context) => {
15295
15726
  this.loggerService.log(METHOD_NAME_VALIDATE, {
15296
15727
  context,
15297
15728
  });
@@ -16886,8 +17317,8 @@ class BacktestLogicPrivateService {
16886
17317
  }
16887
17318
 
16888
17319
  const EMITTER_CHECK_INTERVAL = 5000;
16889
- const MS_PER_MINUTE$3 = 60000;
16890
- const INTERVAL_MINUTES$3 = {
17320
+ const MS_PER_MINUTE$4 = 60000;
17321
+ const INTERVAL_MINUTES$4 = {
16891
17322
  "1m": 1,
16892
17323
  "3m": 3,
16893
17324
  "5m": 5,
@@ -16903,7 +17334,7 @@ const INTERVAL_MINUTES$3 = {
16903
17334
  };
16904
17335
  const createEmitter = functoolsKit.memoize(([interval]) => `${interval}`, (interval) => {
16905
17336
  const tickSubject = new functoolsKit.Subject();
16906
- const intervalMs = INTERVAL_MINUTES$3[interval] * MS_PER_MINUTE$3;
17337
+ const intervalMs = INTERVAL_MINUTES$4[interval] * MS_PER_MINUTE$4;
16907
17338
  {
16908
17339
  let lastAligned = Math.floor(Date.now() / intervalMs) * intervalMs;
16909
17340
  functoolsKit.Source.fromInterval(EMITTER_CHECK_INTERVAL)
@@ -20287,7 +20718,7 @@ const ReportWriter = new ReportWriterAdapter();
20287
20718
  * @param backtest - Whether running in backtest mode
20288
20719
  * @returns Unique string key for memoization
20289
20720
  */
20290
- const CREATE_KEY_FN$l = (symbol, strategyName, exchangeName, frameName, backtest) => {
20721
+ const CREATE_KEY_FN$m = (symbol, strategyName, exchangeName, frameName, backtest) => {
20291
20722
  const parts = [symbol, strategyName, exchangeName];
20292
20723
  if (frameName)
20293
20724
  parts.push(frameName);
@@ -20533,7 +20964,7 @@ class BacktestMarkdownService {
20533
20964
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
20534
20965
  * Each combination gets its own isolated storage instance.
20535
20966
  */
20536
- this.getStorage = functoolsKit.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));
20967
+ this.getStorage = functoolsKit.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));
20537
20968
  /**
20538
20969
  * Processes tick events and accumulates closed signals.
20539
20970
  * Should be called from IStrategyCallbacks.onTick.
@@ -20690,7 +21121,7 @@ class BacktestMarkdownService {
20690
21121
  payload,
20691
21122
  });
20692
21123
  if (payload) {
20693
- const key = CREATE_KEY_FN$l(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21124
+ const key = CREATE_KEY_FN$m(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20694
21125
  this.getStorage.clear(key);
20695
21126
  }
20696
21127
  else {
@@ -20752,7 +21183,7 @@ class BacktestMarkdownService {
20752
21183
  * @param backtest - Whether running in backtest mode
20753
21184
  * @returns Unique string key for memoization
20754
21185
  */
20755
- const CREATE_KEY_FN$k = (symbol, strategyName, exchangeName, frameName, backtest) => {
21186
+ const CREATE_KEY_FN$l = (symbol, strategyName, exchangeName, frameName, backtest) => {
20756
21187
  const parts = [symbol, strategyName, exchangeName];
20757
21188
  if (frameName)
20758
21189
  parts.push(frameName);
@@ -21247,7 +21678,7 @@ class LiveMarkdownService {
21247
21678
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
21248
21679
  * Each combination gets its own isolated storage instance.
21249
21680
  */
21250
- this.getStorage = functoolsKit.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));
21681
+ this.getStorage = functoolsKit.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));
21251
21682
  /**
21252
21683
  * Subscribes to live signal emitter to receive tick events.
21253
21684
  * Protected against multiple subscriptions.
@@ -21465,7 +21896,7 @@ class LiveMarkdownService {
21465
21896
  payload,
21466
21897
  });
21467
21898
  if (payload) {
21468
- const key = CREATE_KEY_FN$k(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21899
+ const key = CREATE_KEY_FN$l(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21469
21900
  this.getStorage.clear(key);
21470
21901
  }
21471
21902
  else {
@@ -21485,7 +21916,7 @@ class LiveMarkdownService {
21485
21916
  * @param backtest - Whether running in backtest mode
21486
21917
  * @returns Unique string key for memoization
21487
21918
  */
21488
- const CREATE_KEY_FN$j = (symbol, strategyName, exchangeName, frameName, backtest) => {
21919
+ const CREATE_KEY_FN$k = (symbol, strategyName, exchangeName, frameName, backtest) => {
21489
21920
  const parts = [symbol, strategyName, exchangeName];
21490
21921
  if (frameName)
21491
21922
  parts.push(frameName);
@@ -21774,7 +22205,7 @@ class ScheduleMarkdownService {
21774
22205
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
21775
22206
  * Each combination gets its own isolated storage instance.
21776
22207
  */
21777
- this.getStorage = functoolsKit.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));
22208
+ this.getStorage = functoolsKit.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));
21778
22209
  /**
21779
22210
  * Subscribes to signal emitter to receive scheduled signal events.
21780
22211
  * Protected against multiple subscriptions.
@@ -21977,7 +22408,7 @@ class ScheduleMarkdownService {
21977
22408
  payload,
21978
22409
  });
21979
22410
  if (payload) {
21980
- const key = CREATE_KEY_FN$j(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22411
+ const key = CREATE_KEY_FN$k(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
21981
22412
  this.getStorage.clear(key);
21982
22413
  }
21983
22414
  else {
@@ -21997,7 +22428,7 @@ class ScheduleMarkdownService {
21997
22428
  * @param backtest - Whether running in backtest mode
21998
22429
  * @returns Unique string key for memoization
21999
22430
  */
22000
- const CREATE_KEY_FN$i = (symbol, strategyName, exchangeName, frameName, backtest) => {
22431
+ const CREATE_KEY_FN$j = (symbol, strategyName, exchangeName, frameName, backtest) => {
22001
22432
  const parts = [symbol, strategyName, exchangeName];
22002
22433
  if (frameName)
22003
22434
  parts.push(frameName);
@@ -22242,7 +22673,7 @@ class PerformanceMarkdownService {
22242
22673
  * Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
22243
22674
  * Each combination gets its own isolated storage instance.
22244
22675
  */
22245
- this.getStorage = functoolsKit.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));
22676
+ this.getStorage = functoolsKit.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));
22246
22677
  /**
22247
22678
  * Subscribes to performance emitter to receive performance events.
22248
22679
  * Protected against multiple subscriptions.
@@ -22409,7 +22840,7 @@ class PerformanceMarkdownService {
22409
22840
  payload,
22410
22841
  });
22411
22842
  if (payload) {
22412
- const key = CREATE_KEY_FN$i(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22843
+ const key = CREATE_KEY_FN$j(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
22413
22844
  this.getStorage.clear(key);
22414
22845
  }
22415
22846
  else {
@@ -22888,7 +23319,7 @@ class WalkerMarkdownService {
22888
23319
  * @param backtest - Whether running in backtest mode
22889
23320
  * @returns Unique string key for memoization
22890
23321
  */
22891
- const CREATE_KEY_FN$h = (exchangeName, frameName, backtest) => {
23322
+ const CREATE_KEY_FN$i = (exchangeName, frameName, backtest) => {
22892
23323
  const parts = [exchangeName];
22893
23324
  if (frameName)
22894
23325
  parts.push(frameName);
@@ -23335,7 +23766,7 @@ class HeatMarkdownService {
23335
23766
  * Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
23336
23767
  * Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
23337
23768
  */
23338
- this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$h(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
23769
+ this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$i(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
23339
23770
  /**
23340
23771
  * Subscribes to signal emitter to receive tick events.
23341
23772
  * Protected against multiple subscriptions.
@@ -23553,7 +23984,7 @@ class HeatMarkdownService {
23553
23984
  payload,
23554
23985
  });
23555
23986
  if (payload) {
23556
- const key = CREATE_KEY_FN$h(payload.exchangeName, payload.frameName, payload.backtest);
23987
+ const key = CREATE_KEY_FN$i(payload.exchangeName, payload.frameName, payload.backtest);
23557
23988
  this.getStorage.clear(key);
23558
23989
  }
23559
23990
  else {
@@ -24584,7 +25015,7 @@ class ClientPartial {
24584
25015
  * @param backtest - Whether running in backtest mode
24585
25016
  * @returns Unique string key for memoization
24586
25017
  */
24587
- const CREATE_KEY_FN$g = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
25018
+ const CREATE_KEY_FN$h = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
24588
25019
  /**
24589
25020
  * Creates a callback function for emitting profit events to partialProfitSubject.
24590
25021
  *
@@ -24706,7 +25137,7 @@ class PartialConnectionService {
24706
25137
  * Key format: "signalId:backtest" or "signalId:live"
24707
25138
  * Value: ClientPartial instance with logger and event emitters
24708
25139
  */
24709
- this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$g(signalId, backtest), (signalId, backtest) => {
25140
+ this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$h(signalId, backtest), (signalId, backtest) => {
24710
25141
  return new ClientPartial({
24711
25142
  signalId,
24712
25143
  logger: this.loggerService,
@@ -24796,7 +25227,7 @@ class PartialConnectionService {
24796
25227
  const partial = this.getPartial(data.id, backtest);
24797
25228
  await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
24798
25229
  await partial.clear(symbol, data, priceClose, backtest);
24799
- const key = CREATE_KEY_FN$g(data.id, backtest);
25230
+ const key = CREATE_KEY_FN$h(data.id, backtest);
24800
25231
  this.getPartial.clear(key);
24801
25232
  };
24802
25233
  }
@@ -24812,7 +25243,7 @@ class PartialConnectionService {
24812
25243
  * @param backtest - Whether running in backtest mode
24813
25244
  * @returns Unique string key for memoization
24814
25245
  */
24815
- const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
25246
+ const CREATE_KEY_FN$g = (symbol, strategyName, exchangeName, frameName, backtest) => {
24816
25247
  const parts = [symbol, strategyName, exchangeName];
24817
25248
  if (frameName)
24818
25249
  parts.push(frameName);
@@ -25035,7 +25466,7 @@ class PartialMarkdownService {
25035
25466
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
25036
25467
  * Each combination gets its own isolated storage instance.
25037
25468
  */
25038
- this.getStorage = functoolsKit.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));
25469
+ this.getStorage = functoolsKit.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));
25039
25470
  /**
25040
25471
  * Subscribes to partial profit/loss signal emitters to receive events.
25041
25472
  * Protected against multiple subscriptions.
@@ -25245,7 +25676,7 @@ class PartialMarkdownService {
25245
25676
  payload,
25246
25677
  });
25247
25678
  if (payload) {
25248
- const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25679
+ const key = CREATE_KEY_FN$g(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25249
25680
  this.getStorage.clear(key);
25250
25681
  }
25251
25682
  else {
@@ -25261,7 +25692,7 @@ class PartialMarkdownService {
25261
25692
  * @param context - Context with strategyName, exchangeName, frameName
25262
25693
  * @returns Unique string key for memoization
25263
25694
  */
25264
- const CREATE_KEY_FN$e = (context) => {
25695
+ const CREATE_KEY_FN$f = (context) => {
25265
25696
  const parts = [context.strategyName, context.exchangeName];
25266
25697
  if (context.frameName)
25267
25698
  parts.push(context.frameName);
@@ -25335,7 +25766,7 @@ class PartialGlobalService {
25335
25766
  * @param context - Context with strategyName, exchangeName and frameName
25336
25767
  * @param methodName - Name of the calling method for error tracking
25337
25768
  */
25338
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$e(context), (context, methodName) => {
25769
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$f(context), (context, methodName) => {
25339
25770
  this.loggerService.log("partialGlobalService validate", {
25340
25771
  context,
25341
25772
  methodName,
@@ -25790,7 +26221,7 @@ class ClientBreakeven {
25790
26221
  * @param backtest - Whether running in backtest mode
25791
26222
  * @returns Unique string key for memoization
25792
26223
  */
25793
- const CREATE_KEY_FN$d = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
26224
+ const CREATE_KEY_FN$e = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
25794
26225
  /**
25795
26226
  * Creates a callback function for emitting breakeven events to breakevenSubject.
25796
26227
  *
@@ -25876,7 +26307,7 @@ class BreakevenConnectionService {
25876
26307
  * Key format: "signalId:backtest" or "signalId:live"
25877
26308
  * Value: ClientBreakeven instance with logger and event emitter
25878
26309
  */
25879
- this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$d(signalId, backtest), (signalId, backtest) => {
26310
+ this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$e(signalId, backtest), (signalId, backtest) => {
25880
26311
  return new ClientBreakeven({
25881
26312
  signalId,
25882
26313
  logger: this.loggerService,
@@ -25937,7 +26368,7 @@ class BreakevenConnectionService {
25937
26368
  const breakeven = this.getBreakeven(data.id, backtest);
25938
26369
  await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
25939
26370
  await breakeven.clear(symbol, data, priceClose, backtest);
25940
- const key = CREATE_KEY_FN$d(data.id, backtest);
26371
+ const key = CREATE_KEY_FN$e(data.id, backtest);
25941
26372
  this.getBreakeven.clear(key);
25942
26373
  };
25943
26374
  }
@@ -25953,7 +26384,7 @@ class BreakevenConnectionService {
25953
26384
  * @param backtest - Whether running in backtest mode
25954
26385
  * @returns Unique string key for memoization
25955
26386
  */
25956
- const CREATE_KEY_FN$c = (symbol, strategyName, exchangeName, frameName, backtest) => {
26387
+ const CREATE_KEY_FN$d = (symbol, strategyName, exchangeName, frameName, backtest) => {
25957
26388
  const parts = [symbol, strategyName, exchangeName];
25958
26389
  if (frameName)
25959
26390
  parts.push(frameName);
@@ -26128,7 +26559,7 @@ class BreakevenMarkdownService {
26128
26559
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
26129
26560
  * Each combination gets its own isolated storage instance.
26130
26561
  */
26131
- this.getStorage = functoolsKit.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));
26562
+ this.getStorage = functoolsKit.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));
26132
26563
  /**
26133
26564
  * Subscribes to breakeven signal emitter to receive events.
26134
26565
  * Protected against multiple subscriptions.
@@ -26317,7 +26748,7 @@ class BreakevenMarkdownService {
26317
26748
  payload,
26318
26749
  });
26319
26750
  if (payload) {
26320
- const key = CREATE_KEY_FN$c(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26751
+ const key = CREATE_KEY_FN$d(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26321
26752
  this.getStorage.clear(key);
26322
26753
  }
26323
26754
  else {
@@ -26333,7 +26764,7 @@ class BreakevenMarkdownService {
26333
26764
  * @param context - Context with strategyName, exchangeName, frameName
26334
26765
  * @returns Unique string key for memoization
26335
26766
  */
26336
- const CREATE_KEY_FN$b = (context) => {
26767
+ const CREATE_KEY_FN$c = (context) => {
26337
26768
  const parts = [context.strategyName, context.exchangeName];
26338
26769
  if (context.frameName)
26339
26770
  parts.push(context.frameName);
@@ -26407,7 +26838,7 @@ class BreakevenGlobalService {
26407
26838
  * @param context - Context with strategyName, exchangeName and frameName
26408
26839
  * @param methodName - Name of the calling method for error tracking
26409
26840
  */
26410
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$b(context), (context, methodName) => {
26841
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$c(context), (context, methodName) => {
26411
26842
  this.loggerService.log("breakevenGlobalService validate", {
26412
26843
  context,
26413
26844
  methodName,
@@ -26628,7 +27059,7 @@ class ConfigValidationService {
26628
27059
  * @param backtest - Whether running in backtest mode
26629
27060
  * @returns Unique string key for memoization
26630
27061
  */
26631
- const CREATE_KEY_FN$a = (symbol, strategyName, exchangeName, frameName, backtest) => {
27062
+ const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
26632
27063
  const parts = [symbol, strategyName, exchangeName];
26633
27064
  if (frameName)
26634
27065
  parts.push(frameName);
@@ -26795,7 +27226,7 @@ class RiskMarkdownService {
26795
27226
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
26796
27227
  * Each combination gets its own isolated storage instance.
26797
27228
  */
26798
- this.getStorage = functoolsKit.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));
27229
+ this.getStorage = functoolsKit.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));
26799
27230
  /**
26800
27231
  * Subscribes to risk rejection emitter to receive rejection events.
26801
27232
  * Protected against multiple subscriptions.
@@ -26984,7 +27415,7 @@ class RiskMarkdownService {
26984
27415
  payload,
26985
27416
  });
26986
27417
  if (payload) {
26987
- const key = CREATE_KEY_FN$a(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
27418
+ const key = CREATE_KEY_FN$b(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
26988
27419
  this.getStorage.clear(key);
26989
27420
  }
26990
27421
  else {
@@ -29363,7 +29794,7 @@ class HighestProfitReportService {
29363
29794
  * @returns Colon-separated key string for memoization
29364
29795
  * @internal
29365
29796
  */
29366
- const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
29797
+ const CREATE_KEY_FN$a = (symbol, strategyName, exchangeName, frameName, backtest) => {
29367
29798
  const parts = [symbol, strategyName, exchangeName];
29368
29799
  if (frameName)
29369
29800
  parts.push(frameName);
@@ -29605,7 +30036,7 @@ class StrategyMarkdownService {
29605
30036
  *
29606
30037
  * @internal
29607
30038
  */
29608
- this.getStorage = functoolsKit.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));
30039
+ this.getStorage = functoolsKit.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));
29609
30040
  /**
29610
30041
  * Records a cancel-scheduled event when a scheduled signal is cancelled.
29611
30042
  *
@@ -30173,7 +30604,7 @@ class StrategyMarkdownService {
30173
30604
  this.clear = async (payload) => {
30174
30605
  this.loggerService.log("strategyMarkdownService clear", { payload });
30175
30606
  if (payload) {
30176
- const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
30607
+ const key = CREATE_KEY_FN$a(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
30177
30608
  this.getStorage.clear(key);
30178
30609
  }
30179
30610
  else {
@@ -30281,7 +30712,7 @@ class StrategyMarkdownService {
30281
30712
  * Creates a unique key for memoizing ReportStorage instances.
30282
30713
  * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
30283
30714
  */
30284
- const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
30715
+ const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
30285
30716
  const parts = [symbol, strategyName, exchangeName];
30286
30717
  if (frameName)
30287
30718
  parts.push(frameName);
@@ -30474,7 +30905,7 @@ let ReportStorage$2 = class ReportStorage {
30474
30905
  class SyncMarkdownService {
30475
30906
  constructor() {
30476
30907
  this.loggerService = inject(TYPES.loggerService);
30477
- this.getStorage = functoolsKit.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));
30908
+ this.getStorage = functoolsKit.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));
30478
30909
  /**
30479
30910
  * Subscribes to `syncSubject` to start receiving `SignalSyncContract` events.
30480
30911
  * Protected against multiple subscriptions via `singleshot` — subsequent calls
@@ -30670,7 +31101,7 @@ class SyncMarkdownService {
30670
31101
  this.clear = async (payload) => {
30671
31102
  this.loggerService.log("syncMarkdownService clear", { payload });
30672
31103
  if (payload) {
30673
- const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31104
+ const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
30674
31105
  this.getStorage.clear(key);
30675
31106
  }
30676
31107
  else {
@@ -30683,7 +31114,7 @@ class SyncMarkdownService {
30683
31114
  /**
30684
31115
  * Creates a unique memoization key for a symbol-strategy-exchange-frame-backtest combination.
30685
31116
  */
30686
- const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31117
+ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
30687
31118
  const parts = [symbol, strategyName, exchangeName];
30688
31119
  if (frameName)
30689
31120
  parts.push(frameName);
@@ -30859,7 +31290,7 @@ let ReportStorage$1 = class ReportStorage {
30859
31290
  class HighestProfitMarkdownService {
30860
31291
  constructor() {
30861
31292
  this.loggerService = inject(TYPES.loggerService);
30862
- this.getStorage = functoolsKit.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));
31293
+ this.getStorage = functoolsKit.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));
30863
31294
  /**
30864
31295
  * Subscribes to `highestProfitSubject` to start receiving `HighestProfitContract`
30865
31296
  * events. Protected against multiple subscriptions via `singleshot` — subsequent
@@ -31025,7 +31456,7 @@ class HighestProfitMarkdownService {
31025
31456
  this.clear = async (payload) => {
31026
31457
  this.loggerService.log("highestProfitMarkdownService clear", { payload });
31027
31458
  if (payload) {
31028
- const key = CREATE_KEY_FN$7(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31459
+ const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31029
31460
  this.getStorage.clear(key);
31030
31461
  }
31031
31462
  else {
@@ -31047,7 +31478,7 @@ const LISTEN_TIMEOUT$1 = 120000;
31047
31478
  * @param backtest - Whether running in backtest mode
31048
31479
  * @returns Unique string key for memoization
31049
31480
  */
31050
- const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31481
+ const CREATE_KEY_FN$7 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31051
31482
  const parts = [symbol, strategyName, exchangeName];
31052
31483
  if (frameName)
31053
31484
  parts.push(frameName);
@@ -31090,7 +31521,7 @@ class PriceMetaService {
31090
31521
  * Each subject holds the latest currentPrice emitted by the strategy iterator for that key.
31091
31522
  * Instances are cached until clear() is called.
31092
31523
  */
31093
- this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
31524
+ this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$7(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
31094
31525
  /**
31095
31526
  * Returns the current market price for the given symbol and context.
31096
31527
  *
@@ -31119,10 +31550,10 @@ class PriceMetaService {
31119
31550
  if (source.data) {
31120
31551
  return source.data;
31121
31552
  }
31122
- 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...`);
31553
+ 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...`);
31123
31554
  const currentPrice = await functoolsKit.waitForNext(source, (data) => !!data, LISTEN_TIMEOUT$1);
31124
31555
  if (typeof currentPrice === "symbol") {
31125
- throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$6(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31556
+ throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$7(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31126
31557
  }
31127
31558
  return currentPrice;
31128
31559
  };
@@ -31164,7 +31595,7 @@ class PriceMetaService {
31164
31595
  this.getSource.clear();
31165
31596
  return;
31166
31597
  }
31167
- const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31598
+ const key = CREATE_KEY_FN$7(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31168
31599
  this.getSource.clear(key);
31169
31600
  };
31170
31601
  }
@@ -31182,7 +31613,7 @@ const LISTEN_TIMEOUT = 120000;
31182
31613
  * @param backtest - Whether running in backtest mode
31183
31614
  * @returns Unique string key for memoization
31184
31615
  */
31185
- const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31616
+ const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31186
31617
  const parts = [symbol, strategyName, exchangeName];
31187
31618
  if (frameName)
31188
31619
  parts.push(frameName);
@@ -31225,7 +31656,7 @@ class TimeMetaService {
31225
31656
  * Each subject holds the latest createdAt timestamp emitted by the strategy iterator for that key.
31226
31657
  * Instances are cached until clear() is called.
31227
31658
  */
31228
- this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
31659
+ this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
31229
31660
  /**
31230
31661
  * Returns the current candle timestamp (in milliseconds) for the given symbol and context.
31231
31662
  *
@@ -31253,10 +31684,10 @@ class TimeMetaService {
31253
31684
  if (source.data) {
31254
31685
  return source.data;
31255
31686
  }
31256
- 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...`);
31687
+ 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...`);
31257
31688
  const timestamp = await functoolsKit.waitForNext(source, (data) => !!data, LISTEN_TIMEOUT);
31258
31689
  if (typeof timestamp === "symbol") {
31259
- throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$5(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31690
+ throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$6(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
31260
31691
  }
31261
31692
  return timestamp;
31262
31693
  };
@@ -31298,7 +31729,7 @@ class TimeMetaService {
31298
31729
  this.getSource.clear();
31299
31730
  return;
31300
31731
  }
31301
- const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31732
+ const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31302
31733
  this.getSource.clear(key);
31303
31734
  };
31304
31735
  }
@@ -31394,7 +31825,7 @@ class MaxDrawdownReportService {
31394
31825
  /**
31395
31826
  * Creates a unique memoization key for a symbol-strategy-exchange-frame-backtest combination.
31396
31827
  */
31397
- const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31828
+ const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
31398
31829
  const parts = [symbol, strategyName, exchangeName];
31399
31830
  if (frameName)
31400
31831
  parts.push(frameName);
@@ -31518,7 +31949,7 @@ class ReportStorage {
31518
31949
  class MaxDrawdownMarkdownService {
31519
31950
  constructor() {
31520
31951
  this.loggerService = inject(TYPES.loggerService);
31521
- this.getStorage = functoolsKit.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));
31952
+ this.getStorage = functoolsKit.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));
31522
31953
  /**
31523
31954
  * Subscribes to `maxDrawdownSubject` to start receiving `MaxDrawdownContract`
31524
31955
  * events. Protected against multiple subscriptions via `singleshot`.
@@ -31597,7 +32028,7 @@ class MaxDrawdownMarkdownService {
31597
32028
  this.clear = async (payload) => {
31598
32029
  this.loggerService.log("maxDrawdownMarkdownService clear", { payload });
31599
32030
  if (payload) {
31600
- const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
32031
+ const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
31601
32032
  this.getStorage.clear(key);
31602
32033
  }
31603
32034
  else {
@@ -31851,7 +32282,7 @@ const EXCHANGE_METHOD_NAME_FORMAT_PRICE = "ExchangeUtils.formatPrice";
31851
32282
  const EXCHANGE_METHOD_NAME_GET_ORDER_BOOK = "ExchangeUtils.getOrderBook";
31852
32283
  const EXCHANGE_METHOD_NAME_GET_RAW_CANDLES = "ExchangeUtils.getRawCandles";
31853
32284
  const EXCHANGE_METHOD_NAME_GET_AGGREGATED_TRADES = "ExchangeUtils.getAggregatedTrades";
31854
- const MS_PER_MINUTE$2 = 60000;
32285
+ const MS_PER_MINUTE$3 = 60000;
31855
32286
  /**
31856
32287
  * Gets current timestamp from execution context if available.
31857
32288
  * Returns current Date() if no execution context exists (non-trading GUI).
@@ -31918,7 +32349,7 @@ const DEFAULT_GET_ORDER_BOOK_FN = async (_symbol, _depth, _from, _to, _backtest)
31918
32349
  const DEFAULT_GET_AGGREGATED_TRADES_FN = async (_symbol, _from, _to, _backtest) => {
31919
32350
  throw new Error(`getAggregatedTrades is not implemented for this exchange`);
31920
32351
  };
31921
- const INTERVAL_MINUTES$2 = {
32352
+ const INTERVAL_MINUTES$3 = {
31922
32353
  "1m": 1,
31923
32354
  "3m": 3,
31924
32355
  "5m": 5,
@@ -31950,7 +32381,7 @@ const INTERVAL_MINUTES$2 = {
31950
32381
  * @returns Aligned timestamp rounded down to interval boundary
31951
32382
  */
31952
32383
  const ALIGN_TO_INTERVAL_FN$1 = (timestamp, intervalMinutes) => {
31953
- const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
32384
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$3;
31954
32385
  return Math.floor(timestamp / intervalMs) * intervalMs;
31955
32386
  };
31956
32387
  /**
@@ -32090,11 +32521,11 @@ class ExchangeInstance {
32090
32521
  limit,
32091
32522
  });
32092
32523
  const getCandles = this._methods.getCandles;
32093
- const step = INTERVAL_MINUTES$2[interval];
32524
+ const step = INTERVAL_MINUTES$3[interval];
32094
32525
  if (!step) {
32095
32526
  throw new Error(`ExchangeInstance unknown interval=${interval}`);
32096
32527
  }
32097
- const stepMs = step * MS_PER_MINUTE$2;
32528
+ const stepMs = step * MS_PER_MINUTE$3;
32098
32529
  // Align when down to interval boundary
32099
32530
  const when = await GET_TIMESTAMP_FN();
32100
32531
  const whenTimestamp = when.getTime();
@@ -32279,7 +32710,7 @@ class ExchangeInstance {
32279
32710
  const when = await GET_TIMESTAMP_FN();
32280
32711
  const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES);
32281
32712
  const to = new Date(alignedTo);
32282
- const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$2);
32713
+ const from = new Date(alignedTo - GLOBAL_CONFIG.CC_ORDER_BOOK_TIME_OFFSET_MINUTES * MS_PER_MINUTE$3);
32283
32714
  const isBacktest = await GET_BACKTEST_FN();
32284
32715
  return await this._methods.getOrderBook(symbol, depth, from, to, isBacktest);
32285
32716
  };
@@ -32311,7 +32742,7 @@ class ExchangeInstance {
32311
32742
  const when = await GET_TIMESTAMP_FN();
32312
32743
  // Align to 1-minute boundary to prevent look-ahead bias
32313
32744
  const alignedTo = ALIGN_TO_INTERVAL_FN$1(when.getTime(), 1);
32314
- const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$2 - MS_PER_MINUTE$2;
32745
+ const windowMs = GLOBAL_CONFIG.CC_AGGREGATED_TRADES_MAX_MINUTES * MS_PER_MINUTE$3 - MS_PER_MINUTE$3;
32315
32746
  const isBacktest = await GET_BACKTEST_FN();
32316
32747
  // No limit: fetch a single window and return as-is
32317
32748
  if (limit === undefined) {
@@ -32374,11 +32805,11 @@ class ExchangeInstance {
32374
32805
  sDate,
32375
32806
  eDate,
32376
32807
  });
32377
- const step = INTERVAL_MINUTES$2[interval];
32808
+ const step = INTERVAL_MINUTES$3[interval];
32378
32809
  if (!step) {
32379
32810
  throw new Error(`ExchangeInstance getRawCandles: unknown interval=${interval}`);
32380
32811
  }
32381
- const stepMs = step * MS_PER_MINUTE$2;
32812
+ const stepMs = step * MS_PER_MINUTE$3;
32382
32813
  const when = await GET_TIMESTAMP_FN();
32383
32814
  const nowTimestamp = when.getTime();
32384
32815
  const alignedNow = ALIGN_TO_INTERVAL_FN$1(nowTimestamp, step);
@@ -32668,8 +33099,8 @@ const Exchange = new ExchangeUtils();
32668
33099
 
32669
33100
  const WARM_CANDLES_METHOD_NAME = "cache.warmCandles";
32670
33101
  const CHECK_CANDLES_METHOD_NAME = "cache.checkCandles";
32671
- const MS_PER_MINUTE$1 = 60000;
32672
- const INTERVAL_MINUTES$1 = {
33102
+ const MS_PER_MINUTE$2 = 60000;
33103
+ const INTERVAL_MINUTES$2 = {
32673
33104
  "1m": 1,
32674
33105
  "3m": 3,
32675
33106
  "5m": 5,
@@ -32684,7 +33115,7 @@ const INTERVAL_MINUTES$1 = {
32684
33115
  "1w": 10080,
32685
33116
  };
32686
33117
  const ALIGN_TO_INTERVAL_FN = (timestamp, intervalMinutes) => {
32687
- const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
33118
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$2;
32688
33119
  return Math.floor(timestamp / intervalMs) * intervalMs;
32689
33120
  };
32690
33121
  const BAR_LENGTH = 30;
@@ -32709,11 +33140,11 @@ const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
32709
33140
  async function checkCandles(params) {
32710
33141
  const { symbol, exchangeName, interval, from, to, baseDir = "./dump/data/candle" } = params;
32711
33142
  backtest.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
32712
- const step = INTERVAL_MINUTES$1[interval];
33143
+ const step = INTERVAL_MINUTES$2[interval];
32713
33144
  if (!step) {
32714
33145
  throw new Error(`checkCandles: unsupported interval=${interval}`);
32715
33146
  }
32716
- const stepMs = step * MS_PER_MINUTE$1;
33147
+ const stepMs = step * MS_PER_MINUTE$2;
32717
33148
  const dir = path.join(baseDir, exchangeName, symbol, interval);
32718
33149
  const fromTs = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
32719
33150
  const toTs = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
@@ -32783,11 +33214,11 @@ async function warmCandles(params) {
32783
33214
  from,
32784
33215
  to,
32785
33216
  });
32786
- const step = INTERVAL_MINUTES$1[interval];
33217
+ const step = INTERVAL_MINUTES$2[interval];
32787
33218
  if (!step) {
32788
33219
  throw new Error(`warmCandles: unsupported interval=${interval}`);
32789
33220
  }
32790
- const stepMs = step * MS_PER_MINUTE$1;
33221
+ const stepMs = step * MS_PER_MINUTE$2;
32791
33222
  const instance = new ExchangeInstance(exchangeName);
32792
33223
  const sinceTimestamp = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
32793
33224
  const untilTimestamp = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
@@ -34726,6 +35157,10 @@ const GET_POSITION_MAX_DRAWDOWN_PRICE_METHOD_NAME = "strategy.getPositionMaxDraw
34726
35157
  const GET_POSITION_MAX_DRAWDOWN_TIMESTAMP_METHOD_NAME = "strategy.getPositionMaxDrawdownTimestamp";
34727
35158
  const GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionMaxDrawdownPnlPercentage";
34728
35159
  const GET_POSITION_MAX_DRAWDOWN_PNL_COST_METHOD_NAME = "strategy.getPositionMaxDrawdownPnlCost";
35160
+ const GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionHighestProfitDistancePnlPercentage";
35161
+ const GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST_METHOD_NAME = "strategy.getPositionHighestProfitDistancePnlCost";
35162
+ const GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME = "strategy.getPositionHighestMaxDrawdownPnlPercentage";
35163
+ const GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST_METHOD_NAME = "strategy.getPositionHighestMaxDrawdownPnlCost";
34729
35164
  const GET_POSITION_ENTRY_OVERLAP_METHOD_NAME = "strategy.getPositionEntryOverlap";
34730
35165
  const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOverlap";
34731
35166
  const HAS_NO_PENDING_SIGNAL_METHOD_NAME = "strategy.hasNoPendingSignal";
@@ -36360,6 +36795,122 @@ async function getPositionMaxDrawdownPnlCost(symbol) {
36360
36795
  const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36361
36796
  return await backtest.strategyCoreService.getPositionMaxDrawdownPnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
36362
36797
  }
36798
+ /**
36799
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
36800
+ *
36801
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
36802
+ * Returns null if no pending signal exists.
36803
+ *
36804
+ * @param symbol - Trading pair symbol
36805
+ * @returns Promise resolving to drawdown distance in PnL% (≥ 0) or null
36806
+ *
36807
+ * @example
36808
+ * ```typescript
36809
+ * import { getPositionHighestProfitDistancePnlPercentage } from "backtest-kit";
36810
+ *
36811
+ * const dist = await getPositionHighestProfitDistancePnlPercentage("BTCUSDT");
36812
+ * // e.g. 1.5 (gave back 1.5% from peak)
36813
+ * ```
36814
+ */
36815
+ async function getPositionHighestProfitDistancePnlPercentage(symbol) {
36816
+ backtest.loggerService.info(GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE_METHOD_NAME, { symbol });
36817
+ if (!ExecutionContextService.hasContext()) {
36818
+ throw new Error("getPositionHighestProfitDistancePnlPercentage requires an execution context");
36819
+ }
36820
+ if (!MethodContextService.hasContext()) {
36821
+ throw new Error("getPositionHighestProfitDistancePnlPercentage requires a method context");
36822
+ }
36823
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36824
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36825
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlPercentage(isBacktest, symbol, { exchangeName, frameName, strategyName });
36826
+ }
36827
+ /**
36828
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
36829
+ *
36830
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
36831
+ * Returns null if no pending signal exists.
36832
+ *
36833
+ * @param symbol - Trading pair symbol
36834
+ * @returns Promise resolving to drawdown distance in PnL cost (≥ 0) or null
36835
+ *
36836
+ * @example
36837
+ * ```typescript
36838
+ * import { getPositionHighestProfitDistancePnlCost } from "backtest-kit";
36839
+ *
36840
+ * const dist = await getPositionHighestProfitDistancePnlCost("BTCUSDT");
36841
+ * // e.g. 3.2 (gave back $3.2 from peak)
36842
+ * ```
36843
+ */
36844
+ async function getPositionHighestProfitDistancePnlCost(symbol) {
36845
+ backtest.loggerService.info(GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST_METHOD_NAME, { symbol });
36846
+ if (!ExecutionContextService.hasContext()) {
36847
+ throw new Error("getPositionHighestProfitDistancePnlCost requires an execution context");
36848
+ }
36849
+ if (!MethodContextService.hasContext()) {
36850
+ throw new Error("getPositionHighestProfitDistancePnlCost requires a method context");
36851
+ }
36852
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36853
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36854
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
36855
+ }
36856
+ /**
36857
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
36858
+ *
36859
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
36860
+ * Returns null if no pending signal exists.
36861
+ *
36862
+ * @param symbol - Trading pair symbol
36863
+ * @returns Promise resolving to recovery distance in PnL% (≥ 0) or null
36864
+ *
36865
+ * @example
36866
+ * ```typescript
36867
+ * import { getPositionHighestMaxDrawdownPnlPercentage } from "backtest-kit";
36868
+ *
36869
+ * const dist = await getPositionHighestMaxDrawdownPnlPercentage("BTCUSDT");
36870
+ * // e.g. 2.1 (recovered 2.1% from trough)
36871
+ * ```
36872
+ */
36873
+ async function getPositionHighestMaxDrawdownPnlPercentage(symbol) {
36874
+ backtest.loggerService.info(GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE_METHOD_NAME, { symbol });
36875
+ if (!ExecutionContextService.hasContext()) {
36876
+ throw new Error("getPositionHighestMaxDrawdownPnlPercentage requires an execution context");
36877
+ }
36878
+ if (!MethodContextService.hasContext()) {
36879
+ throw new Error("getPositionHighestMaxDrawdownPnlPercentage requires a method context");
36880
+ }
36881
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36882
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36883
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlPercentage(isBacktest, symbol, { exchangeName, frameName, strategyName });
36884
+ }
36885
+ /**
36886
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
36887
+ *
36888
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
36889
+ * Returns null if no pending signal exists.
36890
+ *
36891
+ * @param symbol - Trading pair symbol
36892
+ * @returns Promise resolving to recovery distance in PnL cost (≥ 0) or null
36893
+ *
36894
+ * @example
36895
+ * ```typescript
36896
+ * import { getPositionHighestMaxDrawdownPnlCost } from "backtest-kit";
36897
+ *
36898
+ * const dist = await getPositionHighestMaxDrawdownPnlCost("BTCUSDT");
36899
+ * // e.g. 4.8 (recovered $4.8 from trough)
36900
+ * ```
36901
+ */
36902
+ async function getPositionHighestMaxDrawdownPnlCost(symbol) {
36903
+ backtest.loggerService.info(GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST_METHOD_NAME, { symbol });
36904
+ if (!ExecutionContextService.hasContext()) {
36905
+ throw new Error("getPositionHighestMaxDrawdownPnlCost requires an execution context");
36906
+ }
36907
+ if (!MethodContextService.hasContext()) {
36908
+ throw new Error("getPositionHighestMaxDrawdownPnlCost requires a method context");
36909
+ }
36910
+ const { backtest: isBacktest } = backtest.executionContextService.context;
36911
+ const { exchangeName, frameName, strategyName } = backtest.methodContextService.context;
36912
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
36913
+ }
36363
36914
  /**
36364
36915
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
36365
36916
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -38029,6 +38580,10 @@ const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE = "BacktestUtils.getP
38029
38580
  const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP = "BacktestUtils.getPositionMaxDrawdownTimestamp";
38030
38581
  const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE = "BacktestUtils.getPositionMaxDrawdownPnlPercentage";
38031
38582
  const BACKTEST_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST = "BacktestUtils.getPositionMaxDrawdownPnlCost";
38583
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE = "BacktestUtils.getPositionHighestProfitDistancePnlPercentage";
38584
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST = "BacktestUtils.getPositionHighestProfitDistancePnlCost";
38585
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE = "BacktestUtils.getPositionHighestMaxDrawdownPnlPercentage";
38586
+ const BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST = "BacktestUtils.getPositionHighestMaxDrawdownPnlCost";
38032
38587
  const BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "BacktestUtils.getPositionEntryOverlap";
38033
38588
  const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPositionPartialOverlap";
38034
38589
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
@@ -39298,6 +39853,118 @@ class BacktestUtils {
39298
39853
  }
39299
39854
  return await backtest.strategyCoreService.getPositionMaxDrawdownPnlCost(true, symbol, context);
39300
39855
  };
39856
+ /**
39857
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
39858
+ *
39859
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
39860
+ * Returns null if no pending signal exists.
39861
+ *
39862
+ * @param symbol - Trading pair symbol
39863
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39864
+ * @returns drawdown distance in PnL% (≥ 0) or null if no active position
39865
+ */
39866
+ this.getPositionHighestProfitDistancePnlPercentage = async (symbol, context) => {
39867
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE, {
39868
+ symbol,
39869
+ context,
39870
+ });
39871
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
39872
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
39873
+ {
39874
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39875
+ riskName &&
39876
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
39877
+ riskList &&
39878
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
39879
+ actions &&
39880
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
39881
+ }
39882
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlPercentage(true, symbol, context);
39883
+ };
39884
+ /**
39885
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
39886
+ *
39887
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
39888
+ * Returns null if no pending signal exists.
39889
+ *
39890
+ * @param symbol - Trading pair symbol
39891
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39892
+ * @returns drawdown distance in PnL cost (≥ 0) or null if no active position
39893
+ */
39894
+ this.getPositionHighestProfitDistancePnlCost = async (symbol, context) => {
39895
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST, {
39896
+ symbol,
39897
+ context,
39898
+ });
39899
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
39900
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
39901
+ {
39902
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39903
+ riskName &&
39904
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
39905
+ riskList &&
39906
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
39907
+ actions &&
39908
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
39909
+ }
39910
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlCost(true, symbol, context);
39911
+ };
39912
+ /**
39913
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
39914
+ *
39915
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
39916
+ * Returns null if no pending signal exists.
39917
+ *
39918
+ * @param symbol - Trading pair symbol
39919
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39920
+ * @returns recovery distance in PnL% (≥ 0) or null if no active position
39921
+ */
39922
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (symbol, context) => {
39923
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE, {
39924
+ symbol,
39925
+ context,
39926
+ });
39927
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
39928
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
39929
+ {
39930
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39931
+ riskName &&
39932
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
39933
+ riskList &&
39934
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
39935
+ actions &&
39936
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
39937
+ }
39938
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlPercentage(true, symbol, context);
39939
+ };
39940
+ /**
39941
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
39942
+ *
39943
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
39944
+ * Returns null if no pending signal exists.
39945
+ *
39946
+ * @param symbol - Trading pair symbol
39947
+ * @param context - Execution context with strategyName, exchangeName, and frameName
39948
+ * @returns recovery distance in PnL cost (≥ 0) or null if no active position
39949
+ */
39950
+ this.getPositionHighestMaxDrawdownPnlCost = async (symbol, context) => {
39951
+ backtest.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST, {
39952
+ symbol,
39953
+ context,
39954
+ });
39955
+ backtest.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
39956
+ backtest.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
39957
+ {
39958
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
39959
+ riskName &&
39960
+ backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
39961
+ riskList &&
39962
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
39963
+ actions &&
39964
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
39965
+ }
39966
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlCost(true, symbol, context);
39967
+ };
39301
39968
  /**
39302
39969
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
39303
39970
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -40423,6 +41090,10 @@ const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PRICE = "LiveUtils.getPositionM
40423
41090
  const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_TIMESTAMP = "LiveUtils.getPositionMaxDrawdownTimestamp";
40424
41091
  const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_PERCENTAGE = "LiveUtils.getPositionMaxDrawdownPnlPercentage";
40425
41092
  const LIVE_METHOD_NAME_GET_POSITION_MAX_DRAWDOWN_PNL_COST = "LiveUtils.getPositionMaxDrawdownPnlCost";
41093
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE = "LiveUtils.getPositionHighestProfitDistancePnlPercentage";
41094
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST = "LiveUtils.getPositionHighestProfitDistancePnlCost";
41095
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE = "LiveUtils.getPositionHighestMaxDrawdownPnlPercentage";
41096
+ const LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST = "LiveUtils.getPositionHighestMaxDrawdownPnlCost";
40426
41097
  const LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "LiveUtils.getPositionEntryOverlap";
40427
41098
  const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPartialOverlap";
40428
41099
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
@@ -41819,6 +42490,134 @@ class LiveUtils {
41819
42490
  frameName: "",
41820
42491
  });
41821
42492
  };
42493
+ /**
42494
+ * Returns the distance in PnL percentage between the current price and the highest profit peak.
42495
+ *
42496
+ * Computed as: max(0, peakPnlPercentage - currentPnlPercentage).
42497
+ * Returns null if no pending signal exists.
42498
+ *
42499
+ * @param symbol - Trading pair symbol
42500
+ * @param context - Execution context with strategyName and exchangeName
42501
+ * @returns drawdown distance in PnL% (≥ 0) or null if no active position
42502
+ */
42503
+ this.getPositionHighestProfitDistancePnlPercentage = async (symbol, context) => {
42504
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE, {
42505
+ symbol,
42506
+ context,
42507
+ });
42508
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
42509
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
42510
+ {
42511
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42512
+ riskName &&
42513
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE);
42514
+ riskList &&
42515
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
42516
+ actions &&
42517
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_PERCENTAGE));
42518
+ }
42519
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlPercentage(false, symbol, {
42520
+ strategyName: context.strategyName,
42521
+ exchangeName: context.exchangeName,
42522
+ frameName: "",
42523
+ });
42524
+ };
42525
+ /**
42526
+ * Returns the distance in PnL cost between the current price and the highest profit peak.
42527
+ *
42528
+ * Computed as: max(0, peakPnlCost - currentPnlCost).
42529
+ * Returns null if no pending signal exists.
42530
+ *
42531
+ * @param symbol - Trading pair symbol
42532
+ * @param context - Execution context with strategyName and exchangeName
42533
+ * @returns drawdown distance in PnL cost (≥ 0) or null if no active position
42534
+ */
42535
+ this.getPositionHighestProfitDistancePnlCost = async (symbol, context) => {
42536
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST, {
42537
+ symbol,
42538
+ context,
42539
+ });
42540
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
42541
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
42542
+ {
42543
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42544
+ riskName &&
42545
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST);
42546
+ riskList &&
42547
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
42548
+ actions &&
42549
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_PROFIT_DISTANCE_PNL_COST));
42550
+ }
42551
+ return await backtest.strategyCoreService.getPositionHighestProfitDistancePnlCost(false, symbol, {
42552
+ strategyName: context.strategyName,
42553
+ exchangeName: context.exchangeName,
42554
+ frameName: "",
42555
+ });
42556
+ };
42557
+ /**
42558
+ * Returns the distance in PnL percentage between the current price and the worst drawdown trough.
42559
+ *
42560
+ * Computed as: max(0, currentPnlPercentage - fallPnlPercentage).
42561
+ * Returns null if no pending signal exists.
42562
+ *
42563
+ * @param symbol - Trading pair symbol
42564
+ * @param context - Execution context with strategyName and exchangeName
42565
+ * @returns recovery distance in PnL% (≥ 0) or null if no active position
42566
+ */
42567
+ this.getPositionHighestMaxDrawdownPnlPercentage = async (symbol, context) => {
42568
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE, {
42569
+ symbol,
42570
+ context,
42571
+ });
42572
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
42573
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
42574
+ {
42575
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42576
+ riskName &&
42577
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE);
42578
+ riskList &&
42579
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
42580
+ actions &&
42581
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_PERCENTAGE));
42582
+ }
42583
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlPercentage(false, symbol, {
42584
+ strategyName: context.strategyName,
42585
+ exchangeName: context.exchangeName,
42586
+ frameName: "",
42587
+ });
42588
+ };
42589
+ /**
42590
+ * Returns the distance in PnL cost between the current price and the worst drawdown trough.
42591
+ *
42592
+ * Computed as: max(0, currentPnlCost - fallPnlCost).
42593
+ * Returns null if no pending signal exists.
42594
+ *
42595
+ * @param symbol - Trading pair symbol
42596
+ * @param context - Execution context with strategyName and exchangeName
42597
+ * @returns recovery distance in PnL cost (≥ 0) or null if no active position
42598
+ */
42599
+ this.getPositionHighestMaxDrawdownPnlCost = async (symbol, context) => {
42600
+ backtest.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST, {
42601
+ symbol,
42602
+ context,
42603
+ });
42604
+ backtest.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
42605
+ backtest.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
42606
+ {
42607
+ const { riskName, riskList, actions } = backtest.strategySchemaService.get(context.strategyName);
42608
+ riskName &&
42609
+ backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST);
42610
+ riskList &&
42611
+ riskList.forEach((riskName) => backtest.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
42612
+ actions &&
42613
+ actions.forEach((actionName) => backtest.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_HIGHEST_MAX_DRAWDOWN_PNL_COST));
42614
+ }
42615
+ return await backtest.strategyCoreService.getPositionHighestMaxDrawdownPnlCost(false, symbol, {
42616
+ strategyName: context.strategyName,
42617
+ exchangeName: context.exchangeName,
42618
+ frameName: "",
42619
+ });
42620
+ };
41822
42621
  /**
41823
42622
  * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
41824
42623
  * Use this to prevent duplicate DCA entries at the same price area.
@@ -44196,7 +44995,7 @@ const createSearchIndex = () => {
44196
44995
  return { upsert, remove, list, search, read };
44197
44996
  };
44198
44997
 
44199
- const CREATE_KEY_FN$3 = (signalId, bucketName) => `${signalId}-${bucketName}`;
44998
+ const CREATE_KEY_FN$4 = (signalId, bucketName) => `${signalId}-${bucketName}`;
44200
44999
  const LIST_MEMORY_FN = ({ id, content }) => ({
44201
45000
  memoryId: id,
44202
45001
  content: content,
@@ -44518,7 +45317,7 @@ class MemoryDummyInstance {
44518
45317
  class MemoryAdapter {
44519
45318
  constructor() {
44520
45319
  this.MemoryFactory = MemoryPersistInstance;
44521
- this.getInstance = functoolsKit.memoize(([signalId, bucketName]) => CREATE_KEY_FN$3(signalId, bucketName), (signalId, bucketName) => Reflect.construct(this.MemoryFactory, [signalId, bucketName]));
45320
+ this.getInstance = functoolsKit.memoize(([signalId, bucketName]) => CREATE_KEY_FN$4(signalId, bucketName), (signalId, bucketName) => Reflect.construct(this.MemoryFactory, [signalId, bucketName]));
44522
45321
  /**
44523
45322
  * Activates the adapter by subscribing to signal lifecycle events.
44524
45323
  * Clears memoized instances for a signalId when it is cancelled or closed,
@@ -44529,7 +45328,7 @@ class MemoryAdapter {
44529
45328
  this.enable = functoolsKit.singleshot(() => {
44530
45329
  backtest.loggerService.info(MEMORY_ADAPTER_METHOD_NAME_ENABLE);
44531
45330
  const handleDispose = (signalId) => {
44532
- const prefix = CREATE_KEY_FN$3(signalId, "");
45331
+ const prefix = CREATE_KEY_FN$4(signalId, "");
44533
45332
  for (const key of this.getInstance.keys()) {
44534
45333
  if (key.startsWith(prefix)) {
44535
45334
  const instance = this.getInstance.get(key);
@@ -44574,7 +45373,7 @@ class MemoryAdapter {
44574
45373
  bucketName: dto.bucketName,
44575
45374
  memoryId: dto.memoryId,
44576
45375
  });
44577
- const key = CREATE_KEY_FN$3(dto.signalId, dto.bucketName);
45376
+ const key = CREATE_KEY_FN$4(dto.signalId, dto.bucketName);
44578
45377
  const isInitial = !this.getInstance.has(key);
44579
45378
  const instance = this.getInstance(dto.signalId, dto.bucketName);
44580
45379
  await instance.waitForInit(isInitial);
@@ -44596,7 +45395,7 @@ class MemoryAdapter {
44596
45395
  bucketName: dto.bucketName,
44597
45396
  query: dto.query,
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);
@@ -44616,7 +45415,7 @@ class MemoryAdapter {
44616
45415
  signalId: dto.signalId,
44617
45416
  bucketName: dto.bucketName,
44618
45417
  });
44619
- const key = CREATE_KEY_FN$3(dto.signalId, dto.bucketName);
45418
+ const key = CREATE_KEY_FN$4(dto.signalId, dto.bucketName);
44620
45419
  const isInitial = !this.getInstance.has(key);
44621
45420
  const instance = this.getInstance(dto.signalId, dto.bucketName);
44622
45421
  await instance.waitForInit(isInitial);
@@ -44637,7 +45436,7 @@ class MemoryAdapter {
44637
45436
  bucketName: dto.bucketName,
44638
45437
  memoryId: dto.memoryId,
44639
45438
  });
44640
- const key = CREATE_KEY_FN$3(dto.signalId, dto.bucketName);
45439
+ const key = CREATE_KEY_FN$4(dto.signalId, dto.bucketName);
44641
45440
  const isInitial = !this.getInstance.has(key);
44642
45441
  const instance = this.getInstance(dto.signalId, dto.bucketName);
44643
45442
  await instance.waitForInit(isInitial);
@@ -44660,7 +45459,7 @@ class MemoryAdapter {
44660
45459
  bucketName: dto.bucketName,
44661
45460
  memoryId: dto.memoryId,
44662
45461
  });
44663
- const key = CREATE_KEY_FN$3(dto.signalId, dto.bucketName);
45462
+ const key = CREATE_KEY_FN$4(dto.signalId, dto.bucketName);
44664
45463
  const isInitial = !this.getInstance.has(key);
44665
45464
  const instance = this.getInstance(dto.signalId, dto.bucketName);
44666
45465
  await instance.waitForInit(isInitial);
@@ -44952,7 +45751,7 @@ async function removeMemory(dto) {
44952
45751
  });
44953
45752
  }
44954
45753
 
44955
- const CREATE_KEY_FN$2 = (signalId, bucketName) => `${signalId}-${bucketName}`;
45754
+ const CREATE_KEY_FN$3 = (signalId, bucketName) => `${signalId}-${bucketName}`;
44956
45755
  const DUMP_MEMORY_INSTANCE_METHOD_NAME_AGENT = "DumpMemoryInstance.dumpAgentAnswer";
44957
45756
  const DUMP_MEMORY_INSTANCE_METHOD_NAME_RECORD = "DumpMemoryInstance.dumpRecord";
44958
45757
  const DUMP_MEMORY_INSTANCE_METHOD_NAME_TABLE = "DumpMemoryInstance.dumpTable";
@@ -45572,7 +46371,7 @@ class DumpDummyInstance {
45572
46371
  class DumpAdapter {
45573
46372
  constructor() {
45574
46373
  this.DumpFactory = DumpMarkdownInstance;
45575
- this.getInstance = functoolsKit.memoize(([signalId, bucketName]) => CREATE_KEY_FN$2(signalId, bucketName), (signalId, bucketName) => Reflect.construct(this.DumpFactory, [signalId, bucketName]));
46374
+ this.getInstance = functoolsKit.memoize(([signalId, bucketName]) => CREATE_KEY_FN$3(signalId, bucketName), (signalId, bucketName) => Reflect.construct(this.DumpFactory, [signalId, bucketName]));
45576
46375
  /**
45577
46376
  * Activates the adapter by subscribing to signal lifecycle events.
45578
46377
  * Clears memoized instances for a signalId when it is cancelled or closed,
@@ -45583,7 +46382,7 @@ class DumpAdapter {
45583
46382
  this.enable = functoolsKit.singleshot(() => {
45584
46383
  backtest.loggerService.info(DUMP_ADAPTER_METHOD_NAME_ENABLE);
45585
46384
  const handleDispose = (signalId) => {
45586
- const prefix = CREATE_KEY_FN$2(signalId, "");
46385
+ const prefix = CREATE_KEY_FN$3(signalId, "");
45587
46386
  for (const key of this.getInstance.keys()) {
45588
46387
  if (key.startsWith(prefix)) {
45589
46388
  const instance = this.getInstance.get(key);
@@ -48517,6 +49316,49 @@ PositionSizeUtils.atrBased = async (symbol, accountBalance, priceOpen, atr, cont
48517
49316
  };
48518
49317
  const PositionSize = PositionSizeUtils;
48519
49318
 
49319
+ const METHOD_NAME_MOONBAG = "Position.moonbag";
49320
+ const METHOD_NAME_BRACKET = "Position.bracket";
49321
+ /**
49322
+ * Utilities for calculating take profit and stop loss price levels.
49323
+ * Automatically inverts direction based on position type (long/short).
49324
+ */
49325
+ class Position {
49326
+ }
49327
+ /**
49328
+ * Calculates levels for the "moonbag" strategy — fixed TP at 50% from the current price.
49329
+ * @param dto.position - position type: "long" or "short"
49330
+ * @param dto.currentPrice - current asset price
49331
+ * @param dto.percentStopLoss - stop loss percentage from 0 to 100
49332
+ * @returns priceTakeProfit and priceStopLoss in fiat
49333
+ */
49334
+ Position.moonbag = (dto) => {
49335
+ backtest.loggerService.log(METHOD_NAME_MOONBAG, { dto });
49336
+ const percentTakeProfit = 50;
49337
+ const sign = dto.position === "long" ? 1 : -1;
49338
+ return {
49339
+ position: dto.position,
49340
+ priceTakeProfit: dto.currentPrice * (1 + sign * percentTakeProfit / 100),
49341
+ priceStopLoss: dto.currentPrice * (1 - sign * dto.percentStopLoss / 100),
49342
+ };
49343
+ };
49344
+ /**
49345
+ * Calculates levels for a bracket order with custom TP and SL.
49346
+ * @param dto.position - position type: "long" or "short"
49347
+ * @param dto.currentPrice - current asset price
49348
+ * @param dto.percentStopLoss - stop loss percentage from 0 to 100
49349
+ * @param dto.percentTakeProfit - take profit percentage from 0 to 100
49350
+ * @returns priceTakeProfit and priceStopLoss in fiat
49351
+ */
49352
+ Position.bracket = (dto) => {
49353
+ backtest.loggerService.log(METHOD_NAME_BRACKET, { dto });
49354
+ const sign = dto.position === "long" ? 1 : -1;
49355
+ return {
49356
+ position: dto.position,
49357
+ priceTakeProfit: dto.currentPrice * (1 + sign * dto.percentTakeProfit / 100),
49358
+ priceStopLoss: dto.currentPrice * (1 - sign * dto.percentStopLoss / 100),
49359
+ };
49360
+ };
49361
+
48520
49362
  const PARTIAL_METHOD_NAME_GET_DATA = "PartialUtils.getData";
48521
49363
  const PARTIAL_METHOD_NAME_GET_REPORT = "PartialUtils.getReport";
48522
49364
  const PARTIAL_METHOD_NAME_DUMP = "PartialUtils.dump";
@@ -50719,7 +51561,7 @@ const StorageBacktest = new StorageBacktestAdapter();
50719
51561
  * Generates a unique key for notification identification.
50720
51562
  * @returns Random string identifier
50721
51563
  */
50722
- const CREATE_KEY_FN$1 = () => functoolsKit.randomString();
51564
+ const CREATE_KEY_FN$2 = () => functoolsKit.randomString();
50723
51565
  /**
50724
51566
  * Creates a notification model from signal tick result.
50725
51567
  * Handles opened, closed, scheduled, and cancelled signal actions.
@@ -50730,7 +51572,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
50730
51572
  if (data.action === "opened") {
50731
51573
  return {
50732
51574
  type: "signal.opened",
50733
- id: CREATE_KEY_FN$1(),
51575
+ id: CREATE_KEY_FN$2(),
50734
51576
  timestamp: data.signal.pendingAt,
50735
51577
  backtest: data.backtest,
50736
51578
  symbol: data.symbol,
@@ -50764,7 +51606,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
50764
51606
  const durationMin = Math.round(durationMs / 60000);
50765
51607
  return {
50766
51608
  type: "signal.closed",
50767
- id: CREATE_KEY_FN$1(),
51609
+ id: CREATE_KEY_FN$2(),
50768
51610
  timestamp: data.closeTimestamp,
50769
51611
  backtest: data.backtest,
50770
51612
  symbol: data.symbol,
@@ -50798,7 +51640,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
50798
51640
  if (data.action === "scheduled") {
50799
51641
  return {
50800
51642
  type: "signal.scheduled",
50801
- id: CREATE_KEY_FN$1(),
51643
+ id: CREATE_KEY_FN$2(),
50802
51644
  timestamp: data.signal.scheduledAt,
50803
51645
  backtest: data.backtest,
50804
51646
  symbol: data.symbol,
@@ -50831,7 +51673,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
50831
51673
  const durationMin = Math.round(durationMs / 60000);
50832
51674
  return {
50833
51675
  type: "signal.cancelled",
50834
- id: CREATE_KEY_FN$1(),
51676
+ id: CREATE_KEY_FN$2(),
50835
51677
  timestamp: data.closeTimestamp,
50836
51678
  backtest: data.backtest,
50837
51679
  symbol: data.symbol,
@@ -50864,7 +51706,7 @@ const CREATE_SIGNAL_NOTIFICATION_FN = (data) => {
50864
51706
  */
50865
51707
  const CREATE_PARTIAL_PROFIT_NOTIFICATION_FN = (data) => ({
50866
51708
  type: "partial_profit.available",
50867
- id: CREATE_KEY_FN$1(),
51709
+ id: CREATE_KEY_FN$2(),
50868
51710
  timestamp: data.timestamp,
50869
51711
  backtest: data.backtest,
50870
51712
  symbol: data.symbol,
@@ -50899,7 +51741,7 @@ const CREATE_PARTIAL_PROFIT_NOTIFICATION_FN = (data) => ({
50899
51741
  */
50900
51742
  const CREATE_PARTIAL_LOSS_NOTIFICATION_FN = (data) => ({
50901
51743
  type: "partial_loss.available",
50902
- id: CREATE_KEY_FN$1(),
51744
+ id: CREATE_KEY_FN$2(),
50903
51745
  timestamp: data.timestamp,
50904
51746
  backtest: data.backtest,
50905
51747
  symbol: data.symbol,
@@ -50934,7 +51776,7 @@ const CREATE_PARTIAL_LOSS_NOTIFICATION_FN = (data) => ({
50934
51776
  */
50935
51777
  const CREATE_BREAKEVEN_NOTIFICATION_FN = (data) => ({
50936
51778
  type: "breakeven.available",
50937
- id: CREATE_KEY_FN$1(),
51779
+ id: CREATE_KEY_FN$2(),
50938
51780
  timestamp: data.timestamp,
50939
51781
  backtest: data.backtest,
50940
51782
  symbol: data.symbol,
@@ -50972,7 +51814,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
50972
51814
  if (data.action === "partial-profit") {
50973
51815
  return {
50974
51816
  type: "partial_profit.commit",
50975
- id: CREATE_KEY_FN$1(),
51817
+ id: CREATE_KEY_FN$2(),
50976
51818
  timestamp: data.timestamp,
50977
51819
  backtest: data.backtest,
50978
51820
  symbol: data.symbol,
@@ -51004,7 +51846,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51004
51846
  if (data.action === "partial-loss") {
51005
51847
  return {
51006
51848
  type: "partial_loss.commit",
51007
- id: CREATE_KEY_FN$1(),
51849
+ id: CREATE_KEY_FN$2(),
51008
51850
  timestamp: data.timestamp,
51009
51851
  backtest: data.backtest,
51010
51852
  symbol: data.symbol,
@@ -51036,7 +51878,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51036
51878
  if (data.action === "breakeven") {
51037
51879
  return {
51038
51880
  type: "breakeven.commit",
51039
- id: CREATE_KEY_FN$1(),
51881
+ id: CREATE_KEY_FN$2(),
51040
51882
  timestamp: data.timestamp,
51041
51883
  backtest: data.backtest,
51042
51884
  symbol: data.symbol,
@@ -51067,7 +51909,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51067
51909
  if (data.action === "trailing-stop") {
51068
51910
  return {
51069
51911
  type: "trailing_stop.commit",
51070
- id: CREATE_KEY_FN$1(),
51912
+ id: CREATE_KEY_FN$2(),
51071
51913
  timestamp: data.timestamp,
51072
51914
  backtest: data.backtest,
51073
51915
  symbol: data.symbol,
@@ -51099,7 +51941,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51099
51941
  if (data.action === "trailing-take") {
51100
51942
  return {
51101
51943
  type: "trailing_take.commit",
51102
- id: CREATE_KEY_FN$1(),
51944
+ id: CREATE_KEY_FN$2(),
51103
51945
  timestamp: data.timestamp,
51104
51946
  backtest: data.backtest,
51105
51947
  symbol: data.symbol,
@@ -51131,7 +51973,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51131
51973
  if (data.action === "activate-scheduled") {
51132
51974
  return {
51133
51975
  type: "activate_scheduled.commit",
51134
- id: CREATE_KEY_FN$1(),
51976
+ id: CREATE_KEY_FN$2(),
51135
51977
  timestamp: data.timestamp,
51136
51978
  backtest: data.backtest,
51137
51979
  symbol: data.symbol,
@@ -51163,7 +52005,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51163
52005
  if (data.action === "average-buy") {
51164
52006
  return {
51165
52007
  type: "average_buy.commit",
51166
- id: CREATE_KEY_FN$1(),
52008
+ id: CREATE_KEY_FN$2(),
51167
52009
  timestamp: data.timestamp,
51168
52010
  backtest: data.backtest,
51169
52011
  symbol: data.symbol,
@@ -51196,7 +52038,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51196
52038
  if (data.action === "cancel-scheduled") {
51197
52039
  return {
51198
52040
  type: "cancel_scheduled.commit",
51199
- id: CREATE_KEY_FN$1(),
52041
+ id: CREATE_KEY_FN$2(),
51200
52042
  timestamp: data.timestamp,
51201
52043
  backtest: data.backtest,
51202
52044
  symbol: data.symbol,
@@ -51219,7 +52061,7 @@ const CREATE_STRATEGY_COMMIT_NOTIFICATION_FN = (data) => {
51219
52061
  if (data.action === "close-pending") {
51220
52062
  return {
51221
52063
  type: "close_pending.commit",
51222
- id: CREATE_KEY_FN$1(),
52064
+ id: CREATE_KEY_FN$2(),
51223
52065
  timestamp: data.timestamp,
51224
52066
  backtest: data.backtest,
51225
52067
  symbol: data.symbol,
@@ -51251,7 +52093,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
51251
52093
  if (data.action === "signal-open") {
51252
52094
  return {
51253
52095
  type: "signal_sync.open",
51254
- id: CREATE_KEY_FN$1(),
52096
+ id: CREATE_KEY_FN$2(),
51255
52097
  timestamp: data.timestamp,
51256
52098
  backtest: data.backtest,
51257
52099
  symbol: data.symbol,
@@ -51283,7 +52125,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
51283
52125
  if (data.action === "signal-close") {
51284
52126
  return {
51285
52127
  type: "signal_sync.close",
51286
- id: CREATE_KEY_FN$1(),
52128
+ id: CREATE_KEY_FN$2(),
51287
52129
  timestamp: data.timestamp,
51288
52130
  backtest: data.backtest,
51289
52131
  symbol: data.symbol,
@@ -51321,7 +52163,7 @@ const CREATE_SIGNAL_SYNC_NOTIFICATION_FN = (data) => {
51321
52163
  */
51322
52164
  const CREATE_RISK_NOTIFICATION_FN = (data) => ({
51323
52165
  type: "risk.rejection",
51324
- id: CREATE_KEY_FN$1(),
52166
+ id: CREATE_KEY_FN$2(),
51325
52167
  timestamp: data.timestamp,
51326
52168
  backtest: data.backtest,
51327
52169
  symbol: data.symbol,
@@ -51347,7 +52189,7 @@ const CREATE_RISK_NOTIFICATION_FN = (data) => ({
51347
52189
  */
51348
52190
  const CREATE_ERROR_NOTIFICATION_FN = (error) => ({
51349
52191
  type: "error.info",
51350
- id: CREATE_KEY_FN$1(),
52192
+ id: CREATE_KEY_FN$2(),
51351
52193
  error: functoolsKit.errorData(error),
51352
52194
  message: functoolsKit.getErrorMessage(error),
51353
52195
  backtest: false,
@@ -51359,7 +52201,7 @@ const CREATE_ERROR_NOTIFICATION_FN = (error) => ({
51359
52201
  */
51360
52202
  const CREATE_CRITICAL_ERROR_NOTIFICATION_FN = (error) => ({
51361
52203
  type: "error.critical",
51362
- id: CREATE_KEY_FN$1(),
52204
+ id: CREATE_KEY_FN$2(),
51363
52205
  error: functoolsKit.errorData(error),
51364
52206
  message: functoolsKit.getErrorMessage(error),
51365
52207
  backtest: false,
@@ -51371,7 +52213,7 @@ const CREATE_CRITICAL_ERROR_NOTIFICATION_FN = (error) => ({
51371
52213
  */
51372
52214
  const CREATE_VALIDATION_ERROR_NOTIFICATION_FN = (error) => ({
51373
52215
  type: "error.validation",
51374
- id: CREATE_KEY_FN$1(),
52216
+ id: CREATE_KEY_FN$2(),
51375
52217
  error: functoolsKit.errorData(error),
51376
52218
  message: functoolsKit.getErrorMessage(error),
51377
52219
  backtest: false,
@@ -52800,7 +53642,7 @@ const NotificationLive = new NotificationLiveAdapter();
52800
53642
  */
52801
53643
  const NotificationBacktest = new NotificationBacktestAdapter();
52802
53644
 
52803
- const CACHE_METHOD_NAME_RUN = "CacheInstance.run";
53645
+ const CACHE_METHOD_NAME_RUN = "CacheFnInstance.run";
52804
53646
  const CACHE_METHOD_NAME_FN = "CacheUtils.fn";
52805
53647
  const CACHE_METHOD_NAME_FN_CLEAR = "CacheUtils.fn.clear";
52806
53648
  const CACHE_METHOD_NAME_FN_GC = "CacheUtils.fn.gc";
@@ -52809,8 +53651,8 @@ const CACHE_METHOD_NAME_FILE_CLEAR = "CacheUtils.file.clear";
52809
53651
  const CACHE_METHOD_NAME_DISPOSE = "CacheUtils.dispose";
52810
53652
  const CACHE_METHOD_NAME_CLEAR = "CacheUtils.clear";
52811
53653
  const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
52812
- const MS_PER_MINUTE = 60000;
52813
- const INTERVAL_MINUTES = {
53654
+ const MS_PER_MINUTE$1 = 60000;
53655
+ const INTERVAL_MINUTES$1 = {
52814
53656
  "1m": 1,
52815
53657
  "3m": 3,
52816
53658
  "5m": 5,
@@ -52844,12 +53686,12 @@ const INTERVAL_MINUTES = {
52844
53686
  * // Returns timestamp for 2025-10-01T01:00:00Z
52845
53687
  * ```
52846
53688
  */
52847
- const align = (timestamp, interval) => {
52848
- const intervalMinutes = INTERVAL_MINUTES[interval];
53689
+ const align$1 = (timestamp, interval) => {
53690
+ const intervalMinutes = INTERVAL_MINUTES$1[interval];
52849
53691
  if (!intervalMinutes) {
52850
53692
  throw new Error(`align: unknown interval=${interval}`);
52851
53693
  }
52852
- const intervalMs = intervalMinutes * MS_PER_MINUTE;
53694
+ const intervalMs = intervalMinutes * MS_PER_MINUTE$1;
52853
53695
  return Math.floor(timestamp / intervalMs) * intervalMs;
52854
53696
  };
52855
53697
  /**
@@ -52860,7 +53702,7 @@ const align = (timestamp, interval) => {
52860
53702
  * @param backtest - Whether running in backtest mode
52861
53703
  * @returns Cache key string
52862
53704
  */
52863
- const CREATE_KEY_FN = (strategyName, exchangeName, frameName, backtest) => {
53705
+ const CREATE_KEY_FN$1 = (strategyName, exchangeName, frameName, backtest) => {
52864
53706
  const parts = [strategyName, exchangeName];
52865
53707
  if (frameName)
52866
53708
  parts.push(frameName);
@@ -52884,16 +53726,16 @@ const NEVER_VALUE = Symbol("never");
52884
53726
  *
52885
53727
  * @example
52886
53728
  * ```typescript
52887
- * const instance = new CacheInstance(myExpensiveFunction, "1h");
53729
+ * const instance = new CacheFnInstance(myExpensiveFunction, "1h");
52888
53730
  * const result = instance.run(arg1, arg2); // Computed
52889
53731
  * const result2 = instance.run(arg1, arg2); // Cached (within same hour)
52890
53732
  * // After 1 hour passes
52891
53733
  * const result3 = instance.run(arg1, arg2); // Recomputed
52892
53734
  * ```
52893
53735
  */
52894
- class CacheInstance {
53736
+ class CacheFnInstance {
52895
53737
  /**
52896
- * Creates a new CacheInstance for a specific function and interval.
53738
+ * Creates a new CacheFnInstance for a specific function and interval.
52897
53739
  *
52898
53740
  * @param fn - Function to cache
52899
53741
  * @param interval - Candle interval for cache invalidation (e.g., "1m", "1h")
@@ -52930,7 +53772,7 @@ class CacheInstance {
52930
53772
  *
52931
53773
  * @example
52932
53774
  * ```typescript
52933
- * const instance = new CacheInstance(calculateIndicator, "15m");
53775
+ * const instance = new CacheFnInstance(calculateIndicator, "15m");
52934
53776
  * const result = instance.run("BTCUSDT", 100);
52935
53777
  * console.log(result.value); // Calculated value
52936
53778
  * console.log(result.when); // Cache timestamp
@@ -52938,26 +53780,26 @@ class CacheInstance {
52938
53780
  */
52939
53781
  this.run = (...args) => {
52940
53782
  backtest.loggerService.debug(CACHE_METHOD_NAME_RUN, { args });
52941
- const step = INTERVAL_MINUTES[this.interval];
53783
+ const step = INTERVAL_MINUTES$1[this.interval];
52942
53784
  {
52943
53785
  if (!MethodContextService.hasContext()) {
52944
- throw new Error("CacheInstance run requires method context");
53786
+ throw new Error("CacheFnInstance run requires method context");
52945
53787
  }
52946
53788
  if (!ExecutionContextService.hasContext()) {
52947
- throw new Error("CacheInstance run requires execution context");
53789
+ throw new Error("CacheFnInstance run requires execution context");
52948
53790
  }
52949
53791
  if (!step) {
52950
- throw new Error(`CacheInstance unknown cache ttl interval=${this.interval}`);
53792
+ throw new Error(`CacheFnInstance unknown cache ttl interval=${this.interval}`);
52951
53793
  }
52952
53794
  }
52953
- const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
53795
+ const contextKey = CREATE_KEY_FN$1(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
52954
53796
  const argKey = String(this.key(args));
52955
53797
  const key = `${contextKey}:${argKey}`;
52956
53798
  const currentWhen = backtest.executionContextService.context.when;
52957
53799
  const cached = this._cacheMap.get(key);
52958
53800
  if (cached) {
52959
- const currentAligned = align(currentWhen.getTime(), this.interval);
52960
- const cachedAligned = align(cached.when.getTime(), this.interval);
53801
+ const currentAligned = align$1(currentWhen.getTime(), this.interval);
53802
+ const cachedAligned = align$1(cached.when.getTime(), this.interval);
52961
53803
  if (currentAligned === cachedAligned) {
52962
53804
  return cached;
52963
53805
  }
@@ -52980,7 +53822,7 @@ class CacheInstance {
52980
53822
  *
52981
53823
  * @example
52982
53824
  * ```typescript
52983
- * const instance = new CacheInstance(calculateIndicator, "1h");
53825
+ * const instance = new CacheFnInstance(calculateIndicator, "1h");
52984
53826
  * const result1 = instance.run("BTCUSDT", 14); // Computed
52985
53827
  * const result2 = instance.run("BTCUSDT", 14); // Cached
52986
53828
  *
@@ -52990,7 +53832,7 @@ class CacheInstance {
52990
53832
  * ```
52991
53833
  */
52992
53834
  this.clear = () => {
52993
- const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
53835
+ const contextKey = CREATE_KEY_FN$1(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
52994
53836
  const prefix = `${contextKey}:`;
52995
53837
  for (const key of this._cacheMap.keys()) {
52996
53838
  if (key.startsWith(prefix)) {
@@ -53010,7 +53852,7 @@ class CacheInstance {
53010
53852
  *
53011
53853
  * @example
53012
53854
  * ```typescript
53013
- * const instance = new CacheInstance(calculateIndicator, "1h");
53855
+ * const instance = new CacheFnInstance(calculateIndicator, "1h");
53014
53856
  * instance.run("BTCUSDT", 14); // Cached at 10:00
53015
53857
  * instance.run("ETHUSDT", 14); // Cached at 10:00
53016
53858
  * // Time passes to 11:00
@@ -53019,10 +53861,10 @@ class CacheInstance {
53019
53861
  */
53020
53862
  this.gc = () => {
53021
53863
  const currentWhen = backtest.executionContextService.context.when;
53022
- const currentAligned = align(currentWhen.getTime(), this.interval);
53864
+ const currentAligned = align$1(currentWhen.getTime(), this.interval);
53023
53865
  let removed = 0;
53024
53866
  for (const [key, cached] of this._cacheMap.entries()) {
53025
- const cachedAligned = align(cached.when.getTime(), this.interval);
53867
+ const cachedAligned = align$1(cached.when.getTime(), this.interval);
53026
53868
  if (currentAligned !== cachedAligned) {
53027
53869
  this._cacheMap.delete(key);
53028
53870
  removed++;
@@ -53097,7 +53939,7 @@ class CacheFileInstance {
53097
53939
  */
53098
53940
  this.run = async (...args) => {
53099
53941
  backtest.loggerService.debug(CACHE_FILE_INSTANCE_METHOD_NAME_RUN, { args });
53100
- const step = INTERVAL_MINUTES[this.interval];
53942
+ const step = INTERVAL_MINUTES$1[this.interval];
53101
53943
  {
53102
53944
  if (!MethodContextService.hasContext()) {
53103
53945
  throw new Error("CacheFileInstance run requires method context");
@@ -53111,7 +53953,7 @@ class CacheFileInstance {
53111
53953
  }
53112
53954
  const [symbol, ...rest] = args;
53113
53955
  const { when } = backtest.executionContextService.context;
53114
- const alignedTs = align(when.getTime(), this.interval);
53956
+ const alignedTs = align$1(when.getTime(), this.interval);
53115
53957
  const bucket = `${this.name}_${this.interval}_${this.index}`;
53116
53958
  const entityKey = this.key([symbol, alignedTs, ...rest]);
53117
53959
  const cached = await PersistMeasureAdapter.readMeasureData(bucket, entityKey);
@@ -53119,9 +53961,19 @@ class CacheFileInstance {
53119
53961
  return cached.data;
53120
53962
  }
53121
53963
  const result = await this.fn.call(null, ...args);
53122
- await PersistMeasureAdapter.writeMeasureData({ id: entityKey, data: result }, bucket, entityKey);
53964
+ await PersistMeasureAdapter.writeMeasureData({ id: entityKey, data: result, removed: false }, bucket, entityKey);
53123
53965
  return result;
53124
53966
  };
53967
+ /**
53968
+ * Soft-delete all persisted records for this instance's bucket.
53969
+ * After this call the next `run()` will recompute and re-cache the value.
53970
+ */
53971
+ this.clear = async () => {
53972
+ const bucket = `${this.name}_${this.interval}_${this.index}`;
53973
+ for await (const key of PersistMeasureAdapter.listMeasureData(bucket)) {
53974
+ await PersistMeasureAdapter.removeMeasureData(bucket, key);
53975
+ }
53976
+ };
53125
53977
  this.index = CacheFileInstance.createIndex();
53126
53978
  }
53127
53979
  }
@@ -53145,10 +53997,10 @@ CacheFileInstance._indexCounter = 0;
53145
53997
  class CacheUtils {
53146
53998
  constructor() {
53147
53999
  /**
53148
- * Memoized function to get or create CacheInstance for a function.
54000
+ * Memoized function to get or create CacheFnInstance for a function.
53149
54001
  * Each function gets its own isolated cache instance.
53150
54002
  */
53151
- this._getFnInstance = functoolsKit.memoize(([run]) => run, (run, interval, key) => new CacheInstance(run, interval, key));
54003
+ this._getFnInstance = functoolsKit.memoize(([run]) => run, (run, interval, key) => new CacheFnInstance(run, interval, key));
53152
54004
  /**
53153
54005
  * Memoized function to get or create CacheFileInstance for an async function.
53154
54006
  * Each function gets its own isolated file-cache instance.
@@ -53257,31 +54109,34 @@ class CacheUtils {
53257
54109
  */
53258
54110
  this.file = (run, context) => {
53259
54111
  backtest.loggerService.info(CACHE_METHOD_NAME_FILE, { context });
54112
+ {
54113
+ this._getFileInstance(run, context.interval, context.name, context.key);
54114
+ }
53260
54115
  const wrappedFn = (...args) => {
53261
54116
  const instance = this._getFileInstance(run, context.interval, context.name, context.key);
53262
54117
  return instance.run(...args);
53263
54118
  };
53264
- wrappedFn.clear = () => {
54119
+ wrappedFn.clear = async () => {
53265
54120
  backtest.loggerService.info(CACHE_METHOD_NAME_FILE_CLEAR);
53266
- this._getFileInstance.clear(run);
54121
+ await this._getFileInstance.get(run)?.clear();
53267
54122
  };
53268
54123
  return wrappedFn;
53269
54124
  };
53270
54125
  /**
53271
- * Dispose (remove) the memoized CacheInstance for a specific function.
54126
+ * Dispose (remove) the memoized CacheFnInstance for a specific function.
53272
54127
  *
53273
- * Removes the CacheInstance from the internal memoization cache, discarding all cached
54128
+ * Removes the CacheFnInstance from the internal memoization cache, discarding all cached
53274
54129
  * results across all contexts (all strategy/exchange/mode combinations) for that function.
53275
- * The next call to the wrapped function will create a fresh CacheInstance.
54130
+ * The next call to the wrapped function will create a fresh CacheFnInstance.
53276
54131
  *
53277
54132
  * @template T - Function type
53278
- * @param run - Function whose CacheInstance should be disposed.
54133
+ * @param run - Function whose CacheFnInstance should be disposed.
53279
54134
  *
53280
54135
  * @example
53281
54136
  * ```typescript
53282
54137
  * const cachedFn = Cache.fn(calculateIndicator, { interval: "1h" });
53283
54138
  *
53284
- * // Dispose CacheInstance for a specific function
54139
+ * // Dispose CacheFnInstance for a specific function
53285
54140
  * Cache.dispose(calculateIndicator);
53286
54141
  * ```
53287
54142
  */
@@ -53295,7 +54150,7 @@ class CacheUtils {
53295
54150
  }
53296
54151
  };
53297
54152
  /**
53298
- * Clears all memoized CacheInstance and CacheFileInstance objects.
54153
+ * Clears all memoized CacheFnInstance and CacheFileInstance objects.
53299
54154
  * Call this when process.cwd() changes between strategy iterations
53300
54155
  * so new instances are created with the updated base path.
53301
54156
  */
@@ -53325,6 +54180,425 @@ class CacheUtils {
53325
54180
  */
53326
54181
  const Cache = new CacheUtils();
53327
54182
 
54183
+ const INTERVAL_METHOD_NAME_RUN = "IntervalFnInstance.run";
54184
+ const INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN = "IntervalFileInstance.run";
54185
+ const INTERVAL_METHOD_NAME_FN = "IntervalUtils.fn";
54186
+ const INTERVAL_METHOD_NAME_FN_CLEAR = "IntervalUtils.fn.clear";
54187
+ const INTERVAL_METHOD_NAME_FILE = "IntervalUtils.file";
54188
+ const INTERVAL_METHOD_NAME_FILE_CLEAR = "IntervalUtils.file.clear";
54189
+ const INTERVAL_METHOD_NAME_DISPOSE = "IntervalUtils.dispose";
54190
+ const INTERVAL_METHOD_NAME_CLEAR = "IntervalUtils.clear";
54191
+ const MS_PER_MINUTE = 60000;
54192
+ const INTERVAL_MINUTES = {
54193
+ "1m": 1,
54194
+ "3m": 3,
54195
+ "5m": 5,
54196
+ "15m": 15,
54197
+ "30m": 30,
54198
+ "1h": 60,
54199
+ "2h": 120,
54200
+ "4h": 240,
54201
+ "6h": 360,
54202
+ "8h": 480,
54203
+ "1d": 1440,
54204
+ "1w": 10080,
54205
+ };
54206
+ /**
54207
+ * Aligns timestamp down to the nearest interval boundary.
54208
+ * For example, for 15m interval: 00:17 -> 00:15, 00:44 -> 00:30
54209
+ *
54210
+ * @param timestamp - Timestamp in milliseconds
54211
+ * @param interval - Candle interval
54212
+ * @returns Aligned timestamp rounded down to interval boundary
54213
+ * @throws Error if interval is unknown
54214
+ *
54215
+ * @example
54216
+ * ```typescript
54217
+ * // Align to 15-minute boundary
54218
+ * const aligned = align(new Date("2025-10-01T00:35:00Z").getTime(), "15m");
54219
+ * // Returns timestamp for 2025-10-01T00:30:00Z
54220
+ * ```
54221
+ */
54222
+ const align = (timestamp, interval) => {
54223
+ const intervalMinutes = INTERVAL_MINUTES[interval];
54224
+ if (!intervalMinutes) {
54225
+ throw new Error(`align: unknown interval=${interval}`);
54226
+ }
54227
+ const intervalMs = intervalMinutes * MS_PER_MINUTE;
54228
+ return Math.floor(timestamp / intervalMs) * intervalMs;
54229
+ };
54230
+ /**
54231
+ * Build a context key string from strategy name, exchange name, frame name, and execution mode.
54232
+ *
54233
+ * @param strategyName - Name of the strategy
54234
+ * @param exchangeName - Name of the exchange
54235
+ * @param frameName - Name of the backtest frame (omitted in live mode)
54236
+ * @param isBacktest - Whether running in backtest mode
54237
+ * @returns Context key string
54238
+ */
54239
+ const CREATE_KEY_FN = (strategyName, exchangeName, frameName, isBacktest) => {
54240
+ const parts = [strategyName, exchangeName];
54241
+ if (frameName)
54242
+ parts.push(frameName);
54243
+ parts.push(isBacktest ? "backtest" : "live");
54244
+ return parts.join(":");
54245
+ };
54246
+ /**
54247
+ * Instance class for firing a function exactly once per interval boundary.
54248
+ *
54249
+ * On the first call within a new interval the wrapped function is invoked and
54250
+ * its result is returned. Every subsequent call within the same interval returns
54251
+ * `null` without invoking the function again.
54252
+ * If the function itself returns `null`, the interval countdown does not start —
54253
+ * the next call will retry the function.
54254
+ *
54255
+ * State is kept in memory; use `IntervalFileInstance` for persistence across restarts.
54256
+ *
54257
+ * @example
54258
+ * ```typescript
54259
+ * const instance = new IntervalFnInstance(mySignalFn, "1h");
54260
+ * await instance.run("BTCUSDT"); // → T | null (fn called)
54261
+ * await instance.run("BTCUSDT"); // → null (skipped, same interval)
54262
+ * // After 1 hour passes:
54263
+ * await instance.run("BTCUSDT"); // → T | null (fn called again)
54264
+ * ```
54265
+ */
54266
+ class IntervalFnInstance {
54267
+ /**
54268
+ * Creates a new IntervalFnInstance.
54269
+ *
54270
+ * @param fn - Function to fire once per interval
54271
+ * @param interval - Candle interval that controls the firing boundary
54272
+ */
54273
+ constructor(fn, interval) {
54274
+ this.fn = fn;
54275
+ this.interval = interval;
54276
+ /** Stores the last aligned timestamp per context+symbol key. */
54277
+ this._stateMap = new Map();
54278
+ /**
54279
+ * Execute the signal function with once-per-interval enforcement.
54280
+ *
54281
+ * Algorithm:
54282
+ * 1. Align the current execution context `when` to the interval boundary.
54283
+ * 2. If the stored aligned timestamp for this context+symbol equals the current one → return `null`.
54284
+ * 3. Otherwise call `fn`. If it returns a non-null signal, record the aligned timestamp and return
54285
+ * the signal. If it returns `null`, leave state unchanged so the next call retries.
54286
+ *
54287
+ * Requires active method context and execution context.
54288
+ *
54289
+ * @param symbol - Trading pair symbol (e.g. "BTCUSDT")
54290
+ * @returns The value returned by `fn` on the first non-null fire, `null` on all subsequent calls
54291
+ * within the same interval or when `fn` itself returned `null`
54292
+ * @throws Error if method context, execution context, or interval is missing
54293
+ */
54294
+ this.run = async (symbol) => {
54295
+ backtest.loggerService.debug(INTERVAL_METHOD_NAME_RUN, { symbol });
54296
+ const step = INTERVAL_MINUTES[this.interval];
54297
+ {
54298
+ if (!MethodContextService.hasContext()) {
54299
+ throw new Error("IntervalFnInstance run requires method context");
54300
+ }
54301
+ if (!ExecutionContextService.hasContext()) {
54302
+ throw new Error("IntervalFnInstance run requires execution context");
54303
+ }
54304
+ if (!step) {
54305
+ throw new Error(`IntervalFnInstance unknown interval=${this.interval}`);
54306
+ }
54307
+ }
54308
+ const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
54309
+ const key = `${contextKey}:${symbol}`;
54310
+ const currentWhen = backtest.executionContextService.context.when;
54311
+ const currentAligned = align(currentWhen.getTime(), this.interval);
54312
+ if (this._stateMap.get(key) === currentAligned) {
54313
+ return null;
54314
+ }
54315
+ const result = await this.fn(symbol, currentWhen);
54316
+ if (result !== null) {
54317
+ this._stateMap.set(key, currentAligned);
54318
+ }
54319
+ return result;
54320
+ };
54321
+ /**
54322
+ * Clear fired-interval state for the current execution context.
54323
+ *
54324
+ * Removes all entries for the current strategy/exchange/frame/mode combination
54325
+ * from this instance's state map. The next `run()` call will invoke the function again.
54326
+ *
54327
+ * Requires active method context and execution context.
54328
+ */
54329
+ this.clear = () => {
54330
+ const contextKey = CREATE_KEY_FN(backtest.methodContextService.context.strategyName, backtest.methodContextService.context.exchangeName, backtest.methodContextService.context.frameName, backtest.executionContextService.context.backtest);
54331
+ const prefix = `${contextKey}:`;
54332
+ for (const key of this._stateMap.keys()) {
54333
+ if (key.startsWith(prefix)) {
54334
+ this._stateMap.delete(key);
54335
+ }
54336
+ }
54337
+ };
54338
+ }
54339
+ }
54340
+ /**
54341
+ * Instance class for firing an async function exactly once per interval boundary,
54342
+ * with the fired state persisted to disk via `PersistIntervalAdapter`.
54343
+ *
54344
+ * On the first call within a new interval the wrapped function is invoked.
54345
+ * If it returns a non-null signal, that result is written to disk and returned.
54346
+ * Every subsequent call within the same interval returns `null` (record exists on disk).
54347
+ * If the function returns `null`, nothing is written and the next call retries.
54348
+ *
54349
+ * Fired state survives process restarts — unlike `IntervalFnInstance` which is in-memory only.
54350
+ *
54351
+ * @template T - Async function type: `(symbol: string, ...args) => Promise<R | null>`
54352
+ *
54353
+ * @example
54354
+ * ```typescript
54355
+ * const instance = new IntervalFileInstance(fetchSignal, "1h", "mySignal");
54356
+ * await instance.run("BTCUSDT"); // → R | null (fn called, result written to disk)
54357
+ * await instance.run("BTCUSDT"); // → null (record exists, already fired)
54358
+ * ```
54359
+ */
54360
+ class IntervalFileInstance {
54361
+ /**
54362
+ * Allocates a new unique index. Called once in the constructor to give each
54363
+ * IntervalFileInstance its own namespace in the persistent key space.
54364
+ */
54365
+ static createIndex() {
54366
+ return IntervalFileInstance._indexCounter++;
54367
+ }
54368
+ /**
54369
+ * Resets the index counter to zero.
54370
+ * Call this when clearing all instances (e.g. on `IntervalUtils.clear()`).
54371
+ */
54372
+ static clearCounter() {
54373
+ IntervalFileInstance._indexCounter = 0;
54374
+ }
54375
+ /**
54376
+ * Creates a new IntervalFileInstance.
54377
+ *
54378
+ * @param fn - Async signal function to fire once per interval
54379
+ * @param interval - Candle interval that controls the firing boundary
54380
+ * @param name - Human-readable bucket name used as the directory prefix
54381
+ */
54382
+ constructor(fn, interval, name) {
54383
+ this.fn = fn;
54384
+ this.interval = interval;
54385
+ this.name = name;
54386
+ /**
54387
+ * Execute the async function with persistent once-per-interval enforcement.
54388
+ *
54389
+ * Algorithm:
54390
+ * 1. Build bucket = `${name}_${interval}_${index}` — fixed per instance, used as directory name.
54391
+ * 2. Align execution context `when` to interval boundary → `alignedTs`.
54392
+ * 3. Build entity key = `${symbol}_${alignedTs}`.
54393
+ * 4. Try to read from `PersistIntervalAdapter` using (bucket, entityKey).
54394
+ * 5. On hit — return `null` (interval already fired).
54395
+ * 6. On miss — call `fn`. If non-null, write to disk and return result. If null, skip write and return null.
54396
+ *
54397
+ * Requires active method context and execution context.
54398
+ *
54399
+ * @param symbol - Trading pair symbol (e.g. "BTCUSDT")
54400
+ * @returns The value on the first non-null fire, `null` if already fired this interval
54401
+ * or if `fn` itself returned `null`
54402
+ * @throws Error if method context, execution context, or interval is missing
54403
+ */
54404
+ this.run = async (symbol) => {
54405
+ backtest.loggerService.debug(INTERVAL_FILE_INSTANCE_METHOD_NAME_RUN, { symbol });
54406
+ const step = INTERVAL_MINUTES[this.interval];
54407
+ {
54408
+ if (!MethodContextService.hasContext()) {
54409
+ throw new Error("IntervalFileInstance run requires method context");
54410
+ }
54411
+ if (!ExecutionContextService.hasContext()) {
54412
+ throw new Error("IntervalFileInstance run requires execution context");
54413
+ }
54414
+ if (!step) {
54415
+ throw new Error(`IntervalFileInstance unknown interval=${this.interval}`);
54416
+ }
54417
+ }
54418
+ const { when } = backtest.executionContextService.context;
54419
+ const alignedTs = align(when.getTime(), this.interval);
54420
+ const bucket = `${this.name}_${this.interval}_${this.index}`;
54421
+ const entityKey = `${symbol}_${alignedTs}`;
54422
+ const cached = await PersistIntervalAdapter.readIntervalData(bucket, entityKey);
54423
+ if (cached !== null) {
54424
+ return null;
54425
+ }
54426
+ const result = await this.fn(symbol, when);
54427
+ if (result !== null) {
54428
+ await PersistIntervalAdapter.writeIntervalData({ id: entityKey, data: result, removed: false }, bucket, entityKey);
54429
+ }
54430
+ return result;
54431
+ };
54432
+ /**
54433
+ * Soft-delete all persisted records for this instance's bucket.
54434
+ * After this call the function will fire again on the next `run()`.
54435
+ */
54436
+ this.clear = async () => {
54437
+ const bucket = `${this.name}_${this.interval}_${this.index}`;
54438
+ for await (const key of PersistIntervalAdapter.listIntervalData(bucket)) {
54439
+ await PersistIntervalAdapter.removeIntervalData(bucket, key);
54440
+ }
54441
+ };
54442
+ this.index = IntervalFileInstance.createIndex();
54443
+ }
54444
+ }
54445
+ /** Global counter — incremented once per IntervalFileInstance construction. */
54446
+ IntervalFileInstance._indexCounter = 0;
54447
+ /**
54448
+ * Utility class for wrapping signal functions with once-per-interval firing.
54449
+ * Provides two modes: in-memory (`fn`) and persistent file-based (`file`).
54450
+ * Exported as singleton instance `Interval` for convenient usage.
54451
+ *
54452
+ * @example
54453
+ * ```typescript
54454
+ * import { Interval } from "./classes/Interval";
54455
+ *
54456
+ * const fireOncePerHour = Interval.fn(mySignalFn, { interval: "1h" });
54457
+ * await fireOncePerHour("BTCUSDT"); // fn called — returns its result
54458
+ * await fireOncePerHour("BTCUSDT"); // returns null (same interval)
54459
+ * ```
54460
+ */
54461
+ class IntervalUtils {
54462
+ constructor() {
54463
+ /**
54464
+ * Memoized factory to get or create an `IntervalFnInstance` for a function.
54465
+ * Each function reference gets its own isolated instance.
54466
+ */
54467
+ this._getInstance = functoolsKit.memoize(([run]) => run, (run, interval) => new IntervalFnInstance(run, interval));
54468
+ /**
54469
+ * Memoized factory to get or create an `IntervalFileInstance` for an async function.
54470
+ * Each function reference gets its own isolated persistent instance.
54471
+ */
54472
+ this._getFileInstance = functoolsKit.memoize(([run]) => run, (run, interval, name) => new IntervalFileInstance(run, interval, name));
54473
+ /**
54474
+ * Wrap a signal function with in-memory once-per-interval firing.
54475
+ *
54476
+ * Returns a wrapped version of the function that fires at most once per interval boundary.
54477
+ * If the function returns `null`, the countdown does not start and the next call retries.
54478
+ *
54479
+ * The `run` function reference is used as the memoization key for the underlying
54480
+ * `IntervalFnInstance`, so each unique function reference gets its own isolated instance.
54481
+ *
54482
+ * @param run - Signal function to wrap
54483
+ * @param context.interval - Candle interval that controls the firing boundary
54484
+ * @returns Wrapped function with the same signature as `TIntervalFn<T>`, plus a `clear()` method
54485
+ *
54486
+ * @example
54487
+ * ```typescript
54488
+ * const fireOnce = Interval.fn(mySignalFn, { interval: "15m" });
54489
+ *
54490
+ * await fireOnce("BTCUSDT"); // → T or null (fn called)
54491
+ * await fireOnce("BTCUSDT"); // → null (same interval, skipped)
54492
+ * ```
54493
+ */
54494
+ this.fn = (run, context) => {
54495
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_FN, { context });
54496
+ const wrappedFn = (symbol) => {
54497
+ const instance = this._getInstance(run, context.interval);
54498
+ return instance.run(symbol);
54499
+ };
54500
+ wrappedFn.clear = () => {
54501
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_FN_CLEAR);
54502
+ if (!MethodContextService.hasContext()) {
54503
+ backtest.loggerService.warn(`${INTERVAL_METHOD_NAME_FN_CLEAR} called without method context, skipping`);
54504
+ return;
54505
+ }
54506
+ if (!ExecutionContextService.hasContext()) {
54507
+ backtest.loggerService.warn(`${INTERVAL_METHOD_NAME_FN_CLEAR} called without execution context, skipping`);
54508
+ return;
54509
+ }
54510
+ this._getInstance.get(run)?.clear();
54511
+ };
54512
+ return wrappedFn;
54513
+ };
54514
+ /**
54515
+ * Wrap an async signal function with persistent file-based once-per-interval firing.
54516
+ *
54517
+ * Returns a wrapped version of the function that reads from disk on hit (returns `null`)
54518
+ * and writes the fired signal to disk on the first successful fire.
54519
+ * Fired state survives process restarts.
54520
+ *
54521
+ * The `run` function reference is used as the memoization key for the underlying
54522
+ * `IntervalFileInstance`, so each unique function reference gets its own isolated instance.
54523
+ *
54524
+ * @template T - Async function type to wrap
54525
+ * @param run - Async signal function to wrap with persistent once-per-interval firing
54526
+ * @param context.interval - Candle interval that controls the firing boundary
54527
+ * @param context.name - Human-readable bucket name; becomes the directory prefix
54528
+ * @returns Wrapped function with the same signature as `T`, plus an async `clear()` method
54529
+ * that deletes persisted records from disk and disposes the memoized instance
54530
+ *
54531
+ * @example
54532
+ * ```typescript
54533
+ * const fetchSignal = async (symbol: string, when: Date) => { ... };
54534
+ * const fireOnce = Interval.file(fetchSignal, { interval: "1h", name: "fetchSignal" });
54535
+ * await fireOnce.clear(); // delete disk records so the function fires again next call
54536
+ * ```
54537
+ */
54538
+ this.file = (run, context) => {
54539
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_FILE, { context });
54540
+ {
54541
+ this._getFileInstance(run, context.interval, context.name);
54542
+ }
54543
+ const wrappedFn = (symbol) => {
54544
+ const instance = this._getFileInstance(run, context.interval, context.name);
54545
+ return instance.run(symbol);
54546
+ };
54547
+ wrappedFn.clear = async () => {
54548
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_FILE_CLEAR);
54549
+ await this._getFileInstance.get(run)?.clear();
54550
+ };
54551
+ return wrappedFn;
54552
+ };
54553
+ /**
54554
+ * Dispose (remove) the memoized `IntervalFnInstance` for a specific function.
54555
+ *
54556
+ * Removes the instance from the internal memoization cache, discarding all in-memory
54557
+ * fired-interval state across all contexts for that function.
54558
+ * The next call to the wrapped function will create a fresh `IntervalFnInstance`.
54559
+ *
54560
+ * @param run - Function whose `IntervalFnInstance` should be disposed
54561
+ *
54562
+ * @example
54563
+ * ```typescript
54564
+ * const fireOnce = Interval.fn(mySignalFn, { interval: "1h" });
54565
+ * Interval.dispose(mySignalFn);
54566
+ * ```
54567
+ */
54568
+ this.dispose = (run) => {
54569
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_DISPOSE, { run });
54570
+ this._getInstance.clear(run);
54571
+ };
54572
+ /**
54573
+ * Clears all memoized `IntervalFnInstance` and `IntervalFileInstance` objects and
54574
+ * resets the `IntervalFileInstance` index counter.
54575
+ * Call this when `process.cwd()` changes between strategy iterations
54576
+ * so new instances are created with the updated base path.
54577
+ */
54578
+ this.clear = () => {
54579
+ backtest.loggerService.info(INTERVAL_METHOD_NAME_CLEAR);
54580
+ this._getInstance.clear();
54581
+ this._getFileInstance.clear();
54582
+ IntervalFileInstance.clearCounter();
54583
+ };
54584
+ }
54585
+ }
54586
+ /**
54587
+ * Singleton instance of `IntervalUtils` for convenient once-per-interval signal firing.
54588
+ *
54589
+ * @example
54590
+ * ```typescript
54591
+ * import { Interval } from "./classes/Interval";
54592
+ *
54593
+ * // In-memory: fires once per hour, resets on process restart
54594
+ * const fireOnce = Interval.fn(mySignalFn, { interval: "1h" });
54595
+ *
54596
+ * // Persistent: fired state survives restarts
54597
+ * const fireOncePersist = Interval.file(mySignalFn, { interval: "1h", name: "mySignal" });
54598
+ * ```
54599
+ */
54600
+ const Interval = new IntervalUtils();
54601
+
53328
54602
  const BREAKEVEN_METHOD_NAME_GET_DATA = "BreakevenUtils.getData";
53329
54603
  const BREAKEVEN_METHOD_NAME_GET_REPORT = "BreakevenUtils.getReport";
53330
54604
  const BREAKEVEN_METHOD_NAME_DUMP = "BreakevenUtils.dump";
@@ -54451,6 +55725,7 @@ exports.Exchange = Exchange;
54451
55725
  exports.ExecutionContextService = ExecutionContextService;
54452
55726
  exports.Heat = Heat;
54453
55727
  exports.HighestProfit = HighestProfit;
55728
+ exports.Interval = Interval;
54454
55729
  exports.Live = Live;
54455
55730
  exports.Log = Log;
54456
55731
  exports.Markdown = Markdown;
@@ -54468,6 +55743,7 @@ exports.Performance = Performance;
54468
55743
  exports.PersistBase = PersistBase;
54469
55744
  exports.PersistBreakevenAdapter = PersistBreakevenAdapter;
54470
55745
  exports.PersistCandleAdapter = PersistCandleAdapter;
55746
+ exports.PersistIntervalAdapter = PersistIntervalAdapter;
54471
55747
  exports.PersistLogAdapter = PersistLogAdapter;
54472
55748
  exports.PersistMeasureAdapter = PersistMeasureAdapter;
54473
55749
  exports.PersistMemoryAdapter = PersistMemoryAdapter;
@@ -54477,6 +55753,7 @@ exports.PersistRiskAdapter = PersistRiskAdapter;
54477
55753
  exports.PersistScheduleAdapter = PersistScheduleAdapter;
54478
55754
  exports.PersistSignalAdapter = PersistSignalAdapter;
54479
55755
  exports.PersistStorageAdapter = PersistStorageAdapter;
55756
+ exports.Position = Position;
54480
55757
  exports.PositionSize = PositionSize;
54481
55758
  exports.Report = Report;
54482
55759
  exports.ReportBase = ReportBase;
@@ -54546,9 +55823,13 @@ exports.getPositionEffectivePrice = getPositionEffectivePrice;
54546
55823
  exports.getPositionEntries = getPositionEntries;
54547
55824
  exports.getPositionEntryOverlap = getPositionEntryOverlap;
54548
55825
  exports.getPositionEstimateMinutes = getPositionEstimateMinutes;
55826
+ exports.getPositionHighestMaxDrawdownPnlCost = getPositionHighestMaxDrawdownPnlCost;
55827
+ exports.getPositionHighestMaxDrawdownPnlPercentage = getPositionHighestMaxDrawdownPnlPercentage;
54549
55828
  exports.getPositionHighestPnlCost = getPositionHighestPnlCost;
54550
55829
  exports.getPositionHighestPnlPercentage = getPositionHighestPnlPercentage;
54551
55830
  exports.getPositionHighestProfitBreakeven = getPositionHighestProfitBreakeven;
55831
+ exports.getPositionHighestProfitDistancePnlCost = getPositionHighestProfitDistancePnlCost;
55832
+ exports.getPositionHighestProfitDistancePnlPercentage = getPositionHighestProfitDistancePnlPercentage;
54552
55833
  exports.getPositionHighestProfitMinutes = getPositionHighestProfitMinutes;
54553
55834
  exports.getPositionHighestProfitPrice = getPositionHighestProfitPrice;
54554
55835
  exports.getPositionHighestProfitTimestamp = getPositionHighestProfitTimestamp;