backtest-kit 4.0.2 → 5.1.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.
Files changed (4) hide show
  1. package/build/index.cjs +1025 -146
  2. package/build/index.mjs +1025 -148
  3. package/package.json +1 -1
  4. package/types.d.ts +624 -30
package/build/index.cjs CHANGED
@@ -89,6 +89,10 @@ const coreServices$1 = {
89
89
  actionCoreService: Symbol('actionCoreService'),
90
90
  frameCoreService: Symbol('frameCoreService'),
91
91
  };
92
+ const metaServices$1 = {
93
+ priceMetaService: Symbol('priceMetaService'),
94
+ timeMetaService: Symbol('timeMetaService'),
95
+ };
92
96
  const globalServices$1 = {
93
97
  sizingGlobalService: Symbol('sizingGlobalService'),
94
98
  riskGlobalService: Symbol('riskGlobalService'),
@@ -154,6 +158,7 @@ const TYPES = {
154
158
  ...connectionServices$1,
155
159
  ...schemaServices$1,
156
160
  ...coreServices$1,
161
+ ...metaServices$1,
157
162
  ...globalServices$1,
158
163
  ...commandServices$1,
159
164
  ...logicPrivateServices$1,
@@ -470,9 +475,17 @@ const GLOBAL_CONFIG = {
470
475
  * Allows to commitAverageBuy if currentPrice is not the lowest price since entry, but still lower than priceOpen.
471
476
  * This can help improve average entry price in cases where price has rebounded after entry but is still below priceOpen, without waiting for a new lower price.
472
477
  *
473
- * Default: true (DCA logic enabled everywhere, not just when antirecord is broken)
478
+ * Default: false (DCA logic enabled only when antirecord is broken)
474
479
  */
475
480
  CC_ENABLE_DCA_EVERYWHERE: false,
481
+ /**
482
+ * Enables PPPL (Partial Profit, Partial Loss) logic even if this breaks a direction of exits
483
+ * Allows to take partial profit or loss on a position even if it results in a mix of profit and loss exits
484
+ * This can help lock in profits or cut losses on part of the position without waiting for a perfect exit scenario.
485
+ *
486
+ * Default: false (PPPL logic is only applied when it does not break the direction of exits, ensuring clearer profit/loss outcomes)
487
+ */
488
+ CC_ENABLE_PPPL_EVERYWHERE: false,
476
489
  /**
477
490
  * Cost of entering a position (in USD).
478
491
  * This is used as a default value for calculating position size and risk management when cost data is not provided by the strategy
@@ -3550,19 +3563,6 @@ const beginTime = (run) => (...args) => {
3550
3563
  return fn();
3551
3564
  };
3552
3565
 
3553
- /**
3554
- * Retrieves the current timestamp for debugging purposes.
3555
- * If an execution context is active (e.g., during a backtest), it returns the timestamp from the context to ensure consistency with the simulated time.
3556
- * Can be empty (undefined) if not called from strategy async context, as it's intended for debugging and not critical for logic.
3557
- * @return {number | undefined} The current timestamp in milliseconds from the execution context, or undefined if not available.
3558
- */
3559
- const getDebugTimestamp = () => {
3560
- if (ExecutionContextService.hasContext()) {
3561
- return bt.executionContextService.context.when.getTime();
3562
- }
3563
- return undefined;
3564
- };
3565
-
3566
3566
  const INTERVAL_MINUTES$6 = {
3567
3567
  "1m": 1,
3568
3568
  "3m": 3,
@@ -4270,7 +4270,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4270
4270
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
4271
4271
  timestamp: currentTime,
4272
4272
  _isScheduled: false,
4273
- _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: currentTime }],
4273
+ _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4274
4274
  };
4275
4275
  // Валидируем сигнал перед возвратом
4276
4276
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -4294,7 +4294,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4294
4294
  pendingAt: SCHEDULED_SIGNAL_PENDING_MOCK, // Временно, обновится при активации
4295
4295
  timestamp: currentTime,
4296
4296
  _isScheduled: true,
4297
- _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: currentTime }],
4297
+ _entry: [{ price: signal.priceOpen, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4298
4298
  };
4299
4299
  // Валидируем сигнал перед возвратом
4300
4300
  VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
@@ -4314,7 +4314,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4314
4314
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
4315
4315
  timestamp: currentTime,
4316
4316
  _isScheduled: false,
4317
- _entry: [{ price: currentPrice, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: currentTime }],
4317
+ _entry: [{ price: currentPrice, cost: signal.cost ?? GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp: currentTime }],
4318
4318
  };
4319
4319
  // Валидируем сигнал перед возвратом
4320
4320
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -4384,7 +4384,7 @@ const WAIT_FOR_DISPOSE_FN$1 = async (self) => {
4384
4384
  self.params.logger.debug("ClientStrategy dispose");
4385
4385
  await self.params.onDispose(self.params.execution.context.symbol, self.params.strategyName, self.params.exchangeName, self.params.method.context.frameName, self.params.execution.context.backtest);
4386
4386
  };
4387
- const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4387
+ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
4388
4388
  // Initialize partial array if not present
4389
4389
  if (!signal._partial)
4390
4390
  signal._partial = [];
@@ -4413,7 +4413,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4413
4413
  entryCountAtClose,
4414
4414
  currentPrice,
4415
4415
  costBasisAtClose: remainingCostBasis,
4416
- debugTimestamp: getDebugTimestamp(),
4416
+ timestamp,
4417
4417
  });
4418
4418
  self.params.logger.info("PARTIAL_PROFIT_FN executed", {
4419
4419
  signalId: signal.id,
@@ -4423,7 +4423,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4423
4423
  });
4424
4424
  return true;
4425
4425
  };
4426
- const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
4426
+ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice, timestamp) => {
4427
4427
  // Initialize partial array if not present
4428
4428
  if (!signal._partial)
4429
4429
  signal._partial = [];
@@ -4451,7 +4451,7 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
4451
4451
  currentPrice,
4452
4452
  entryCountAtClose,
4453
4453
  costBasisAtClose: remainingCostBasis,
4454
- debugTimestamp: getDebugTimestamp(),
4454
+ timestamp,
4455
4455
  });
4456
4456
  self.params.logger.warn("PARTIAL_LOSS_FN executed", {
4457
4457
  signalId: signal.id,
@@ -4848,10 +4848,10 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4848
4848
  });
4849
4849
  return true;
4850
4850
  };
4851
- const AVERAGE_BUY_FN = (self, signal, currentPrice, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) => {
4851
+ const AVERAGE_BUY_FN = (self, signal, currentPrice, timestamp, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) => {
4852
4852
  // Ensure _entry is initialized (handles signals loaded from disk without _entry)
4853
4853
  if (!signal._entry || signal._entry.length === 0) {
4854
- signal._entry = [{ price: signal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, debugTimestamp: getDebugTimestamp() }];
4854
+ signal._entry = [{ price: signal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp }];
4855
4855
  }
4856
4856
  if (signal.position === "long") {
4857
4857
  // LONG: new entry must beat the all-time low — strictly below every prior entry price
@@ -4881,7 +4881,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice, cost = GLOBAL_CONFIG.CC_POSI
4881
4881
  return false;
4882
4882
  }
4883
4883
  }
4884
- signal._entry.push({ price: currentPrice, cost, debugTimestamp: getDebugTimestamp() });
4884
+ signal._entry.push({ price: currentPrice, cost, timestamp });
4885
4885
  self.params.logger.info("AVERAGE_BUY_FN executed", {
4886
4886
  signalId: signal.id,
4887
4887
  position: signal.position,
@@ -6666,16 +6666,16 @@ class ClientStrategy {
6666
6666
  * // No DCA: [{ price: 43000, cost: 100 }]
6667
6667
  * // One DCA: [{ price: 43000, cost: 100 }, { price: 42000, cost: 100 }]
6668
6668
  */
6669
- async getPositionEntries(symbol) {
6669
+ async getPositionEntries(symbol, timestamp) {
6670
6670
  this.params.logger.debug("ClientStrategy getPositionEntries", { symbol });
6671
6671
  if (!this._pendingSignal) {
6672
6672
  return null;
6673
6673
  }
6674
6674
  const entries = this._pendingSignal._entry;
6675
6675
  if (!entries || entries.length === 0) {
6676
- return [{ price: this._pendingSignal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST }];
6676
+ return [{ price: this._pendingSignal.priceOpen, cost: GLOBAL_CONFIG.CC_POSITION_ENTRY_COST, timestamp }];
6677
6677
  }
6678
- return entries.map(({ price, cost }) => ({ price, cost }));
6678
+ return entries.map(({ price, cost, timestamp }) => ({ price, cost, timestamp }));
6679
6679
  }
6680
6680
  /**
6681
6681
  * Performs a single tick of strategy execution.
@@ -7448,10 +7448,12 @@ class ClientStrategy {
7448
7448
  if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7449
7449
  return false;
7450
7450
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7451
- if (this._pendingSignal.position === "long" && currentPrice <= effectivePriceOpen)
7452
- return false;
7453
- if (this._pendingSignal.position === "short" && currentPrice >= effectivePriceOpen)
7454
- return false;
7451
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7452
+ if (this._pendingSignal.position === "long" && currentPrice <= effectivePriceOpen)
7453
+ return false;
7454
+ if (this._pendingSignal.position === "short" && currentPrice >= effectivePriceOpen)
7455
+ return false;
7456
+ }
7455
7457
  const effectiveTakeProfit = this._pendingSignal._trailingPriceTakeProfit ?? this._pendingSignal.priceTakeProfit;
7456
7458
  if (this._pendingSignal.position === "long" && currentPrice >= effectiveTakeProfit)
7457
7459
  return false;
@@ -7509,7 +7511,7 @@ class ClientStrategy {
7509
7511
  * // success3 = false (skipped, would exceed 100%)
7510
7512
  * ```
7511
7513
  */
7512
- async partialProfit(symbol, percentToClose, currentPrice, backtest) {
7514
+ async partialProfit(symbol, percentToClose, currentPrice, backtest, timestamp) {
7513
7515
  this.params.logger.debug("ClientStrategy partialProfit", {
7514
7516
  symbol,
7515
7517
  percentToClose,
@@ -7535,7 +7537,7 @@ class ClientStrategy {
7535
7537
  throw new Error(`ClientStrategy partialProfit: currentPrice must be a positive finite number, got ${currentPrice}`);
7536
7538
  }
7537
7539
  // Validation: currentPrice must be moving toward TP (profit direction)
7538
- {
7540
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7539
7541
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7540
7542
  if (this._pendingSignal.position === "long") {
7541
7543
  // For LONG: currentPrice must be higher than effectivePriceOpen (moving toward TP)
@@ -7573,7 +7575,7 @@ class ClientStrategy {
7573
7575
  return false;
7574
7576
  }
7575
7577
  // Execute partial close logic
7576
- const wasExecuted = PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
7578
+ const wasExecuted = PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice, timestamp);
7577
7579
  // If partial was not executed (exceeded 100%), return false without persistence
7578
7580
  if (!wasExecuted) {
7579
7581
  return false;
@@ -7629,10 +7631,12 @@ class ClientStrategy {
7629
7631
  if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0)
7630
7632
  return false;
7631
7633
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7632
- if (this._pendingSignal.position === "long" && currentPrice >= effectivePriceOpen)
7633
- return false;
7634
- if (this._pendingSignal.position === "short" && currentPrice <= effectivePriceOpen)
7635
- return false;
7634
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7635
+ if (this._pendingSignal.position === "long" && currentPrice >= effectivePriceOpen)
7636
+ return false;
7637
+ if (this._pendingSignal.position === "short" && currentPrice <= effectivePriceOpen)
7638
+ return false;
7639
+ }
7636
7640
  const effectiveStopLoss = this._pendingSignal._trailingPriceStopLoss ?? this._pendingSignal.priceStopLoss;
7637
7641
  if (this._pendingSignal.position === "long" && currentPrice <= effectiveStopLoss)
7638
7642
  return false;
@@ -7690,7 +7694,7 @@ class ClientStrategy {
7690
7694
  * // success3 = false (skipped, would exceed 100%)
7691
7695
  * ```
7692
7696
  */
7693
- async partialLoss(symbol, percentToClose, currentPrice, backtest) {
7697
+ async partialLoss(symbol, percentToClose, currentPrice, backtest, timestamp) {
7694
7698
  this.params.logger.debug("ClientStrategy partialLoss", {
7695
7699
  symbol,
7696
7700
  percentToClose,
@@ -7716,7 +7720,7 @@ class ClientStrategy {
7716
7720
  throw new Error(`ClientStrategy partialLoss: currentPrice must be a positive finite number, got ${currentPrice}`);
7717
7721
  }
7718
7722
  // Validation: currentPrice must be moving toward SL (loss direction)
7719
- {
7723
+ if (!GLOBAL_CONFIG.CC_ENABLE_PPPL_EVERYWHERE) {
7720
7724
  const effectivePriceOpen = getEffectivePriceOpen(this._pendingSignal);
7721
7725
  if (this._pendingSignal.position === "long") {
7722
7726
  // For LONG: currentPrice must be lower than effectivePriceOpen (moving toward SL)
@@ -7754,7 +7758,7 @@ class ClientStrategy {
7754
7758
  return false;
7755
7759
  }
7756
7760
  // Execute partial close logic
7757
- const wasExecuted = PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
7761
+ const wasExecuted = PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice, timestamp);
7758
7762
  // If partial was not executed (exceeded 100%), return false without persistence
7759
7763
  if (!wasExecuted) {
7760
7764
  return false;
@@ -8510,7 +8514,7 @@ class ClientStrategy {
8510
8514
  * @param backtest - Whether running in backtest mode
8511
8515
  * @returns Promise<boolean> - true if entry added, false if rejected by direction check
8512
8516
  */
8513
- async averageBuy(symbol, currentPrice, backtest, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) {
8517
+ async averageBuy(symbol, currentPrice, backtest, timestamp, cost = GLOBAL_CONFIG.CC_POSITION_ENTRY_COST) {
8514
8518
  this.params.logger.debug("ClientStrategy averageBuy", {
8515
8519
  symbol,
8516
8520
  currentPrice,
@@ -8525,7 +8529,7 @@ class ClientStrategy {
8525
8529
  throw new Error(`ClientStrategy averageBuy: currentPrice must be a positive finite number, got ${currentPrice}`);
8526
8530
  }
8527
8531
  // Execute averaging logic
8528
- const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice, cost);
8532
+ const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice, timestamp, cost);
8529
8533
  if (!result) {
8530
8534
  return false;
8531
8535
  }
@@ -8989,7 +8993,7 @@ const GET_RISK_FN = (dto, backtest, exchangeName, frameName, self) => {
8989
8993
  * @param backtest - Whether running in backtest mode
8990
8994
  * @returns Unique string key for memoization
8991
8995
  */
8992
- const CREATE_KEY_FN$m = (symbol, strategyName, exchangeName, frameName, backtest) => {
8996
+ const CREATE_KEY_FN$o = (symbol, strategyName, exchangeName, frameName, backtest) => {
8993
8997
  const parts = [symbol, strategyName, exchangeName];
8994
8998
  if (frameName)
8995
8999
  parts.push(frameName);
@@ -9166,6 +9170,8 @@ class StrategyConnectionService {
9166
9170
  this.partialConnectionService = inject(TYPES.partialConnectionService);
9167
9171
  this.breakevenConnectionService = inject(TYPES.breakevenConnectionService);
9168
9172
  this.actionCoreService = inject(TYPES.actionCoreService);
9173
+ this.timeMetaService = inject(TYPES.timeMetaService);
9174
+ this.priceMetaService = inject(TYPES.priceMetaService);
9169
9175
  /**
9170
9176
  * Retrieves memoized ClientStrategy instance for given symbol-strategy pair with exchange and frame isolation.
9171
9177
  *
@@ -9179,7 +9185,7 @@ class StrategyConnectionService {
9179
9185
  * @param backtest - Whether running in backtest mode
9180
9186
  * @returns Configured ClientStrategy instance
9181
9187
  */
9182
- this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$m(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
9188
+ this.getStrategy = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$o(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => {
9183
9189
  const { riskName = "", riskList = [], getSignal, interval, callbacks, } = this.strategySchemaService.get(strategyName);
9184
9190
  return new ClientStrategy({
9185
9191
  symbol,
@@ -9266,6 +9272,20 @@ class StrategyConnectionService {
9266
9272
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9267
9273
  return await strategy.getTotalCostClosed(symbol);
9268
9274
  };
9275
+ /**
9276
+ * Returns the effective (DCA-averaged) entry price for the current pending signal.
9277
+ *
9278
+ * This is the harmonic mean of all _entry prices, which is the correct
9279
+ * cost-basis price used in all PNL calculations.
9280
+ * With no DCA entries, equals the original priceOpen.
9281
+ *
9282
+ * Returns null if no pending signal exists.
9283
+ *
9284
+ * @param backtest - Whether running in backtest mode
9285
+ * @param symbol - Trading pair symbol
9286
+ * @param context - Execution context with strategyName, exchangeName, frameName
9287
+ * @returns Promise resolving to effective entry price or null
9288
+ */
9269
9289
  this.getPositionAveragePrice = async (backtest, symbol, context) => {
9270
9290
  this.loggerService.log("strategyConnectionService getPositionAveragePrice", {
9271
9291
  symbol,
@@ -9275,6 +9295,19 @@ class StrategyConnectionService {
9275
9295
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9276
9296
  return await strategy.getPositionAveragePrice(symbol);
9277
9297
  };
9298
+ /**
9299
+ * Returns the number of DCA entries made for the current pending signal.
9300
+ *
9301
+ * 1 = original entry only (no DCA).
9302
+ * Increases by 1 with each successful commitAverageBuy().
9303
+ *
9304
+ * Returns null if no pending signal exists.
9305
+ *
9306
+ * @param backtest - Whether running in backtest mode
9307
+ * @param symbol - Trading pair symbol
9308
+ * @param context - Execution context with strategyName, exchangeName, frameName
9309
+ * @returns Promise resolving to entry count or null
9310
+ */
9278
9311
  this.getPositionInvestedCount = async (backtest, symbol, context) => {
9279
9312
  this.loggerService.log("strategyConnectionService getPositionInvestedCount", {
9280
9313
  symbol,
@@ -9284,6 +9317,19 @@ class StrategyConnectionService {
9284
9317
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9285
9318
  return await strategy.getPositionInvestedCount(symbol);
9286
9319
  };
9320
+ /**
9321
+ * Returns the total invested cost basis in dollars for the current pending signal.
9322
+ *
9323
+ * Equal to entryCount × $100 (COST_BASIS_PER_ENTRY).
9324
+ * 1 entry = $100, 2 entries = $200, etc.
9325
+ *
9326
+ * Returns null if no pending signal exists.
9327
+ *
9328
+ * @param backtest - Whether running in backtest mode
9329
+ * @param symbol - Trading pair symbol
9330
+ * @param context - Execution context with strategyName, exchangeName, frameName
9331
+ * @returns Promise resolving to total invested cost in dollars or null
9332
+ */
9287
9333
  this.getPositionInvestedCost = async (backtest, symbol, context) => {
9288
9334
  this.loggerService.log("strategyConnectionService getPositionInvestedCost", {
9289
9335
  symbol,
@@ -9293,6 +9339,20 @@ class StrategyConnectionService {
9293
9339
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9294
9340
  return await strategy.getPositionInvestedCost(symbol);
9295
9341
  };
9342
+ /**
9343
+ * Returns the unrealized PNL percentage for the current pending signal at currentPrice.
9344
+ *
9345
+ * Accounts for partial closes, DCA entries, slippage and fees
9346
+ * (delegates to toProfitLossDto).
9347
+ *
9348
+ * Returns null if no pending signal exists.
9349
+ *
9350
+ * @param backtest - Whether running in backtest mode
9351
+ * @param symbol - Trading pair symbol
9352
+ * @param currentPrice - Current market price
9353
+ * @param context - Execution context with strategyName, exchangeName, frameName
9354
+ * @returns Promise resolving to pnlPercentage or null
9355
+ */
9296
9356
  this.getPositionPnlPercent = async (backtest, symbol, currentPrice, context) => {
9297
9357
  this.loggerService.log("strategyConnectionService getPositionPnlPercent", {
9298
9358
  symbol,
@@ -9303,6 +9363,20 @@ class StrategyConnectionService {
9303
9363
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9304
9364
  return await strategy.getPositionPnlPercent(symbol, currentPrice);
9305
9365
  };
9366
+ /**
9367
+ * Returns the unrealized PNL in dollars for the current pending signal at currentPrice.
9368
+ *
9369
+ * Calculated as: pnlPercentage / 100 × totalInvestedCost
9370
+ * Accounts for partial closes, DCA entries, slippage and fees.
9371
+ *
9372
+ * Returns null if no pending signal exists.
9373
+ *
9374
+ * @param backtest - Whether running in backtest mode
9375
+ * @param symbol - Trading pair symbol
9376
+ * @param currentPrice - Current market price
9377
+ * @param context - Execution context with strategyName, exchangeName, frameName
9378
+ * @returns Promise resolving to pnl in dollars or null
9379
+ */
9306
9380
  this.getPositionPnlCost = async (backtest, symbol, currentPrice, context) => {
9307
9381
  this.loggerService.log("strategyConnectionService getPositionPnlCost", {
9308
9382
  symbol,
@@ -9313,6 +9387,27 @@ class StrategyConnectionService {
9313
9387
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9314
9388
  return await strategy.getPositionPnlCost(symbol, currentPrice);
9315
9389
  };
9390
+ /**
9391
+ * Returns the list of DCA entry prices for the current pending signal.
9392
+ *
9393
+ * The first element is always the original priceOpen (initial entry).
9394
+ * Each subsequent element is a price added by commitAverageBuy().
9395
+ *
9396
+ * Returns null if no pending signal exists.
9397
+ * Returns a single-element array [priceOpen] if no DCA entries were made.
9398
+ *
9399
+ * @param backtest - Whether running in backtest mode
9400
+ * @param symbol - Trading pair symbol
9401
+ * @param context - Execution context with strategyName, exchangeName, frameName
9402
+ * @returns Promise resolving to array of entry prices or null
9403
+ *
9404
+ * @example
9405
+ * ```typescript
9406
+ * // No DCA: [43000]
9407
+ * // One DCA: [43000, 42000]
9408
+ * // Two DCA: [43000, 42000, 41500]
9409
+ * ```
9410
+ */
9316
9411
  this.getPositionLevels = async (backtest, symbol, context) => {
9317
9412
  this.loggerService.log("strategyConnectionService getPositionLevels", {
9318
9413
  symbol,
@@ -9322,6 +9417,20 @@ class StrategyConnectionService {
9322
9417
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9323
9418
  return await strategy.getPositionLevels(symbol);
9324
9419
  };
9420
+ /**
9421
+ * Returns the list of partial closes for the current pending signal.
9422
+ *
9423
+ * Each entry records a partial profit or loss close event with its type,
9424
+ * percent closed, price at close, cost basis snapshot, and entry count at close.
9425
+ *
9426
+ * Returns null if no pending signal exists.
9427
+ * Returns an empty array if no partial closes have been executed.
9428
+ *
9429
+ * @param backtest - Whether running in backtest mode
9430
+ * @param symbol - Trading pair symbol
9431
+ * @param context - Execution context with strategyName, exchangeName, frameName
9432
+ * @returns Promise resolving to array of partial close records or null
9433
+ */
9325
9434
  this.getPositionPartials = async (backtest, symbol, context) => {
9326
9435
  this.loggerService.log("strategyConnectionService getPositionPartials", {
9327
9436
  symbol,
@@ -9331,6 +9440,27 @@ class StrategyConnectionService {
9331
9440
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9332
9441
  return await strategy.getPositionPartials(symbol);
9333
9442
  };
9443
+ /**
9444
+ * Returns the list of DCA entry prices and costs for the current pending signal.
9445
+ *
9446
+ * Each entry records the price and cost of a single position entry.
9447
+ * The first element is always the original priceOpen (initial entry).
9448
+ * Each subsequent element is an entry added by averageBuy().
9449
+ *
9450
+ * Returns null if no pending signal exists.
9451
+ * Returns a single-element array [{ price: priceOpen, cost }] if no DCA entries were made.
9452
+ *
9453
+ * @param backtest - Whether running in backtest mode
9454
+ * @param symbol - Trading pair symbol
9455
+ * @param context - Execution context with strategyName, exchangeName, frameName
9456
+ * @returns Promise resolving to array of entry records or null
9457
+ *
9458
+ * @example
9459
+ * ```typescript
9460
+ * // No DCA: [{ price: 43000, cost: 100 }]
9461
+ * // One DCA: [{ price: 43000, cost: 100 }, { price: 42000, cost: 100 }]
9462
+ * ```
9463
+ */
9334
9464
  this.getPositionEntries = async (backtest, symbol, context) => {
9335
9465
  this.loggerService.log("strategyConnectionService getPositionEntries", {
9336
9466
  symbol,
@@ -9338,7 +9468,8 @@ class StrategyConnectionService {
9338
9468
  backtest,
9339
9469
  });
9340
9470
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9341
- return await strategy.getPositionEntries(symbol);
9471
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
9472
+ return await strategy.getPositionEntries(symbol, timestamp);
9342
9473
  };
9343
9474
  /**
9344
9475
  * Retrieves the currently active scheduled signal for the strategy.
@@ -9440,6 +9571,10 @@ class StrategyConnectionService {
9440
9571
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9441
9572
  await strategy.waitForInit();
9442
9573
  const tick = await strategy.tick(symbol, context.strategyName);
9574
+ {
9575
+ this.priceMetaService.next(symbol, tick.currentPrice, context, backtest);
9576
+ this.timeMetaService.next(symbol, tick.createdAt, context, backtest);
9577
+ }
9443
9578
  {
9444
9579
  await CALL_SIGNAL_EMIT_FN(this, tick, context, backtest, symbol);
9445
9580
  }
@@ -9546,7 +9681,7 @@ class StrategyConnectionService {
9546
9681
  }
9547
9682
  return;
9548
9683
  }
9549
- const key = CREATE_KEY_FN$m(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
9684
+ const key = CREATE_KEY_FN$o(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
9550
9685
  if (!this.getStrategy.has(key)) {
9551
9686
  return;
9552
9687
  }
@@ -9615,9 +9750,9 @@ class StrategyConnectionService {
9615
9750
  * @param context - Execution context with strategyName, exchangeName, frameName
9616
9751
  * @returns Promise<boolean> - true if `partialProfit` would execute, false otherwise
9617
9752
  */
9618
- this.validatePartialProfit = (backtest, symbol, percentToClose, currentPrice, context) => {
9753
+ this.validatePartialProfit = async (backtest, symbol, percentToClose, currentPrice, context) => {
9619
9754
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9620
- return Promise.resolve(strategy.validatePartialProfit(symbol, percentToClose, currentPrice));
9755
+ return await strategy.validatePartialProfit(symbol, percentToClose, currentPrice);
9621
9756
  };
9622
9757
  /**
9623
9758
  * Executes partial close at profit level (moving toward TP).
@@ -9658,7 +9793,8 @@ class StrategyConnectionService {
9658
9793
  backtest,
9659
9794
  });
9660
9795
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9661
- return await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest);
9796
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
9797
+ return await strategy.partialProfit(symbol, percentToClose, currentPrice, backtest, timestamp);
9662
9798
  };
9663
9799
  /**
9664
9800
  * Checks whether `partialLoss` would succeed without executing it.
@@ -9671,9 +9807,9 @@ class StrategyConnectionService {
9671
9807
  * @param context - Execution context with strategyName, exchangeName, frameName
9672
9808
  * @returns Promise<boolean> - true if `partialLoss` would execute, false otherwise
9673
9809
  */
9674
- this.validatePartialLoss = (backtest, symbol, percentToClose, currentPrice, context) => {
9810
+ this.validatePartialLoss = async (backtest, symbol, percentToClose, currentPrice, context) => {
9675
9811
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9676
- return Promise.resolve(strategy.validatePartialLoss(symbol, percentToClose, currentPrice));
9812
+ return await strategy.validatePartialLoss(symbol, percentToClose, currentPrice);
9677
9813
  };
9678
9814
  /**
9679
9815
  * Executes partial close at loss level (moving toward SL).
@@ -9714,7 +9850,8 @@ class StrategyConnectionService {
9714
9850
  backtest,
9715
9851
  });
9716
9852
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9717
- return await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest);
9853
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
9854
+ return await strategy.partialLoss(symbol, percentToClose, currentPrice, backtest, timestamp);
9718
9855
  };
9719
9856
  /**
9720
9857
  * Checks whether `trailingStop` would succeed without executing it.
@@ -9727,9 +9864,9 @@ class StrategyConnectionService {
9727
9864
  * @param context - Execution context with strategyName, exchangeName, frameName
9728
9865
  * @returns Promise<boolean> - true if `trailingStop` would execute, false otherwise
9729
9866
  */
9730
- this.validateTrailingStop = (backtest, symbol, percentShift, currentPrice, context) => {
9867
+ this.validateTrailingStop = async (backtest, symbol, percentShift, currentPrice, context) => {
9731
9868
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9732
- return Promise.resolve(strategy.validateTrailingStop(symbol, percentShift, currentPrice));
9869
+ return await strategy.validateTrailingStop(symbol, percentShift, currentPrice);
9733
9870
  };
9734
9871
  /**
9735
9872
  * Adjusts the trailing stop-loss distance for an active pending signal.
@@ -9781,9 +9918,9 @@ class StrategyConnectionService {
9781
9918
  * @param context - Execution context with strategyName, exchangeName, frameName
9782
9919
  * @returns Promise<boolean> - true if `trailingTake` would execute, false otherwise
9783
9920
  */
9784
- this.validateTrailingTake = (backtest, symbol, percentShift, currentPrice, context) => {
9921
+ this.validateTrailingTake = async (backtest, symbol, percentShift, currentPrice, context) => {
9785
9922
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9786
- return Promise.resolve(strategy.validateTrailingTake(symbol, percentShift, currentPrice));
9923
+ return await strategy.validateTrailingTake(symbol, percentShift, currentPrice);
9787
9924
  };
9788
9925
  /**
9789
9926
  * Adjusts the trailing take-profit distance for an active pending signal.
@@ -9834,9 +9971,9 @@ class StrategyConnectionService {
9834
9971
  * @param context - Execution context with strategyName, exchangeName, frameName
9835
9972
  * @returns Promise<boolean> - true if `breakeven` would execute, false otherwise
9836
9973
  */
9837
- this.validateBreakeven = (backtest, symbol, currentPrice, context) => {
9974
+ this.validateBreakeven = async (backtest, symbol, currentPrice, context) => {
9838
9975
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9839
- return Promise.resolve(strategy.validateBreakeven(symbol, currentPrice));
9976
+ return await strategy.validateBreakeven(symbol, currentPrice);
9840
9977
  };
9841
9978
  /**
9842
9979
  * Delegates to ClientStrategy.breakeven() with current execution context.
@@ -9913,9 +10050,9 @@ class StrategyConnectionService {
9913
10050
  * @param context - Execution context with strategyName, exchangeName, frameName
9914
10051
  * @returns Promise<boolean> - true if `averageBuy` would execute, false otherwise
9915
10052
  */
9916
- this.validateAverageBuy = (backtest, symbol, currentPrice, context) => {
10053
+ this.validateAverageBuy = async (backtest, symbol, currentPrice, context) => {
9917
10054
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9918
- return Promise.resolve(strategy.validateAverageBuy(symbol, currentPrice));
10055
+ return await strategy.validateAverageBuy(symbol, currentPrice);
9919
10056
  };
9920
10057
  /**
9921
10058
  * Adds a new DCA entry to the active pending signal.
@@ -9936,7 +10073,8 @@ class StrategyConnectionService {
9936
10073
  backtest,
9937
10074
  });
9938
10075
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
9939
- return await strategy.averageBuy(symbol, currentPrice, backtest, cost);
10076
+ const timestamp = await this.timeMetaService.getTimestamp(symbol, context, backtest);
10077
+ return await strategy.averageBuy(symbol, currentPrice, backtest, timestamp, cost);
9940
10078
  };
9941
10079
  }
9942
10080
  }
@@ -10677,7 +10815,7 @@ class ClientRisk {
10677
10815
  * @param backtest - Whether running in backtest mode
10678
10816
  * @returns Unique string key for memoization
10679
10817
  */
10680
- const CREATE_KEY_FN$l = (riskName, exchangeName, frameName, backtest) => {
10818
+ const CREATE_KEY_FN$n = (riskName, exchangeName, frameName, backtest) => {
10681
10819
  const parts = [riskName, exchangeName];
10682
10820
  if (frameName)
10683
10821
  parts.push(frameName);
@@ -10776,7 +10914,7 @@ class RiskConnectionService {
10776
10914
  * @param backtest - True if backtest mode, false if live mode
10777
10915
  * @returns Configured ClientRisk instance
10778
10916
  */
10779
- this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$l(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
10917
+ this.getRisk = functoolsKit.memoize(([riskName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$n(riskName, exchangeName, frameName, backtest), (riskName, exchangeName, frameName, backtest) => {
10780
10918
  const schema = this.riskSchemaService.get(riskName);
10781
10919
  return new ClientRisk({
10782
10920
  ...schema,
@@ -10844,7 +10982,7 @@ class RiskConnectionService {
10844
10982
  payload,
10845
10983
  });
10846
10984
  if (payload) {
10847
- const key = CREATE_KEY_FN$l(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
10985
+ const key = CREATE_KEY_FN$n(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest);
10848
10986
  this.getRisk.clear(key);
10849
10987
  }
10850
10988
  else {
@@ -10864,7 +11002,6 @@ const METHOD_NAME_PARTIAL_LOSS_AVAILABLE = "ActionBase.partialLossAvailable";
10864
11002
  const METHOD_NAME_PING_SCHEDULED = "ActionBase.pingScheduled";
10865
11003
  const METHOD_NAME_PING_ACTIVE = "ActionBase.pingActive";
10866
11004
  const METHOD_NAME_RISK_REJECTION = "ActionBase.riskRejection";
10867
- const METHOD_NAME_SIGNAL_SYNC = "ActionBase.signalSync";
10868
11005
  const METHOD_NAME_DISPOSE = "ActionBase.dispose";
10869
11006
  const DEFAULT_SOURCE = "default";
10870
11007
  /**
@@ -11276,6 +11413,11 @@ class ActionProxy {
11276
11413
  */
11277
11414
  async signalSync(event) {
11278
11415
  if (this._target.signalSync) {
11416
+ console.error("Action::signalSync is unwanted cause exchange integration should be implemented in Broker.useBrokerAdapter as an infrastructure domain layer");
11417
+ console.error("If you need to implement custom logic on signal open/close, please use signal(), signalBacktest(), signalLive()");
11418
+ console.error("If Action::signalSync throws the exchange will not execute the order!");
11419
+ console.error("");
11420
+ console.error("You have been warned!");
11279
11421
  await this._target.signalSync(event);
11280
11422
  }
11281
11423
  }
@@ -11738,19 +11880,6 @@ class ActionBase {
11738
11880
  source,
11739
11881
  });
11740
11882
  }
11741
- /**
11742
- * Gate for position open/close via limit order. Default allows all.
11743
- * Throw to reject — framework retries next tick.
11744
- *
11745
- * NOTE: Exceptions are NOT swallowed — they propagate to CREATE_SYNC_FN.
11746
- *
11747
- * @param event - Sync event with action "signal-open" or "signal-close"
11748
- */
11749
- signalSync(_event, source = DEFAULT_SOURCE) {
11750
- bt.loggerService.info(METHOD_NAME_SIGNAL_SYNC, {
11751
- source,
11752
- });
11753
- }
11754
11883
  /**
11755
11884
  * Cleans up resources and subscriptions when action handler is disposed.
11756
11885
  *
@@ -12320,7 +12449,7 @@ class ClientAction {
12320
12449
  * @param backtest - Whether running in backtest mode
12321
12450
  * @returns Unique string key for memoization
12322
12451
  */
12323
- const CREATE_KEY_FN$k = (actionName, strategyName, exchangeName, frameName, backtest) => {
12452
+ const CREATE_KEY_FN$m = (actionName, strategyName, exchangeName, frameName, backtest) => {
12324
12453
  const parts = [actionName, strategyName, exchangeName];
12325
12454
  if (frameName)
12326
12455
  parts.push(frameName);
@@ -12371,7 +12500,7 @@ class ActionConnectionService {
12371
12500
  * @param backtest - True if backtest mode, false if live mode
12372
12501
  * @returns Configured ClientAction instance
12373
12502
  */
12374
- this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$k(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
12503
+ this.getAction = functoolsKit.memoize(([actionName, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$m(actionName, strategyName, exchangeName, frameName, backtest), (actionName, strategyName, exchangeName, frameName, backtest) => {
12375
12504
  const schema = this.actionSchemaService.get(actionName);
12376
12505
  return new ClientAction({
12377
12506
  ...schema,
@@ -12581,7 +12710,7 @@ class ActionConnectionService {
12581
12710
  await Promise.all(actions.map(async (action) => await action.dispose()));
12582
12711
  return;
12583
12712
  }
12584
- const key = CREATE_KEY_FN$k(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
12713
+ const key = CREATE_KEY_FN$m(payload.actionName, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
12585
12714
  if (!this.getAction.has(key)) {
12586
12715
  return;
12587
12716
  }
@@ -12599,7 +12728,7 @@ const METHOD_NAME_VALIDATE$2 = "exchangeCoreService validate";
12599
12728
  * @param exchangeName - Exchange name
12600
12729
  * @returns Unique string key for memoization
12601
12730
  */
12602
- const CREATE_KEY_FN$j = (exchangeName) => {
12731
+ const CREATE_KEY_FN$l = (exchangeName) => {
12603
12732
  return exchangeName;
12604
12733
  };
12605
12734
  /**
@@ -12623,7 +12752,7 @@ class ExchangeCoreService {
12623
12752
  * @param exchangeName - Name of the exchange to validate
12624
12753
  * @returns Promise that resolves when validation is complete
12625
12754
  */
12626
- this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$j(exchangeName), async (exchangeName) => {
12755
+ this.validate = functoolsKit.memoize(([exchangeName]) => CREATE_KEY_FN$l(exchangeName), async (exchangeName) => {
12627
12756
  this.loggerService.log(METHOD_NAME_VALIDATE$2, {
12628
12757
  exchangeName,
12629
12758
  });
@@ -12875,7 +13004,7 @@ const METHOD_NAME_VALIDATE$1 = "strategyCoreService validate";
12875
13004
  * @param context - Execution context with strategyName, exchangeName, frameName
12876
13005
  * @returns Unique string key for memoization
12877
13006
  */
12878
- const CREATE_KEY_FN$i = (context) => {
13007
+ const CREATE_KEY_FN$k = (context) => {
12879
13008
  const parts = [context.strategyName, context.exchangeName];
12880
13009
  if (context.frameName)
12881
13010
  parts.push(context.frameName);
@@ -12907,7 +13036,7 @@ class StrategyCoreService {
12907
13036
  * @param context - Execution context with strategyName, exchangeName, frameName
12908
13037
  * @returns Promise that resolves when validation is complete
12909
13038
  */
12910
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$i(context), async (context) => {
13039
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$k(context), async (context) => {
12911
13040
  this.loggerService.log(METHOD_NAME_VALIDATE$1, {
12912
13041
  context,
12913
13042
  });
@@ -13742,7 +13871,7 @@ class SizingGlobalService {
13742
13871
  * @param context - Context with riskName, exchangeName, frameName
13743
13872
  * @returns Unique string key for memoization
13744
13873
  */
13745
- const CREATE_KEY_FN$h = (context) => {
13874
+ const CREATE_KEY_FN$j = (context) => {
13746
13875
  const parts = [context.riskName, context.exchangeName];
13747
13876
  if (context.frameName)
13748
13877
  parts.push(context.frameName);
@@ -13768,7 +13897,7 @@ class RiskGlobalService {
13768
13897
  * @param payload - Payload with riskName, exchangeName and frameName
13769
13898
  * @returns Promise that resolves when validation is complete
13770
13899
  */
13771
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$h(context), async (context) => {
13900
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$j(context), async (context) => {
13772
13901
  this.loggerService.log("riskGlobalService validate", {
13773
13902
  context,
13774
13903
  });
@@ -13846,7 +13975,7 @@ const METHOD_NAME_VALIDATE = "actionCoreService validate";
13846
13975
  * @param context - Execution context with strategyName, exchangeName, frameName
13847
13976
  * @returns Unique string key for memoization
13848
13977
  */
13849
- const CREATE_KEY_FN$g = (context) => {
13978
+ const CREATE_KEY_FN$i = (context) => {
13850
13979
  const parts = [context.strategyName, context.exchangeName];
13851
13980
  if (context.frameName)
13852
13981
  parts.push(context.frameName);
@@ -13890,7 +14019,7 @@ class ActionCoreService {
13890
14019
  * @param context - Strategy execution context with strategyName, exchangeName and frameName
13891
14020
  * @returns Promise that resolves when all validations complete
13892
14021
  */
13893
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$g(context), async (context) => {
14022
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$i(context), async (context) => {
13894
14023
  this.loggerService.log(METHOD_NAME_VALIDATE, {
13895
14024
  context,
13896
14025
  });
@@ -18425,7 +18554,7 @@ const Markdown = new MarkdownAdapter();
18425
18554
  * @param backtest - Whether running in backtest mode
18426
18555
  * @returns Unique string key for memoization
18427
18556
  */
18428
- const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
18557
+ const CREATE_KEY_FN$h = (symbol, strategyName, exchangeName, frameName, backtest) => {
18429
18558
  const parts = [symbol, strategyName, exchangeName];
18430
18559
  if (frameName)
18431
18560
  parts.push(frameName);
@@ -18664,7 +18793,7 @@ class BacktestMarkdownService {
18664
18793
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
18665
18794
  * Each combination gets its own isolated storage instance.
18666
18795
  */
18667
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$f(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$8(symbol, strategyName, exchangeName, frameName));
18796
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$h(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$8(symbol, strategyName, exchangeName, frameName));
18668
18797
  /**
18669
18798
  * Processes tick events and accumulates closed signals.
18670
18799
  * Should be called from IStrategyCallbacks.onTick.
@@ -18821,7 +18950,7 @@ class BacktestMarkdownService {
18821
18950
  payload,
18822
18951
  });
18823
18952
  if (payload) {
18824
- const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
18953
+ const key = CREATE_KEY_FN$h(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
18825
18954
  this.getStorage.clear(key);
18826
18955
  }
18827
18956
  else {
@@ -18883,7 +19012,7 @@ class BacktestMarkdownService {
18883
19012
  * @param backtest - Whether running in backtest mode
18884
19013
  * @returns Unique string key for memoization
18885
19014
  */
18886
- const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest) => {
19015
+ const CREATE_KEY_FN$g = (symbol, strategyName, exchangeName, frameName, backtest) => {
18887
19016
  const parts = [symbol, strategyName, exchangeName];
18888
19017
  if (frameName)
18889
19018
  parts.push(frameName);
@@ -19366,7 +19495,7 @@ class LiveMarkdownService {
19366
19495
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
19367
19496
  * Each combination gets its own isolated storage instance.
19368
19497
  */
19369
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$e(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$7(symbol, strategyName, exchangeName, frameName));
19498
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$g(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$7(symbol, strategyName, exchangeName, frameName));
19370
19499
  /**
19371
19500
  * Subscribes to live signal emitter to receive tick events.
19372
19501
  * Protected against multiple subscriptions.
@@ -19584,7 +19713,7 @@ class LiveMarkdownService {
19584
19713
  payload,
19585
19714
  });
19586
19715
  if (payload) {
19587
- const key = CREATE_KEY_FN$e(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
19716
+ const key = CREATE_KEY_FN$g(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
19588
19717
  this.getStorage.clear(key);
19589
19718
  }
19590
19719
  else {
@@ -19604,7 +19733,7 @@ class LiveMarkdownService {
19604
19733
  * @param backtest - Whether running in backtest mode
19605
19734
  * @returns Unique string key for memoization
19606
19735
  */
19607
- const CREATE_KEY_FN$d = (symbol, strategyName, exchangeName, frameName, backtest) => {
19736
+ const CREATE_KEY_FN$f = (symbol, strategyName, exchangeName, frameName, backtest) => {
19608
19737
  const parts = [symbol, strategyName, exchangeName];
19609
19738
  if (frameName)
19610
19739
  parts.push(frameName);
@@ -19895,7 +20024,7 @@ class ScheduleMarkdownService {
19895
20024
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
19896
20025
  * Each combination gets its own isolated storage instance.
19897
20026
  */
19898
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$d(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$6(symbol, strategyName, exchangeName, frameName));
20027
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$f(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$6(symbol, strategyName, exchangeName, frameName));
19899
20028
  /**
19900
20029
  * Subscribes to signal emitter to receive scheduled signal events.
19901
20030
  * Protected against multiple subscriptions.
@@ -20098,7 +20227,7 @@ class ScheduleMarkdownService {
20098
20227
  payload,
20099
20228
  });
20100
20229
  if (payload) {
20101
- const key = CREATE_KEY_FN$d(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20230
+ const key = CREATE_KEY_FN$f(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20102
20231
  this.getStorage.clear(key);
20103
20232
  }
20104
20233
  else {
@@ -20118,7 +20247,7 @@ class ScheduleMarkdownService {
20118
20247
  * @param backtest - Whether running in backtest mode
20119
20248
  * @returns Unique string key for memoization
20120
20249
  */
20121
- const CREATE_KEY_FN$c = (symbol, strategyName, exchangeName, frameName, backtest) => {
20250
+ const CREATE_KEY_FN$e = (symbol, strategyName, exchangeName, frameName, backtest) => {
20122
20251
  const parts = [symbol, strategyName, exchangeName];
20123
20252
  if (frameName)
20124
20253
  parts.push(frameName);
@@ -20366,7 +20495,7 @@ class PerformanceMarkdownService {
20366
20495
  * Memoized function to get or create PerformanceStorage for a symbol-strategy-exchange-frame-backtest combination.
20367
20496
  * Each combination gets its own isolated storage instance.
20368
20497
  */
20369
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$c(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
20498
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$e(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new PerformanceStorage(symbol, strategyName, exchangeName, frameName));
20370
20499
  /**
20371
20500
  * Subscribes to performance emitter to receive performance events.
20372
20501
  * Protected against multiple subscriptions.
@@ -20533,7 +20662,7 @@ class PerformanceMarkdownService {
20533
20662
  payload,
20534
20663
  });
20535
20664
  if (payload) {
20536
- const key = CREATE_KEY_FN$c(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20665
+ const key = CREATE_KEY_FN$e(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
20537
20666
  this.getStorage.clear(key);
20538
20667
  }
20539
20668
  else {
@@ -21003,7 +21132,7 @@ class WalkerMarkdownService {
21003
21132
  * @param backtest - Whether running in backtest mode
21004
21133
  * @returns Unique string key for memoization
21005
21134
  */
21006
- const CREATE_KEY_FN$b = (exchangeName, frameName, backtest) => {
21135
+ const CREATE_KEY_FN$d = (exchangeName, frameName, backtest) => {
21007
21136
  const parts = [exchangeName];
21008
21137
  if (frameName)
21009
21138
  parts.push(frameName);
@@ -21370,7 +21499,7 @@ class HeatMarkdownService {
21370
21499
  * Memoized function to get or create HeatmapStorage for exchange, frame and backtest mode.
21371
21500
  * Each exchangeName + frameName + backtest mode combination gets its own isolated heatmap storage instance.
21372
21501
  */
21373
- this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$b(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
21502
+ this.getStorage = functoolsKit.memoize(([exchangeName, frameName, backtest]) => CREATE_KEY_FN$d(exchangeName, frameName, backtest), (exchangeName, frameName, backtest) => new HeatmapStorage(exchangeName, frameName, backtest));
21374
21503
  /**
21375
21504
  * Subscribes to signal emitter to receive tick events.
21376
21505
  * Protected against multiple subscriptions.
@@ -21565,7 +21694,7 @@ class HeatMarkdownService {
21565
21694
  payload,
21566
21695
  });
21567
21696
  if (payload) {
21568
- const key = CREATE_KEY_FN$b(payload.exchangeName, payload.frameName, payload.backtest);
21697
+ const key = CREATE_KEY_FN$d(payload.exchangeName, payload.frameName, payload.backtest);
21569
21698
  this.getStorage.clear(key);
21570
21699
  }
21571
21700
  else {
@@ -22596,7 +22725,7 @@ class ClientPartial {
22596
22725
  * @param backtest - Whether running in backtest mode
22597
22726
  * @returns Unique string key for memoization
22598
22727
  */
22599
- const CREATE_KEY_FN$a = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
22728
+ const CREATE_KEY_FN$c = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
22600
22729
  /**
22601
22730
  * Creates a callback function for emitting profit events to partialProfitSubject.
22602
22731
  *
@@ -22718,7 +22847,7 @@ class PartialConnectionService {
22718
22847
  * Key format: "signalId:backtest" or "signalId:live"
22719
22848
  * Value: ClientPartial instance with logger and event emitters
22720
22849
  */
22721
- this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$a(signalId, backtest), (signalId, backtest) => {
22850
+ this.getPartial = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$c(signalId, backtest), (signalId, backtest) => {
22722
22851
  return new ClientPartial({
22723
22852
  signalId,
22724
22853
  logger: this.loggerService,
@@ -22808,7 +22937,7 @@ class PartialConnectionService {
22808
22937
  const partial = this.getPartial(data.id, backtest);
22809
22938
  await partial.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
22810
22939
  await partial.clear(symbol, data, priceClose, backtest);
22811
- const key = CREATE_KEY_FN$a(data.id, backtest);
22940
+ const key = CREATE_KEY_FN$c(data.id, backtest);
22812
22941
  this.getPartial.clear(key);
22813
22942
  };
22814
22943
  }
@@ -22824,7 +22953,7 @@ class PartialConnectionService {
22824
22953
  * @param backtest - Whether running in backtest mode
22825
22954
  * @returns Unique string key for memoization
22826
22955
  */
22827
- const CREATE_KEY_FN$9 = (symbol, strategyName, exchangeName, frameName, backtest) => {
22956
+ const CREATE_KEY_FN$b = (symbol, strategyName, exchangeName, frameName, backtest) => {
22828
22957
  const parts = [symbol, strategyName, exchangeName];
22829
22958
  if (frameName)
22830
22959
  parts.push(frameName);
@@ -23049,7 +23178,7 @@ class PartialMarkdownService {
23049
23178
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
23050
23179
  * Each combination gets its own isolated storage instance.
23051
23180
  */
23052
- 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$4(symbol, strategyName, exchangeName, frameName));
23181
+ 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));
23053
23182
  /**
23054
23183
  * Subscribes to partial profit/loss signal emitters to receive events.
23055
23184
  * Protected against multiple subscriptions.
@@ -23259,7 +23388,7 @@ class PartialMarkdownService {
23259
23388
  payload,
23260
23389
  });
23261
23390
  if (payload) {
23262
- const key = CREATE_KEY_FN$9(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
23391
+ const key = CREATE_KEY_FN$b(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
23263
23392
  this.getStorage.clear(key);
23264
23393
  }
23265
23394
  else {
@@ -23275,7 +23404,7 @@ class PartialMarkdownService {
23275
23404
  * @param context - Context with strategyName, exchangeName, frameName
23276
23405
  * @returns Unique string key for memoization
23277
23406
  */
23278
- const CREATE_KEY_FN$8 = (context) => {
23407
+ const CREATE_KEY_FN$a = (context) => {
23279
23408
  const parts = [context.strategyName, context.exchangeName];
23280
23409
  if (context.frameName)
23281
23410
  parts.push(context.frameName);
@@ -23349,7 +23478,7 @@ class PartialGlobalService {
23349
23478
  * @param context - Context with strategyName, exchangeName and frameName
23350
23479
  * @param methodName - Name of the calling method for error tracking
23351
23480
  */
23352
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$8(context), (context, methodName) => {
23481
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$a(context), (context, methodName) => {
23353
23482
  this.loggerService.log("partialGlobalService validate", {
23354
23483
  context,
23355
23484
  methodName,
@@ -23804,7 +23933,7 @@ class ClientBreakeven {
23804
23933
  * @param backtest - Whether running in backtest mode
23805
23934
  * @returns Unique string key for memoization
23806
23935
  */
23807
- const CREATE_KEY_FN$7 = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
23936
+ const CREATE_KEY_FN$9 = (signalId, backtest) => `${signalId}:${backtest ? "backtest" : "live"}`;
23808
23937
  /**
23809
23938
  * Creates a callback function for emitting breakeven events to breakevenSubject.
23810
23939
  *
@@ -23890,7 +24019,7 @@ class BreakevenConnectionService {
23890
24019
  * Key format: "signalId:backtest" or "signalId:live"
23891
24020
  * Value: ClientBreakeven instance with logger and event emitter
23892
24021
  */
23893
- this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$7(signalId, backtest), (signalId, backtest) => {
24022
+ this.getBreakeven = functoolsKit.memoize(([signalId, backtest]) => CREATE_KEY_FN$9(signalId, backtest), (signalId, backtest) => {
23894
24023
  return new ClientBreakeven({
23895
24024
  signalId,
23896
24025
  logger: this.loggerService,
@@ -23951,7 +24080,7 @@ class BreakevenConnectionService {
23951
24080
  const breakeven = this.getBreakeven(data.id, backtest);
23952
24081
  await breakeven.waitForInit(symbol, data.strategyName, data.exchangeName, backtest);
23953
24082
  await breakeven.clear(symbol, data, priceClose, backtest);
23954
- const key = CREATE_KEY_FN$7(data.id, backtest);
24083
+ const key = CREATE_KEY_FN$9(data.id, backtest);
23955
24084
  this.getBreakeven.clear(key);
23956
24085
  };
23957
24086
  }
@@ -23967,7 +24096,7 @@ class BreakevenConnectionService {
23967
24096
  * @param backtest - Whether running in backtest mode
23968
24097
  * @returns Unique string key for memoization
23969
24098
  */
23970
- const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
24099
+ const CREATE_KEY_FN$8 = (symbol, strategyName, exchangeName, frameName, backtest) => {
23971
24100
  const parts = [symbol, strategyName, exchangeName];
23972
24101
  if (frameName)
23973
24102
  parts.push(frameName);
@@ -24144,7 +24273,7 @@ class BreakevenMarkdownService {
24144
24273
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
24145
24274
  * Each combination gets its own isolated storage instance.
24146
24275
  */
24147
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$3(symbol, strategyName, exchangeName, frameName));
24276
+ 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$3(symbol, strategyName, exchangeName, frameName));
24148
24277
  /**
24149
24278
  * Subscribes to breakeven signal emitter to receive events.
24150
24279
  * Protected against multiple subscriptions.
@@ -24333,7 +24462,7 @@ class BreakevenMarkdownService {
24333
24462
  payload,
24334
24463
  });
24335
24464
  if (payload) {
24336
- const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
24465
+ const key = CREATE_KEY_FN$8(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
24337
24466
  this.getStorage.clear(key);
24338
24467
  }
24339
24468
  else {
@@ -24349,7 +24478,7 @@ class BreakevenMarkdownService {
24349
24478
  * @param context - Context with strategyName, exchangeName, frameName
24350
24479
  * @returns Unique string key for memoization
24351
24480
  */
24352
- const CREATE_KEY_FN$5 = (context) => {
24481
+ const CREATE_KEY_FN$7 = (context) => {
24353
24482
  const parts = [context.strategyName, context.exchangeName];
24354
24483
  if (context.frameName)
24355
24484
  parts.push(context.frameName);
@@ -24423,7 +24552,7 @@ class BreakevenGlobalService {
24423
24552
  * @param context - Context with strategyName, exchangeName and frameName
24424
24553
  * @param methodName - Name of the calling method for error tracking
24425
24554
  */
24426
- this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$5(context), (context, methodName) => {
24555
+ this.validate = functoolsKit.memoize(([context]) => CREATE_KEY_FN$7(context), (context, methodName) => {
24427
24556
  this.loggerService.log("breakevenGlobalService validate", {
24428
24557
  context,
24429
24558
  methodName,
@@ -24643,7 +24772,7 @@ class ConfigValidationService {
24643
24772
  * @param backtest - Whether running in backtest mode
24644
24773
  * @returns Unique string key for memoization
24645
24774
  */
24646
- const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
24775
+ const CREATE_KEY_FN$6 = (symbol, strategyName, exchangeName, frameName, backtest) => {
24647
24776
  const parts = [symbol, strategyName, exchangeName];
24648
24777
  if (frameName)
24649
24778
  parts.push(frameName);
@@ -24812,7 +24941,7 @@ class RiskMarkdownService {
24812
24941
  * Memoized function to get or create ReportStorage for a symbol-strategy-exchange-frame-backtest combination.
24813
24942
  * Each combination gets its own isolated storage instance.
24814
24943
  */
24815
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$4(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName));
24944
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$6(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage$2(symbol, strategyName, exchangeName, frameName));
24816
24945
  /**
24817
24946
  * Subscribes to risk rejection emitter to receive rejection events.
24818
24947
  * Protected against multiple subscriptions.
@@ -25001,7 +25130,7 @@ class RiskMarkdownService {
25001
25130
  payload,
25002
25131
  });
25003
25132
  if (payload) {
25004
- const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25133
+ const key = CREATE_KEY_FN$6(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
25005
25134
  this.getStorage.clear(key);
25006
25135
  }
25007
25136
  else {
@@ -27689,7 +27818,7 @@ class SyncReportService {
27689
27818
  * @returns Colon-separated key string for memoization
27690
27819
  * @internal
27691
27820
  */
27692
- const CREATE_KEY_FN$3 = (symbol, strategyName, exchangeName, frameName, backtest) => {
27821
+ const CREATE_KEY_FN$5 = (symbol, strategyName, exchangeName, frameName, backtest) => {
27693
27822
  const parts = [symbol, strategyName, exchangeName];
27694
27823
  if (frameName)
27695
27824
  parts.push(frameName);
@@ -27937,7 +28066,7 @@ class StrategyMarkdownService {
27937
28066
  *
27938
28067
  * @internal
27939
28068
  */
27940
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$3(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
28069
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$5(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName) => new ReportStorage$1(symbol, strategyName, exchangeName, frameName));
27941
28070
  /**
27942
28071
  * Records a cancel-scheduled event when a scheduled signal is cancelled.
27943
28072
  *
@@ -28505,7 +28634,7 @@ class StrategyMarkdownService {
28505
28634
  this.clear = async (payload) => {
28506
28635
  this.loggerService.log("strategyMarkdownService clear", { payload });
28507
28636
  if (payload) {
28508
- const key = CREATE_KEY_FN$3(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28637
+ const key = CREATE_KEY_FN$5(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28509
28638
  this.getStorage.clear(key);
28510
28639
  }
28511
28640
  else {
@@ -28613,7 +28742,7 @@ class StrategyMarkdownService {
28613
28742
  * Creates a unique key for memoizing ReportStorage instances.
28614
28743
  * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
28615
28744
  */
28616
- const CREATE_KEY_FN$2 = (symbol, strategyName, exchangeName, frameName, backtest) => {
28745
+ const CREATE_KEY_FN$4 = (symbol, strategyName, exchangeName, frameName, backtest) => {
28617
28746
  const parts = [symbol, strategyName, exchangeName];
28618
28747
  if (frameName)
28619
28748
  parts.push(frameName);
@@ -28745,7 +28874,7 @@ class ReportStorage {
28745
28874
  class SyncMarkdownService {
28746
28875
  constructor() {
28747
28876
  this.loggerService = inject(TYPES.loggerService);
28748
- this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$2(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
28877
+ this.getStorage = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$4(symbol, strategyName, exchangeName, frameName, backtest), (symbol, strategyName, exchangeName, frameName, backtest) => new ReportStorage(symbol, strategyName, exchangeName, frameName));
28749
28878
  this.subscribe = functoolsKit.singleshot(() => {
28750
28879
  this.loggerService.log("syncMarkdownService init");
28751
28880
  const unsubscribe = syncSubject.subscribe(this.tick);
@@ -28820,7 +28949,7 @@ class SyncMarkdownService {
28820
28949
  this.clear = async (payload) => {
28821
28950
  this.loggerService.log("syncMarkdownService clear", { payload });
28822
28951
  if (payload) {
28823
- const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28952
+ const key = CREATE_KEY_FN$4(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
28824
28953
  this.getStorage.clear(key);
28825
28954
  }
28826
28955
  else {
@@ -28830,6 +28959,275 @@ class SyncMarkdownService {
28830
28959
  }
28831
28960
  }
28832
28961
 
28962
+ const LISTEN_TIMEOUT$1 = 120000;
28963
+ /**
28964
+ * Creates a unique memoization key for a price stream.
28965
+ * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
28966
+ *
28967
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
28968
+ * @param strategyName - Strategy identifier
28969
+ * @param exchangeName - Exchange identifier
28970
+ * @param frameName - Frame identifier (omitted when empty)
28971
+ * @param backtest - Whether running in backtest mode
28972
+ * @returns Unique string key for memoization
28973
+ */
28974
+ const CREATE_KEY_FN$3 = (symbol, strategyName, exchangeName, frameName, backtest) => {
28975
+ const parts = [symbol, strategyName, exchangeName];
28976
+ if (frameName)
28977
+ parts.push(frameName);
28978
+ parts.push(backtest ? "backtest" : "live");
28979
+ return parts.join(":");
28980
+ };
28981
+ /**
28982
+ * Service for tracking the latest market price per symbol-strategy-exchange-frame combination.
28983
+ *
28984
+ * Maintains a memoized BehaviorSubject per unique key that is updated on every strategy tick
28985
+ * by StrategyConnectionService. Consumers can synchronously read the last known price or
28986
+ * await the first value if none has arrived yet.
28987
+ *
28988
+ * Primary use case: providing the current price outside of a tick execution context,
28989
+ * e.g., when a command is triggered between ticks.
28990
+ *
28991
+ * Features:
28992
+ * - One BehaviorSubject per (symbol, strategyName, exchangeName, frameName, backtest) key
28993
+ * - Falls back to ExchangeConnectionService.getAveragePrice when called inside an execution context
28994
+ * - Waits up to LISTEN_TIMEOUT ms for the first price if none is cached yet
28995
+ * - clear() disposes the BehaviorSubject for a single key or all keys
28996
+ *
28997
+ * Architecture:
28998
+ * - Registered as singleton in DI container
28999
+ * - Updated by StrategyConnectionService after each tick
29000
+ * - Cleared by Backtest/Live/Walker at strategy start to prevent stale data
29001
+ *
29002
+ * @example
29003
+ * ```typescript
29004
+ * const price = await backtest.priceMetaService.getCurrentPrice("BTCUSDT", context, false);
29005
+ * ```
29006
+ */
29007
+ class PriceMetaService {
29008
+ constructor() {
29009
+ this.loggerService = inject(TYPES.loggerService);
29010
+ this.exchangeConnectionService = inject(TYPES.exchangeConnectionService);
29011
+ /**
29012
+ * Memoized factory for BehaviorSubject streams keyed by (symbol, strategyName, exchangeName, frameName, backtest).
29013
+ *
29014
+ * Each subject holds the latest currentPrice emitted by the strategy iterator for that key.
29015
+ * Instances are cached until clear() is called.
29016
+ */
29017
+ this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$3(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
29018
+ /**
29019
+ * Returns the current market price for the given symbol and context.
29020
+ *
29021
+ * When called inside an execution context (i.e., during a signal handler or action),
29022
+ * delegates to ExchangeConnectionService.getAveragePrice for the live exchange price.
29023
+ * Otherwise, reads the last value from the cached BehaviorSubject. If no value has
29024
+ * been emitted yet, waits up to LISTEN_TIMEOUT ms for the first tick before throwing.
29025
+ *
29026
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29027
+ * @param context - Strategy, exchange, and frame identifiers
29028
+ * @param backtest - True if backtest mode, false if live mode
29029
+ * @returns Current market price in quote currency
29030
+ * @throws When no price arrives within LISTEN_TIMEOUT ms
29031
+ */
29032
+ this.getCurrentPrice = async (symbol, context, backtest) => {
29033
+ this.loggerService.log("priceMetaService getCurrentPrice", {
29034
+ symbol,
29035
+ context,
29036
+ backtest,
29037
+ });
29038
+ if (ExecutionContextService.hasContext() &&
29039
+ MethodContextService.hasContext()) {
29040
+ return await this.exchangeConnectionService.getAveragePrice(symbol);
29041
+ }
29042
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29043
+ if (source.data) {
29044
+ return source.data;
29045
+ }
29046
+ console.warn(`PriceMetaService: No currentPrice available for ${CREATE_KEY_FN$3(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
29047
+ const currentPrice = await functoolsKit.waitForNext(source, (data) => !!data, LISTEN_TIMEOUT$1);
29048
+ if (typeof currentPrice === "symbol") {
29049
+ throw new Error(`PriceMetaService: Timeout while waiting for currentPrice for ${CREATE_KEY_FN$3(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
29050
+ }
29051
+ return currentPrice;
29052
+ };
29053
+ /**
29054
+ * Pushes a new price value into the BehaviorSubject for the given key.
29055
+ *
29056
+ * Called by StrategyConnectionService after each strategy tick to keep
29057
+ * the cached price up to date.
29058
+ *
29059
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29060
+ * @param currentPrice - The latest price from the tick
29061
+ * @param context - Strategy, exchange, and frame identifiers
29062
+ * @param backtest - True if backtest mode, false if live mode
29063
+ */
29064
+ this.next = async (symbol, currentPrice, context, backtest) => {
29065
+ this.loggerService.log("priceMetaService next", {
29066
+ symbol,
29067
+ currentPrice,
29068
+ context,
29069
+ backtest,
29070
+ });
29071
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29072
+ source.next(currentPrice);
29073
+ };
29074
+ /**
29075
+ * Disposes cached BehaviorSubject(s) to free memory and prevent stale data.
29076
+ *
29077
+ * When called without arguments, clears all memoized price streams.
29078
+ * When called with a payload, clears only the stream for the specified key.
29079
+ * Should be called at strategy start (Backtest/Live/Walker) to reset state.
29080
+ *
29081
+ * @param payload - Optional key to clear a single stream; omit to clear all
29082
+ */
29083
+ this.clear = (payload) => {
29084
+ this.loggerService.log("priceMetaService clear", {
29085
+ payload
29086
+ });
29087
+ if (!payload) {
29088
+ this.getSource.clear();
29089
+ return;
29090
+ }
29091
+ const key = CREATE_KEY_FN$3(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
29092
+ this.getSource.clear(key);
29093
+ };
29094
+ }
29095
+ }
29096
+
29097
+ const LISTEN_TIMEOUT = 120000;
29098
+ /**
29099
+ * Creates a unique memoization key for a timestamp stream.
29100
+ * Key format: "symbol:strategyName:exchangeName[:frameName]:backtest|live"
29101
+ *
29102
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29103
+ * @param strategyName - Strategy identifier
29104
+ * @param exchangeName - Exchange identifier
29105
+ * @param frameName - Frame identifier (omitted when empty)
29106
+ * @param backtest - Whether running in backtest mode
29107
+ * @returns Unique string key for memoization
29108
+ */
29109
+ const CREATE_KEY_FN$2 = (symbol, strategyName, exchangeName, frameName, backtest) => {
29110
+ const parts = [symbol, strategyName, exchangeName];
29111
+ if (frameName)
29112
+ parts.push(frameName);
29113
+ parts.push(backtest ? "backtest" : "live");
29114
+ return parts.join(":");
29115
+ };
29116
+ /**
29117
+ * Service for tracking the latest candle timestamp per symbol-strategy-exchange-frame combination.
29118
+ *
29119
+ * Maintains a memoized BehaviorSubject per unique key that is updated on every strategy tick
29120
+ * by StrategyConnectionService. Consumers can synchronously read the last known timestamp or
29121
+ * await the first value if none has arrived yet.
29122
+ *
29123
+ * Primary use case: providing the current candle time outside of a tick execution context,
29124
+ * e.g., when a command is triggered between ticks.
29125
+ *
29126
+ * Features:
29127
+ * - One BehaviorSubject per (symbol, strategyName, exchangeName, frameName, backtest) key
29128
+ * - Falls back to ExecutionContextService.context.when when called inside an execution context
29129
+ * - Waits up to LISTEN_TIMEOUT ms for the first timestamp if none is cached yet
29130
+ * - clear() disposes the BehaviorSubject for a single key or all keys
29131
+ *
29132
+ * Architecture:
29133
+ * - Registered as singleton in DI container
29134
+ * - Updated by StrategyConnectionService after each tick
29135
+ * - Cleared by Backtest/Live/Walker at strategy start to prevent stale data
29136
+ *
29137
+ * @example
29138
+ * ```typescript
29139
+ * const ts = await backtest.timeMetaService.getTimestamp("BTCUSDT", context, false);
29140
+ * ```
29141
+ */
29142
+ class TimeMetaService {
29143
+ constructor() {
29144
+ this.loggerService = inject(TYPES.loggerService);
29145
+ this.executionContextService = inject(TYPES.executionContextService);
29146
+ /**
29147
+ * Memoized factory for BehaviorSubject streams keyed by (symbol, strategyName, exchangeName, frameName, backtest).
29148
+ *
29149
+ * Each subject holds the latest createdAt timestamp emitted by the strategy iterator for that key.
29150
+ * Instances are cached until clear() is called.
29151
+ */
29152
+ this.getSource = functoolsKit.memoize(([symbol, strategyName, exchangeName, frameName, backtest]) => CREATE_KEY_FN$2(symbol, strategyName, exchangeName, frameName, backtest), () => new functoolsKit.BehaviorSubject());
29153
+ /**
29154
+ * Returns the current candle timestamp (in milliseconds) for the given symbol and context.
29155
+ *
29156
+ * When called inside an execution context (i.e., during a signal handler or action),
29157
+ * reads the timestamp directly from ExecutionContextService.context.when.
29158
+ * Otherwise, reads the last value from the cached BehaviorSubject. If no value has
29159
+ * been emitted yet, waits up to LISTEN_TIMEOUT ms for the first tick before throwing.
29160
+ *
29161
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29162
+ * @param context - Strategy, exchange, and frame identifiers
29163
+ * @param backtest - True if backtest mode, false if live mode
29164
+ * @returns Unix timestamp in milliseconds of the latest processed candle
29165
+ * @throws When no timestamp arrives within LISTEN_TIMEOUT ms
29166
+ */
29167
+ this.getTimestamp = async (symbol, context, backtest) => {
29168
+ this.loggerService.log("timeMetaService getTimestamp", {
29169
+ symbol,
29170
+ context,
29171
+ backtest,
29172
+ });
29173
+ if (ExecutionContextService.hasContext()) {
29174
+ return this.executionContextService.context.when.getTime();
29175
+ }
29176
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29177
+ if (source.data) {
29178
+ return source.data;
29179
+ }
29180
+ console.warn(`TimeMetaService: No timestamp available for ${CREATE_KEY_FN$2(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}. Trying to fetch from strategy iterator as a fallback...`);
29181
+ const timestamp = await functoolsKit.waitForNext(source, (data) => !!data, LISTEN_TIMEOUT);
29182
+ if (typeof timestamp === "symbol") {
29183
+ throw new Error(`TimeMetaService: Timeout while waiting for timestamp for ${CREATE_KEY_FN$2(symbol, context.strategyName, context.exchangeName, context.frameName, backtest)}`);
29184
+ }
29185
+ return timestamp;
29186
+ };
29187
+ /**
29188
+ * Pushes a new timestamp value into the BehaviorSubject for the given key.
29189
+ *
29190
+ * Called by StrategyConnectionService after each strategy tick to keep
29191
+ * the cached timestamp up to date.
29192
+ *
29193
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
29194
+ * @param timestamp - The createdAt timestamp from the tick (milliseconds)
29195
+ * @param context - Strategy, exchange, and frame identifiers
29196
+ * @param backtest - True if backtest mode, false if live mode
29197
+ */
29198
+ this.next = async (symbol, timestamp, context, backtest) => {
29199
+ this.loggerService.log("timeMetaService next", {
29200
+ symbol,
29201
+ timestamp,
29202
+ context,
29203
+ backtest,
29204
+ });
29205
+ const source = this.getSource(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
29206
+ source.next(timestamp);
29207
+ };
29208
+ /**
29209
+ * Disposes cached BehaviorSubject(s) to free memory and prevent stale data.
29210
+ *
29211
+ * When called without arguments, clears all memoized timestamp streams.
29212
+ * When called with a payload, clears only the stream for the specified key.
29213
+ * Should be called at strategy start (Backtest/Live/Walker) to reset state.
29214
+ *
29215
+ * @param payload - Optional key to clear a single stream; omit to clear all
29216
+ */
29217
+ this.clear = (payload) => {
29218
+ this.loggerService.log("timeMetaService clear", {
29219
+ payload,
29220
+ });
29221
+ if (!payload) {
29222
+ this.getSource.clear();
29223
+ return;
29224
+ }
29225
+ const key = CREATE_KEY_FN$2(payload.symbol, payload.strategyName, payload.exchangeName, payload.frameName, payload.backtest);
29226
+ this.getSource.clear(key);
29227
+ };
29228
+ }
29229
+ }
29230
+
28833
29231
  {
28834
29232
  provide(TYPES.loggerService, () => new LoggerService());
28835
29233
  }
@@ -28862,6 +29260,10 @@ class SyncMarkdownService {
28862
29260
  provide(TYPES.actionCoreService, () => new ActionCoreService());
28863
29261
  provide(TYPES.frameCoreService, () => new FrameCoreService());
28864
29262
  }
29263
+ {
29264
+ provide(TYPES.priceMetaService, () => new PriceMetaService());
29265
+ provide(TYPES.timeMetaService, () => new TimeMetaService());
29266
+ }
28865
29267
  {
28866
29268
  provide(TYPES.sizingGlobalService, () => new SizingGlobalService());
28867
29269
  provide(TYPES.riskGlobalService, () => new RiskGlobalService());
@@ -28953,6 +29355,10 @@ const coreServices = {
28953
29355
  actionCoreService: inject(TYPES.actionCoreService),
28954
29356
  frameCoreService: inject(TYPES.frameCoreService),
28955
29357
  };
29358
+ const metaServices = {
29359
+ timeMetaService: inject(TYPES.timeMetaService),
29360
+ priceMetaService: inject(TYPES.priceMetaService),
29361
+ };
28956
29362
  const globalServices = {
28957
29363
  sizingGlobalService: inject(TYPES.sizingGlobalService),
28958
29364
  riskGlobalService: inject(TYPES.riskGlobalService),
@@ -29017,6 +29423,7 @@ const backtest = {
29017
29423
  ...connectionServices,
29018
29424
  ...schemaServices,
29019
29425
  ...coreServices,
29426
+ ...metaServices,
29020
29427
  ...globalServices,
29021
29428
  ...commandServices,
29022
29429
  ...logicPrivateServices,
@@ -31781,6 +32188,11 @@ BrokerBase = functoolsKit.makeExtendable(BrokerBase);
31781
32188
  */
31782
32189
  const Broker = new BrokerAdapter();
31783
32190
 
32191
+ const POSITION_OVERLAP_LADDER_DEFAULT = {
32192
+ upperPercent: 1.5,
32193
+ lowerPercent: 1.5,
32194
+ };
32195
+
31784
32196
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
31785
32197
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
31786
32198
  const PARTIAL_PROFIT_METHOD_NAME = "strategy.commitPartialProfit";
@@ -31806,6 +32218,8 @@ const GET_POSITION_PNL_PERCENT_METHOD_NAME = "strategy.getPositionPnlPercent";
31806
32218
  const GET_POSITION_PNL_COST_METHOD_NAME = "strategy.getPositionPnlCost";
31807
32219
  const GET_POSITION_LEVELS_METHOD_NAME = "strategy.getPositionLevels";
31808
32220
  const GET_POSITION_PARTIALS_METHOD_NAME = "strategy.getPositionPartials";
32221
+ const GET_POSITION_ENTRY_OVERLAP_METHOD_NAME = "strategy.getPositionEntryOverlap";
32222
+ const GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME = "strategy.getPositionPartialOverlap";
31809
32223
  /**
31810
32224
  * Cancels the scheduled signal without stopping the strategy.
31811
32225
  *
@@ -32082,6 +32496,7 @@ async function commitTrailingStop(symbol, percentShift, currentPrice) {
32082
32496
  percentShift,
32083
32497
  currentPrice,
32084
32498
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
32499
+ takeProfitPrice: signal.priceTakeProfit,
32085
32500
  position: signal.position,
32086
32501
  context: { exchangeName, frameName, strategyName },
32087
32502
  backtest: isBacktest,
@@ -32161,6 +32576,7 @@ async function commitTrailingTake(symbol, percentShift, currentPrice) {
32161
32576
  percentShift,
32162
32577
  currentPrice,
32163
32578
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
32579
+ takeProfitPrice: signal.priceTakeProfit,
32164
32580
  position: signal.position,
32165
32581
  context: { exchangeName, frameName, strategyName },
32166
32582
  backtest: isBacktest,
@@ -32212,6 +32628,7 @@ async function commitTrailingStopCost(symbol, newStopLossPrice) {
32212
32628
  currentPrice,
32213
32629
  newStopLossPrice,
32214
32630
  position: signal.position,
32631
+ takeProfitPrice: signal.priceTakeProfit,
32215
32632
  context: { exchangeName, frameName, strategyName },
32216
32633
  backtest: isBacktest,
32217
32634
  });
@@ -32261,6 +32678,7 @@ async function commitTrailingTakeCost(symbol, newTakeProfitPrice) {
32261
32678
  percentShift,
32262
32679
  currentPrice,
32263
32680
  newTakeProfitPrice,
32681
+ takeProfitPrice: signal.priceTakeProfit,
32264
32682
  position: signal.position,
32265
32683
  context: { exchangeName, frameName, strategyName },
32266
32684
  backtest: isBacktest,
@@ -32587,6 +33005,31 @@ async function getBreakeven(symbol, currentPrice) {
32587
33005
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32588
33006
  return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
32589
33007
  }
33008
+ /**
33009
+ * Returns the effective (DCA-weighted) entry price for the current pending signal.
33010
+ *
33011
+ * Uses cost-weighted harmonic mean: Σcost / Σ(cost/price).
33012
+ * When partial closes exist, the price is computed iteratively using
33013
+ * costBasisAtClose snapshots from each partial, then blended with any
33014
+ * DCA entries added after the last partial.
33015
+ * With no DCA entries, equals the original priceOpen.
33016
+ *
33017
+ * Returns null if no pending signal exists.
33018
+ *
33019
+ * Automatically detects backtest/live mode from execution context.
33020
+ *
33021
+ * @param symbol - Trading pair symbol
33022
+ * @returns Promise resolving to effective entry price or null
33023
+ *
33024
+ * @example
33025
+ * ```typescript
33026
+ * import { getPositionAveragePrice } from "backtest-kit";
33027
+ *
33028
+ * const avgPrice = await getPositionAveragePrice("BTCUSDT");
33029
+ * // No DCA: avgPrice === priceOpen
33030
+ * // After DCA at lower price: avgPrice < priceOpen
33031
+ * ```
33032
+ */
32590
33033
  async function getPositionAveragePrice(symbol) {
32591
33034
  bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, {
32592
33035
  symbol,
@@ -32601,6 +33044,28 @@ async function getPositionAveragePrice(symbol) {
32601
33044
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32602
33045
  return await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
32603
33046
  }
33047
+ /**
33048
+ * Returns the number of DCA entries made for the current pending signal.
33049
+ *
33050
+ * 1 = original entry only (no DCA).
33051
+ * Increases by 1 with each successful commitAverageBuy().
33052
+ *
33053
+ * Returns null if no pending signal exists.
33054
+ *
33055
+ * Automatically detects backtest/live mode from execution context.
33056
+ *
33057
+ * @param symbol - Trading pair symbol
33058
+ * @returns Promise resolving to entry count or null
33059
+ *
33060
+ * @example
33061
+ * ```typescript
33062
+ * import { getPositionInvestedCount } from "backtest-kit";
33063
+ *
33064
+ * const count = await getPositionInvestedCount("BTCUSDT");
33065
+ * // No DCA: count === 1
33066
+ * // After one DCA: count === 2
33067
+ * ```
33068
+ */
32604
33069
  async function getPositionInvestedCount(symbol) {
32605
33070
  bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, {
32606
33071
  symbol,
@@ -32615,6 +33080,28 @@ async function getPositionInvestedCount(symbol) {
32615
33080
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32616
33081
  return await bt.strategyCoreService.getPositionInvestedCount(isBacktest, symbol, { exchangeName, frameName, strategyName });
32617
33082
  }
33083
+ /**
33084
+ * Returns the total invested cost basis in dollars for the current pending signal.
33085
+ *
33086
+ * Equal to the sum of all _entry costs (Σ entry.cost).
33087
+ * Each entry cost is set at the time of commitAverageBuy (defaults to CC_POSITION_ENTRY_COST).
33088
+ *
33089
+ * Returns null if no pending signal exists.
33090
+ *
33091
+ * Automatically detects backtest/live mode from execution context.
33092
+ *
33093
+ * @param symbol - Trading pair symbol
33094
+ * @returns Promise resolving to total invested cost in dollars or null
33095
+ *
33096
+ * @example
33097
+ * ```typescript
33098
+ * import { getPositionInvestedCost } from "backtest-kit";
33099
+ *
33100
+ * const cost = await getPositionInvestedCost("BTCUSDT");
33101
+ * // No DCA, default cost: cost === 100
33102
+ * // After one DCA with default cost: cost === 200
33103
+ * ```
33104
+ */
32618
33105
  async function getPositionInvestedCost(symbol) {
32619
33106
  bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, {
32620
33107
  symbol,
@@ -32629,6 +33116,29 @@ async function getPositionInvestedCost(symbol) {
32629
33116
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32630
33117
  return await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
32631
33118
  }
33119
+ /**
33120
+ * Returns the unrealized PNL percentage for the current pending signal at current market price.
33121
+ *
33122
+ * Accounts for partial closes, DCA entries, slippage and fees
33123
+ * (delegates to toProfitLossDto).
33124
+ *
33125
+ * Returns null if no pending signal exists.
33126
+ *
33127
+ * Automatically detects backtest/live mode from execution context.
33128
+ * Automatically fetches current price via getAveragePrice.
33129
+ *
33130
+ * @param symbol - Trading pair symbol
33131
+ * @returns Promise resolving to PNL percentage or null
33132
+ *
33133
+ * @example
33134
+ * ```typescript
33135
+ * import { getPositionPnlPercent } from "backtest-kit";
33136
+ *
33137
+ * const pnlPct = await getPositionPnlPercent("BTCUSDT");
33138
+ * // LONG at 100, current=105: pnlPct ≈ 5
33139
+ * // LONG at 100, current=95: pnlPct ≈ -5
33140
+ * ```
33141
+ */
32632
33142
  async function getPositionPnlPercent(symbol) {
32633
33143
  bt.loggerService.info(GET_POSITION_PNL_PERCENT_METHOD_NAME, { symbol });
32634
33144
  if (!ExecutionContextService.hasContext()) {
@@ -32778,6 +33288,29 @@ async function commitPartialLossCost(symbol, dollarAmount) {
32778
33288
  });
32779
33289
  return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
32780
33290
  }
33291
+ /**
33292
+ * Returns the unrealized PNL in dollars for the current pending signal at current market price.
33293
+ *
33294
+ * Calculated as: pnlPercentage / 100 × totalInvestedCost.
33295
+ * Accounts for partial closes, DCA entries, slippage and fees.
33296
+ *
33297
+ * Returns null if no pending signal exists.
33298
+ *
33299
+ * Automatically detects backtest/live mode from execution context.
33300
+ * Automatically fetches current price via getAveragePrice.
33301
+ *
33302
+ * @param symbol - Trading pair symbol
33303
+ * @returns Promise resolving to PNL in dollars or null
33304
+ *
33305
+ * @example
33306
+ * ```typescript
33307
+ * import { getPositionPnlCost } from "backtest-kit";
33308
+ *
33309
+ * const pnlCost = await getPositionPnlCost("BTCUSDT");
33310
+ * // LONG at 100, invested $100, current=105: pnlCost ≈ 5
33311
+ * // LONG at 100, invested $200 (DCA), current=95: pnlCost ≈ -10
33312
+ * ```
33313
+ */
32781
33314
  async function getPositionPnlCost(symbol) {
32782
33315
  bt.loggerService.info(GET_POSITION_PNL_COST_METHOD_NAME, { symbol });
32783
33316
  if (!ExecutionContextService.hasContext()) {
@@ -32864,6 +33397,104 @@ async function getPositionPartials(symbol) {
32864
33397
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
32865
33398
  return await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
32866
33399
  }
33400
+ /**
33401
+ * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
33402
+ * Use this to prevent duplicate DCA entries at the same price area.
33403
+ *
33404
+ * Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
33405
+ * where step = level * percent / 100.
33406
+ * Returns false if no pending signal exists.
33407
+ *
33408
+ * @param symbol - Trading pair symbol
33409
+ * @param currentPrice - Price to check against existing DCA levels
33410
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
33411
+ * @returns Promise<boolean> - true if price overlaps an existing entry level (DCA not recommended)
33412
+ *
33413
+ * @example
33414
+ * ```typescript
33415
+ * import { getPositionEntryOverlap } from "backtest-kit";
33416
+ *
33417
+ * // LONG with levels [43000, 42000], check if 42100 is too close to 42000
33418
+ * const overlap = await getPositionEntryOverlap("BTCUSDT", 42100, { upperPercent: 5, lowerPercent: 5 });
33419
+ * // overlap = true (42100 is within 5% of 42000 = [39900, 44100])
33420
+ * if (!overlap) {
33421
+ * await commitAverageBuy("BTCUSDT");
33422
+ * }
33423
+ * ```
33424
+ */
33425
+ async function getPositionEntryOverlap(symbol, currentPrice, ladder = POSITION_OVERLAP_LADDER_DEFAULT) {
33426
+ bt.loggerService.info(GET_POSITION_ENTRY_OVERLAP_METHOD_NAME, {
33427
+ symbol,
33428
+ currentPrice,
33429
+ ladder,
33430
+ });
33431
+ if (!ExecutionContextService.hasContext()) {
33432
+ throw new Error("getPositionEntryOverlap requires an execution context");
33433
+ }
33434
+ if (!MethodContextService.hasContext()) {
33435
+ throw new Error("getPositionEntryOverlap requires a method context");
33436
+ }
33437
+ const { backtest: isBacktest } = bt.executionContextService.context;
33438
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
33439
+ const levels = await bt.strategyCoreService.getPositionLevels(isBacktest, symbol, { exchangeName, frameName, strategyName });
33440
+ if (!levels) {
33441
+ return false;
33442
+ }
33443
+ return levels.some((level) => {
33444
+ const upperStep = (level * ladder.upperPercent) / 100;
33445
+ const lowerStep = (level * ladder.lowerPercent) / 100;
33446
+ return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
33447
+ });
33448
+ }
33449
+ /**
33450
+ * Checks whether the current price falls within the tolerance zone of any existing partial close price.
33451
+ * Use this to prevent duplicate partial closes at the same price area.
33452
+ *
33453
+ * Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
33454
+ * for any partial, where step = partial.currentPrice * percent / 100.
33455
+ * Returns false if no pending signal exists or no partials have been executed yet.
33456
+ *
33457
+ * @param symbol - Trading pair symbol
33458
+ * @param currentPrice - Price to check against existing partial close prices
33459
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
33460
+ * @returns Promise<boolean> - true if price overlaps an existing partial price (partial not recommended)
33461
+ *
33462
+ * @example
33463
+ * ```typescript
33464
+ * import { getPositionPartialOverlap } from "backtest-kit";
33465
+ *
33466
+ * // Partials at [45000], check if 45100 is too close
33467
+ * const overlap = await getPositionPartialOverlap("BTCUSDT", 45100, { upperPercent: 1.5, lowerPercent: 1.5 });
33468
+ * // overlap = true (45100 is within 1.5% of 45000)
33469
+ * if (!overlap) {
33470
+ * await commitPartialProfit("BTCUSDT", 50);
33471
+ * }
33472
+ * ```
33473
+ */
33474
+ async function getPositionPartialOverlap(symbol, currentPrice, ladder = POSITION_OVERLAP_LADDER_DEFAULT) {
33475
+ bt.loggerService.info(GET_POSITION_PARTIAL_OVERLAP_METHOD_NAME, {
33476
+ symbol,
33477
+ currentPrice,
33478
+ ladder,
33479
+ });
33480
+ if (!ExecutionContextService.hasContext()) {
33481
+ throw new Error("getPositionPartialOverlap requires an execution context");
33482
+ }
33483
+ if (!MethodContextService.hasContext()) {
33484
+ throw new Error("getPositionPartialOverlap requires a method context");
33485
+ }
33486
+ const { backtest: isBacktest } = bt.executionContextService.context;
33487
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
33488
+ const partials = await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
33489
+ if (!partials) {
33490
+ return false;
33491
+ }
33492
+ return partials.some((partial) => {
33493
+ const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
33494
+ const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
33495
+ return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
33496
+ });
33497
+ }
32867
33498
 
32868
33499
  const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
32869
33500
  /**
@@ -34090,6 +34721,8 @@ const BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST = "BacktestUtils.getPositionPnl
34090
34721
  const BACKTEST_METHOD_NAME_GET_POSITION_LEVELS = "BacktestUtils.getPositionLevels";
34091
34722
  const BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS = "BacktestUtils.getPositionPartials";
34092
34723
  const BACKTEST_METHOD_NAME_GET_POSITION_ENTRIES = "BacktestUtils.getPositionEntries";
34724
+ const BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "BacktestUtils.getPositionEntryOverlap";
34725
+ const BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "BacktestUtils.getPositionPartialOverlap";
34093
34726
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
34094
34727
  const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
34095
34728
  const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
@@ -34277,6 +34910,20 @@ class BacktestInstance {
34277
34910
  frameName: context.frameName,
34278
34911
  backtest: true,
34279
34912
  });
34913
+ bt.timeMetaService.clear({
34914
+ symbol,
34915
+ strategyName: context.strategyName,
34916
+ exchangeName: context.exchangeName,
34917
+ frameName: context.frameName,
34918
+ backtest: true,
34919
+ });
34920
+ bt.priceMetaService.clear({
34921
+ symbol,
34922
+ strategyName: context.strategyName,
34923
+ exchangeName: context.exchangeName,
34924
+ frameName: context.frameName,
34925
+ backtest: true,
34926
+ });
34280
34927
  }
34281
34928
  {
34282
34929
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
@@ -34880,6 +35527,90 @@ class BacktestUtils {
34880
35527
  }
34881
35528
  return await bt.strategyCoreService.getPositionEntries(true, symbol, context);
34882
35529
  };
35530
+ /**
35531
+ * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
35532
+ * Use this to prevent duplicate DCA entries at the same price area.
35533
+ *
35534
+ * Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
35535
+ * where step = level * percent / 100.
35536
+ * Returns false if no pending signal exists.
35537
+ *
35538
+ * @param symbol - Trading pair symbol
35539
+ * @param currentPrice - Price to check against existing DCA levels
35540
+ * @param context - Execution context with strategyName, exchangeName, and frameName
35541
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
35542
+ * @returns true if price overlaps an existing entry level (DCA not recommended)
35543
+ */
35544
+ this.getPositionEntryOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
35545
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP, {
35546
+ symbol,
35547
+ currentPrice,
35548
+ context,
35549
+ ladder,
35550
+ });
35551
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
35552
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
35553
+ {
35554
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35555
+ riskName &&
35556
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
35557
+ riskList &&
35558
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
35559
+ actions &&
35560
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
35561
+ }
35562
+ const levels = await bt.strategyCoreService.getPositionLevels(true, symbol, context);
35563
+ if (!levels) {
35564
+ return false;
35565
+ }
35566
+ return levels.some((level) => {
35567
+ const upperStep = (level * ladder.upperPercent) / 100;
35568
+ const lowerStep = (level * ladder.lowerPercent) / 100;
35569
+ return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
35570
+ });
35571
+ };
35572
+ /**
35573
+ * Checks whether the current price falls within the tolerance zone of any existing partial close price.
35574
+ * Use this to prevent duplicate partial closes at the same price area.
35575
+ *
35576
+ * Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
35577
+ * for any partial, where step = partial.currentPrice * percent / 100.
35578
+ * Returns false if no pending signal exists or no partials have been executed yet.
35579
+ *
35580
+ * @param symbol - Trading pair symbol
35581
+ * @param currentPrice - Price to check against existing partial close prices
35582
+ * @param context - Execution context with strategyName, exchangeName, and frameName
35583
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
35584
+ * @returns true if price overlaps an existing partial price (partial not recommended)
35585
+ */
35586
+ this.getPositionPartialOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
35587
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP, {
35588
+ symbol,
35589
+ currentPrice,
35590
+ context,
35591
+ ladder,
35592
+ });
35593
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
35594
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
35595
+ {
35596
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
35597
+ riskName &&
35598
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
35599
+ riskList &&
35600
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
35601
+ actions &&
35602
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
35603
+ }
35604
+ const partials = await bt.strategyCoreService.getPositionPartials(true, symbol, context);
35605
+ if (!partials) {
35606
+ return false;
35607
+ }
35608
+ return partials.some((partial) => {
35609
+ const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
35610
+ const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
35611
+ return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
35612
+ });
35613
+ };
34883
35614
  /**
34884
35615
  * Stops the strategy from generating new signals.
34885
35616
  *
@@ -35369,6 +36100,7 @@ class BacktestUtils {
35369
36100
  percentShift,
35370
36101
  currentPrice,
35371
36102
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
36103
+ takeProfitPrice: signal.priceTakeProfit,
35372
36104
  position: signal.position,
35373
36105
  context,
35374
36106
  backtest: true,
@@ -35453,6 +36185,7 @@ class BacktestUtils {
35453
36185
  percentShift,
35454
36186
  currentPrice,
35455
36187
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
36188
+ takeProfitPrice: signal.priceTakeProfit,
35456
36189
  position: signal.position,
35457
36190
  context,
35458
36191
  backtest: true,
@@ -35507,6 +36240,7 @@ class BacktestUtils {
35507
36240
  currentPrice,
35508
36241
  newStopLossPrice,
35509
36242
  position: signal.position,
36243
+ takeProfitPrice: signal.priceTakeProfit,
35510
36244
  context,
35511
36245
  backtest: true,
35512
36246
  });
@@ -35560,6 +36294,7 @@ class BacktestUtils {
35560
36294
  currentPrice,
35561
36295
  newTakeProfitPrice,
35562
36296
  position: signal.position,
36297
+ takeProfitPrice: signal.priceTakeProfit,
35563
36298
  context,
35564
36299
  backtest: true,
35565
36300
  });
@@ -35903,6 +36638,8 @@ const LIVE_METHOD_NAME_GET_POSITION_PNL_COST = "LiveUtils.getPositionPnlCost";
35903
36638
  const LIVE_METHOD_NAME_GET_POSITION_LEVELS = "LiveUtils.getPositionLevels";
35904
36639
  const LIVE_METHOD_NAME_GET_POSITION_PARTIALS = "LiveUtils.getPositionPartials";
35905
36640
  const LIVE_METHOD_NAME_GET_POSITION_ENTRIES = "LiveUtils.getPositionEntries";
36641
+ const LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP = "LiveUtils.getPositionEntryOverlap";
36642
+ const LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP = "LiveUtils.getPositionPartialOverlap";
35906
36643
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
35907
36644
  const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
35908
36645
  const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
@@ -36091,6 +36828,20 @@ class LiveInstance {
36091
36828
  frameName: "",
36092
36829
  backtest: false,
36093
36830
  });
36831
+ bt.timeMetaService.clear({
36832
+ symbol,
36833
+ strategyName: context.strategyName,
36834
+ exchangeName: context.exchangeName,
36835
+ frameName: "",
36836
+ backtest: false,
36837
+ });
36838
+ bt.priceMetaService.clear({
36839
+ symbol,
36840
+ strategyName: context.strategyName,
36841
+ exchangeName: context.exchangeName,
36842
+ frameName: "",
36843
+ backtest: false,
36844
+ });
36094
36845
  }
36095
36846
  {
36096
36847
  const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
@@ -36756,6 +37507,98 @@ class LiveUtils {
36756
37507
  frameName: "",
36757
37508
  });
36758
37509
  };
37510
+ /**
37511
+ * Checks whether the current price falls within the tolerance zone of any existing DCA entry level.
37512
+ * Use this to prevent duplicate DCA entries at the same price area.
37513
+ *
37514
+ * Returns true if currentPrice is within [level - lowerStep, level + upperStep] for any level,
37515
+ * where step = level * percent / 100.
37516
+ * Returns false if no pending signal exists.
37517
+ *
37518
+ * @param symbol - Trading pair symbol
37519
+ * @param currentPrice - Price to check against existing DCA levels
37520
+ * @param context - Execution context with strategyName and exchangeName
37521
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
37522
+ * @returns true if price overlaps an existing entry level (DCA not recommended)
37523
+ */
37524
+ this.getPositionEntryOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
37525
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP, {
37526
+ symbol,
37527
+ currentPrice,
37528
+ context,
37529
+ ladder,
37530
+ });
37531
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
37532
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
37533
+ {
37534
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
37535
+ riskName &&
37536
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP);
37537
+ riskList &&
37538
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
37539
+ actions &&
37540
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_ENTRY_OVERLAP));
37541
+ }
37542
+ const levels = await bt.strategyCoreService.getPositionLevels(false, symbol, {
37543
+ strategyName: context.strategyName,
37544
+ exchangeName: context.exchangeName,
37545
+ frameName: "",
37546
+ });
37547
+ if (!levels) {
37548
+ return false;
37549
+ }
37550
+ return levels.some((level) => {
37551
+ const upperStep = (level * ladder.upperPercent) / 100;
37552
+ const lowerStep = (level * ladder.lowerPercent) / 100;
37553
+ return currentPrice >= level - lowerStep && currentPrice <= level + upperStep;
37554
+ });
37555
+ };
37556
+ /**
37557
+ * Checks whether the current price falls within the tolerance zone of any existing partial close price.
37558
+ * Use this to prevent duplicate partial closes at the same price area.
37559
+ *
37560
+ * Returns true if currentPrice is within [partial.currentPrice - lowerStep, partial.currentPrice + upperStep]
37561
+ * for any partial, where step = partial.currentPrice * percent / 100.
37562
+ * Returns false if no pending signal exists or no partials have been executed yet.
37563
+ *
37564
+ * @param symbol - Trading pair symbol
37565
+ * @param currentPrice - Price to check against existing partial close prices
37566
+ * @param context - Execution context with strategyName and exchangeName
37567
+ * @param ladder - Tolerance zone config; percentages in 0–100 format (default: 1.5% up and down)
37568
+ * @returns true if price overlaps an existing partial price (partial not recommended)
37569
+ */
37570
+ this.getPositionPartialOverlap = async (symbol, currentPrice, context, ladder = POSITION_OVERLAP_LADDER_DEFAULT) => {
37571
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP, {
37572
+ symbol,
37573
+ currentPrice,
37574
+ context,
37575
+ ladder,
37576
+ });
37577
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
37578
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
37579
+ {
37580
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
37581
+ riskName &&
37582
+ bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP);
37583
+ riskList &&
37584
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
37585
+ actions &&
37586
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIAL_OVERLAP));
37587
+ }
37588
+ const partials = await bt.strategyCoreService.getPositionPartials(false, symbol, {
37589
+ strategyName: context.strategyName,
37590
+ exchangeName: context.exchangeName,
37591
+ frameName: "",
37592
+ });
37593
+ if (!partials) {
37594
+ return false;
37595
+ }
37596
+ return partials.some((partial) => {
37597
+ const upperStep = (partial.currentPrice * ladder.upperPercent) / 100;
37598
+ const lowerStep = (partial.currentPrice * ladder.lowerPercent) / 100;
37599
+ return currentPrice >= partial.currentPrice - lowerStep && currentPrice <= partial.currentPrice + upperStep;
37600
+ });
37601
+ };
36759
37602
  /**
36760
37603
  * Stops the strategy from generating new signals.
36761
37604
  *
@@ -37338,6 +38181,7 @@ class LiveUtils {
37338
38181
  percentShift,
37339
38182
  currentPrice,
37340
38183
  newStopLossPrice: slPercentShiftToPrice(percentShift, signal.priceStopLoss, effectivePriceOpen, signal.position),
38184
+ takeProfitPrice: signal.priceTakeProfit,
37341
38185
  position: signal.position,
37342
38186
  context,
37343
38187
  backtest: false,
@@ -37437,6 +38281,7 @@ class LiveUtils {
37437
38281
  percentShift,
37438
38282
  currentPrice,
37439
38283
  newTakeProfitPrice: tpPercentShiftToPrice(percentShift, signal.priceTakeProfit, effectivePriceOpen, signal.position),
38284
+ takeProfitPrice: signal.priceTakeProfit,
37440
38285
  position: signal.position,
37441
38286
  context,
37442
38287
  backtest: false,
@@ -37506,6 +38351,7 @@ class LiveUtils {
37506
38351
  percentShift,
37507
38352
  currentPrice,
37508
38353
  newStopLossPrice,
38354
+ takeProfitPrice: signal.priceTakeProfit,
37509
38355
  position: signal.position,
37510
38356
  context,
37511
38357
  backtest: false,
@@ -37575,6 +38421,7 @@ class LiveUtils {
37575
38421
  percentShift,
37576
38422
  currentPrice,
37577
38423
  newTakeProfitPrice,
38424
+ takeProfitPrice: signal.priceTakeProfit,
37578
38425
  position: signal.position,
37579
38426
  context,
37580
38427
  backtest: false,
@@ -40143,6 +40990,20 @@ class WalkerInstance {
40143
40990
  frameName: walkerSchema.frameName,
40144
40991
  backtest: true,
40145
40992
  });
40993
+ bt.timeMetaService.clear({
40994
+ symbol,
40995
+ strategyName,
40996
+ exchangeName: walkerSchema.exchangeName,
40997
+ frameName: walkerSchema.frameName,
40998
+ backtest: true,
40999
+ });
41000
+ bt.priceMetaService.clear({
41001
+ symbol,
41002
+ strategyName,
41003
+ exchangeName: walkerSchema.exchangeName,
41004
+ frameName: walkerSchema.frameName,
41005
+ backtest: true,
41006
+ });
40146
41007
  }
40147
41008
  {
40148
41009
  const { riskName, riskList, actions } = bt.strategySchemaService.get(strategyName);
@@ -43110,13 +43971,15 @@ class NotificationMemoryBacktestUtils {
43110
43971
  * Handles signal sync events (signal-open, signal-close).
43111
43972
  * @param data - The signal sync contract data
43112
43973
  */
43113
- this.handleSync = async (data) => {
43974
+ this.handleSync = functoolsKit.trycatch(async (data) => {
43114
43975
  bt.loggerService.info(NOTIFICATION_MEMORY_BACKTEST_METHOD_NAME_HANDLE_SYNC, {
43115
43976
  signalId: data.signalId,
43116
43977
  action: data.action,
43117
43978
  });
43118
43979
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
43119
- };
43980
+ }, {
43981
+ defaultValue: null,
43982
+ });
43120
43983
  /**
43121
43984
  * Handles risk rejection event.
43122
43985
  * @param data - The risk contract data
@@ -43225,8 +44088,10 @@ class NotificationDummyBacktestUtils {
43225
44088
  /**
43226
44089
  * No-op handler for signal sync event.
43227
44090
  */
43228
- this.handleSync = async () => {
43229
- };
44091
+ this.handleSync = functoolsKit.trycatch(async () => {
44092
+ }, {
44093
+ defaultValue: null,
44094
+ });
43230
44095
  /**
43231
44096
  * No-op handler for risk rejection event.
43232
44097
  */
@@ -43365,7 +44230,7 @@ class NotificationPersistBacktestUtils {
43365
44230
  * Handles signal sync events (signal-open, signal-close).
43366
44231
  * @param data - The signal sync contract data
43367
44232
  */
43368
- this.handleSync = async (data) => {
44233
+ this.handleSync = functoolsKit.trycatch(async (data) => {
43369
44234
  bt.loggerService.info(NOTIFICATION_PERSIST_BACKTEST_METHOD_NAME_HANDLE_SYNC, {
43370
44235
  signalId: data.signalId,
43371
44236
  action: data.action,
@@ -43373,7 +44238,9 @@ class NotificationPersistBacktestUtils {
43373
44238
  await this.waitForInit();
43374
44239
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
43375
44240
  await this._updateNotifications();
43376
- };
44241
+ }, {
44242
+ defaultValue: null,
44243
+ });
43377
44244
  /**
43378
44245
  * Handles risk rejection event.
43379
44246
  * @param data - The risk contract data
@@ -43556,13 +44423,15 @@ class NotificationMemoryLiveUtils {
43556
44423
  * Handles signal sync events (signal-open, signal-close).
43557
44424
  * @param data - The signal sync contract data
43558
44425
  */
43559
- this.handleSync = async (data) => {
44426
+ this.handleSync = functoolsKit.trycatch(async (data) => {
43560
44427
  bt.loggerService.info(NOTIFICATION_MEMORY_LIVE_METHOD_NAME_HANDLE_SYNC, {
43561
44428
  signalId: data.signalId,
43562
44429
  action: data.action,
43563
44430
  });
43564
44431
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
43565
- };
44432
+ }, {
44433
+ defaultValue: null,
44434
+ });
43566
44435
  /**
43567
44436
  * Handles risk rejection event.
43568
44437
  * @param data - The risk contract data
@@ -43671,8 +44540,10 @@ class NotificationDummyLiveUtils {
43671
44540
  /**
43672
44541
  * No-op handler for signal sync event.
43673
44542
  */
43674
- this.handleSync = async () => {
43675
- };
44543
+ this.handleSync = functoolsKit.trycatch(async () => {
44544
+ }, {
44545
+ defaultValue: null,
44546
+ });
43676
44547
  /**
43677
44548
  * No-op handler for risk rejection event.
43678
44549
  */
@@ -43812,7 +44683,7 @@ class NotificationPersistLiveUtils {
43812
44683
  * Handles signal sync events (signal-open, signal-close).
43813
44684
  * @param data - The signal sync contract data
43814
44685
  */
43815
- this.handleSync = async (data) => {
44686
+ this.handleSync = functoolsKit.trycatch(async (data) => {
43816
44687
  bt.loggerService.info(NOTIFICATION_PERSIST_LIVE_METHOD_NAME_HANDLE_SYNC, {
43817
44688
  signalId: data.signalId,
43818
44689
  action: data.action,
@@ -43820,7 +44691,9 @@ class NotificationPersistLiveUtils {
43820
44691
  await this.waitForInit();
43821
44692
  this._addNotification(CREATE_SIGNAL_SYNC_NOTIFICATION_FN(data));
43822
44693
  await this._updateNotifications();
43823
- };
44694
+ }, {
44695
+ defaultValue: null,
44696
+ });
43824
44697
  /**
43825
44698
  * Handles risk rejection event.
43826
44699
  * @param data - The risk contract data
@@ -43982,9 +44855,11 @@ class NotificationBacktestAdapter {
43982
44855
  * Proxies call to the underlying notification adapter.
43983
44856
  * @param data - The signal sync contract data
43984
44857
  */
43985
- this.handleSync = async (data) => {
44858
+ this.handleSync = functoolsKit.trycatch(async (data) => {
43986
44859
  return await this._notificationBacktestUtils.handleSync(data);
43987
- };
44860
+ }, {
44861
+ defaultValue: null,
44862
+ });
43988
44863
  /**
43989
44864
  * Handles risk rejection event.
43990
44865
  * Proxies call to the underlying notification adapter.
@@ -44126,9 +45001,11 @@ class NotificationLiveAdapter {
44126
45001
  * Proxies call to the underlying notification adapter.
44127
45002
  * @param data - The signal sync contract data
44128
45003
  */
44129
- this.handleSync = async (data) => {
45004
+ this.handleSync = functoolsKit.trycatch(async (data) => {
44130
45005
  return await this._notificationLiveUtils.handleSync(data);
44131
- };
45006
+ }, {
45007
+ defaultValue: null,
45008
+ });
44132
45009
  /**
44133
45010
  * Handles risk rejection event.
44134
45011
  * Proxies call to the underlying notification adapter.
@@ -45575,9 +46452,11 @@ exports.getNextCandles = getNextCandles;
45575
46452
  exports.getOrderBook = getOrderBook;
45576
46453
  exports.getPendingSignal = getPendingSignal;
45577
46454
  exports.getPositionAveragePrice = getPositionAveragePrice;
46455
+ exports.getPositionEntryOverlap = getPositionEntryOverlap;
45578
46456
  exports.getPositionInvestedCost = getPositionInvestedCost;
45579
46457
  exports.getPositionInvestedCount = getPositionInvestedCount;
45580
46458
  exports.getPositionLevels = getPositionLevels;
46459
+ exports.getPositionPartialOverlap = getPositionPartialOverlap;
45581
46460
  exports.getPositionPartials = getPositionPartials;
45582
46461
  exports.getPositionPnlCost = getPositionPnlCost;
45583
46462
  exports.getPositionPnlPercent = getPositionPnlPercent;