backtest-kit 3.7.1 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -3318,8 +3318,8 @@ const toProfitLossDto = (signal, priceClose) => {
3318
3318
  ? partial.effectivePrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3319
3319
  : partial.effectivePrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3320
3320
  const priceCloseWithSlippage = signal.position === "long"
3321
- ? partial.price * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3322
- : partial.price * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3321
+ ? partial.currentPrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3322
+ : partial.currentPrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3323
3323
  const partialPnl = signal.position === "long"
3324
3324
  ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
3325
3325
  : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
@@ -3804,8 +3804,8 @@ const PROCESS_COMMIT_QUEUE_FN = async (self, timestamp) => {
3804
3804
  const TO_PUBLIC_SIGNAL = (signal) => {
3805
3805
  const hasTrailingSL = "_trailingPriceStopLoss" in signal && signal._trailingPriceStopLoss !== undefined;
3806
3806
  const hasTrailingTP = "_trailingPriceTakeProfit" in signal && signal._trailingPriceTakeProfit !== undefined;
3807
- const partialExecuted = ("_partial" in signal && Array.isArray(signal._partial))
3808
- ? signal._partial.reduce((sum, partial) => sum + partial.percent, 0)
3807
+ const partialExecuted = "_partial" in signal
3808
+ ? getTotalClosed(signal).totalClosedPercent
3809
3809
  : 0;
3810
3810
  const totalEntries = ("_entry" in signal && Array.isArray(signal._entry))
3811
3811
  ? signal._entry.length
@@ -4288,7 +4288,7 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4288
4288
  type: "profit",
4289
4289
  percent: percentToClose,
4290
4290
  entryCountAtClose,
4291
- price: currentPrice,
4291
+ currentPrice,
4292
4292
  debugTimestamp: getDebugTimestamp(),
4293
4293
  effectivePrice,
4294
4294
  });
@@ -4327,7 +4327,7 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
4327
4327
  signal._partial.push({
4328
4328
  type: "loss",
4329
4329
  percent: percentToClose,
4330
- price: currentPrice,
4330
+ currentPrice,
4331
4331
  entryCountAtClose,
4332
4332
  effectivePrice,
4333
4333
  debugTimestamp: getDebugTimestamp(),
@@ -6270,6 +6270,137 @@ class ClientStrategy {
6270
6270
  const { remainingCostBasis } = getTotalClosed(this._pendingSignal);
6271
6271
  return remainingCostBasis;
6272
6272
  }
6273
+ /**
6274
+ * Returns the effective (DCA-averaged) entry price for the current pending signal.
6275
+ *
6276
+ * This is the harmonic mean of all _entry prices, which is the correct
6277
+ * cost-basis price used in all PNL calculations.
6278
+ * With no DCA entries, equals the original priceOpen.
6279
+ *
6280
+ * Returns null if no pending signal exists.
6281
+ *
6282
+ * @param symbol - Trading pair symbol
6283
+ * @returns Promise resolving to effective entry price or null
6284
+ */
6285
+ async getPositionAveragePrice(symbol) {
6286
+ this.params.logger.debug("ClientStrategy getPositionAveragePrice", { symbol });
6287
+ if (!this._pendingSignal) {
6288
+ return null;
6289
+ }
6290
+ return getEffectivePriceOpen(this._pendingSignal);
6291
+ }
6292
+ /**
6293
+ * Returns the number of DCA entries made for the current pending signal.
6294
+ *
6295
+ * 1 = original entry only (no DCA).
6296
+ * Increases by 1 with each successful commitAverageBuy().
6297
+ *
6298
+ * Returns null if no pending signal exists.
6299
+ *
6300
+ * @param symbol - Trading pair symbol
6301
+ * @returns Promise resolving to entry count or null
6302
+ */
6303
+ async getPositionInvestedCount(symbol) {
6304
+ this.params.logger.debug("ClientStrategy getPositionInvestedCount", { symbol });
6305
+ if (!this._pendingSignal) {
6306
+ return null;
6307
+ }
6308
+ return this._pendingSignal._entry?.length ?? 1;
6309
+ }
6310
+ /**
6311
+ * Returns the total invested cost basis in dollars for the current pending signal.
6312
+ *
6313
+ * Equal to entryCount × $100 (COST_BASIS_PER_ENTRY).
6314
+ * 1 entry = $100, 2 entries = $200, etc.
6315
+ *
6316
+ * Returns null if no pending signal exists.
6317
+ *
6318
+ * @param symbol - Trading pair symbol
6319
+ * @returns Promise resolving to total invested cost in dollars or null
6320
+ */
6321
+ async getPositionInvestedCost(symbol) {
6322
+ this.params.logger.debug("ClientStrategy getPositionInvestedCost", { symbol });
6323
+ if (!this._pendingSignal) {
6324
+ return null;
6325
+ }
6326
+ return (this._pendingSignal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
6327
+ }
6328
+ /**
6329
+ * Returns the unrealized PNL percentage for the current pending signal at currentPrice.
6330
+ *
6331
+ * Accounts for partial closes, DCA entries, slippage and fees
6332
+ * (delegates to toProfitLossDto).
6333
+ *
6334
+ * Returns null if no pending signal exists.
6335
+ *
6336
+ * @param symbol - Trading pair symbol
6337
+ * @param currentPrice - Current market price
6338
+ * @returns Promise resolving to pnlPercentage or null
6339
+ */
6340
+ async getPositionPnlPercent(symbol, currentPrice) {
6341
+ this.params.logger.debug("ClientStrategy getPositionPnlPercent", { symbol, currentPrice });
6342
+ if (!this._pendingSignal) {
6343
+ return null;
6344
+ }
6345
+ const pnl = toProfitLossDto(this._pendingSignal, currentPrice);
6346
+ return pnl.pnlPercentage;
6347
+ }
6348
+ /**
6349
+ * Returns the unrealized PNL in dollars for the current pending signal at currentPrice.
6350
+ *
6351
+ * Calculated as: pnlPercentage / 100 × totalInvestedCost
6352
+ * Accounts for partial closes, DCA entries, slippage and fees.
6353
+ *
6354
+ * Returns null if no pending signal exists.
6355
+ *
6356
+ * @param symbol - Trading pair symbol
6357
+ * @param currentPrice - Current market price
6358
+ * @returns Promise resolving to pnl in dollars or null
6359
+ */
6360
+ async getPositionPnlCost(symbol, currentPrice) {
6361
+ this.params.logger.debug("ClientStrategy getPositionPnlCost", { symbol, currentPrice });
6362
+ if (!this._pendingSignal) {
6363
+ return null;
6364
+ }
6365
+ const totalInvested = (this._pendingSignal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
6366
+ const pnl = toProfitLossDto(this._pendingSignal, currentPrice);
6367
+ return (pnl.pnlPercentage / 100) * totalInvested;
6368
+ }
6369
+ /**
6370
+ * Returns the list of DCA entry prices for the current pending signal.
6371
+ *
6372
+ * The first element is always the original priceOpen (initial entry).
6373
+ * Each subsequent element is a price added by commitAverageBuy().
6374
+ *
6375
+ * Returns null if no pending signal exists.
6376
+ * Returns a single-element array [priceOpen] if no DCA entries were made.
6377
+ *
6378
+ * @param symbol - Trading pair symbol
6379
+ * @returns Promise resolving to array of entry prices or null
6380
+ *
6381
+ * @example
6382
+ * // No DCA: [43000]
6383
+ * // One DCA: [43000, 42000]
6384
+ * // Two DCA: [43000, 42000, 41500]
6385
+ */
6386
+ async getPositionLevels(symbol) {
6387
+ this.params.logger.debug("ClientStrategy getPositionLevels", { symbol });
6388
+ if (!this._pendingSignal) {
6389
+ return null;
6390
+ }
6391
+ const entries = this._pendingSignal._entry;
6392
+ if (!entries || entries.length === 0) {
6393
+ return [this._pendingSignal.priceOpen];
6394
+ }
6395
+ return entries.map((e) => e.price);
6396
+ }
6397
+ async getPositionPartials(symbol) {
6398
+ this.params.logger.debug("ClientStrategy getPositionPartials", { symbol });
6399
+ if (!this._pendingSignal) {
6400
+ return null;
6401
+ }
6402
+ return this._pendingSignal._partial ?? [];
6403
+ }
6273
6404
  /**
6274
6405
  * Performs a single tick of strategy execution.
6275
6406
  *
@@ -8445,6 +8576,71 @@ class StrategyConnectionService {
8445
8576
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8446
8577
  return await strategy.getTotalCostClosed(symbol);
8447
8578
  };
8579
+ this.getPositionAveragePrice = async (backtest, symbol, context) => {
8580
+ this.loggerService.log("strategyConnectionService getPositionAveragePrice", {
8581
+ symbol,
8582
+ context,
8583
+ backtest,
8584
+ });
8585
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8586
+ return await strategy.getPositionAveragePrice(symbol);
8587
+ };
8588
+ this.getPositionInvestedCount = async (backtest, symbol, context) => {
8589
+ this.loggerService.log("strategyConnectionService getPositionInvestedCount", {
8590
+ symbol,
8591
+ context,
8592
+ backtest,
8593
+ });
8594
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8595
+ return await strategy.getPositionInvestedCount(symbol);
8596
+ };
8597
+ this.getPositionInvestedCost = async (backtest, symbol, context) => {
8598
+ this.loggerService.log("strategyConnectionService getPositionInvestedCost", {
8599
+ symbol,
8600
+ context,
8601
+ backtest,
8602
+ });
8603
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8604
+ return await strategy.getPositionInvestedCost(symbol);
8605
+ };
8606
+ this.getPositionPnlPercent = async (backtest, symbol, currentPrice, context) => {
8607
+ this.loggerService.log("strategyConnectionService getPositionPnlPercent", {
8608
+ symbol,
8609
+ currentPrice,
8610
+ context,
8611
+ backtest,
8612
+ });
8613
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8614
+ return await strategy.getPositionPnlPercent(symbol, currentPrice);
8615
+ };
8616
+ this.getPositionPnlCost = async (backtest, symbol, currentPrice, context) => {
8617
+ this.loggerService.log("strategyConnectionService getPositionPnlCost", {
8618
+ symbol,
8619
+ currentPrice,
8620
+ context,
8621
+ backtest,
8622
+ });
8623
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8624
+ return await strategy.getPositionPnlCost(symbol, currentPrice);
8625
+ };
8626
+ this.getPositionLevels = async (backtest, symbol, context) => {
8627
+ this.loggerService.log("strategyConnectionService getPositionLevels", {
8628
+ symbol,
8629
+ context,
8630
+ backtest,
8631
+ });
8632
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8633
+ return await strategy.getPositionLevels(symbol);
8634
+ };
8635
+ this.getPositionPartials = async (backtest, symbol, context) => {
8636
+ this.loggerService.log("strategyConnectionService getPositionPartials", {
8637
+ symbol,
8638
+ context,
8639
+ backtest,
8640
+ });
8641
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8642
+ return await strategy.getPositionPartials(symbol);
8643
+ };
8448
8644
  /**
8449
8645
  * Retrieves the currently active scheduled signal for the strategy.
8450
8646
  * If no scheduled signal exists, returns null.
@@ -11883,6 +12079,64 @@ class StrategyCoreService {
11883
12079
  await this.validate(context);
11884
12080
  return await this.strategyConnectionService.getTotalCostClosed(backtest, symbol, context);
11885
12081
  };
12082
+ this.getPositionAveragePrice = async (backtest, symbol, context) => {
12083
+ this.loggerService.log("strategyCoreService getPositionAveragePrice", {
12084
+ symbol,
12085
+ context,
12086
+ });
12087
+ await this.validate(context);
12088
+ return await this.strategyConnectionService.getPositionAveragePrice(backtest, symbol, context);
12089
+ };
12090
+ this.getPositionInvestedCount = async (backtest, symbol, context) => {
12091
+ this.loggerService.log("strategyCoreService getPositionInvestedCount", {
12092
+ symbol,
12093
+ context,
12094
+ });
12095
+ await this.validate(context);
12096
+ return await this.strategyConnectionService.getPositionInvestedCount(backtest, symbol, context);
12097
+ };
12098
+ this.getPositionInvestedCost = async (backtest, symbol, context) => {
12099
+ this.loggerService.log("strategyCoreService getPositionInvestedCost", {
12100
+ symbol,
12101
+ context,
12102
+ });
12103
+ await this.validate(context);
12104
+ return await this.strategyConnectionService.getPositionInvestedCost(backtest, symbol, context);
12105
+ };
12106
+ this.getPositionPnlPercent = async (backtest, symbol, currentPrice, context) => {
12107
+ this.loggerService.log("strategyCoreService getPositionPnlPercent", {
12108
+ symbol,
12109
+ currentPrice,
12110
+ context,
12111
+ });
12112
+ await this.validate(context);
12113
+ return await this.strategyConnectionService.getPositionPnlPercent(backtest, symbol, currentPrice, context);
12114
+ };
12115
+ this.getPositionPnlCost = async (backtest, symbol, currentPrice, context) => {
12116
+ this.loggerService.log("strategyCoreService getPositionPnlCost", {
12117
+ symbol,
12118
+ currentPrice,
12119
+ context,
12120
+ });
12121
+ await this.validate(context);
12122
+ return await this.strategyConnectionService.getPositionPnlCost(backtest, symbol, currentPrice, context);
12123
+ };
12124
+ this.getPositionLevels = async (backtest, symbol, context) => {
12125
+ this.loggerService.log("strategyCoreService getPositionLevels", {
12126
+ symbol,
12127
+ context,
12128
+ });
12129
+ await this.validate(context);
12130
+ return await this.strategyConnectionService.getPositionLevels(backtest, symbol, context);
12131
+ };
12132
+ this.getPositionPartials = async (backtest, symbol, context) => {
12133
+ this.loggerService.log("strategyCoreService getPositionPartials", {
12134
+ symbol,
12135
+ context,
12136
+ });
12137
+ await this.validate(context);
12138
+ return await this.strategyConnectionService.getPositionPartials(backtest, symbol, context);
12139
+ };
11886
12140
  /**
11887
12141
  * Retrieves the currently active scheduled signal for the symbol.
11888
12142
  * If no scheduled signal exists, returns null.
@@ -28905,10 +29159,28 @@ async function getAggregatedTrades(symbol, limit) {
28905
29159
  return await bt.exchangeConnectionService.getAggregatedTrades(symbol, limit);
28906
29160
  }
28907
29161
 
29162
+ /**
29163
+ * Convert an absolute dollar amount to a percentage of the invested position cost.
29164
+ * Use the result as the `percent` argument to `commitPartialProfit` / `commitPartialLoss`.
29165
+ *
29166
+ * @param dollarAmount - Dollar value to close (e.g. 150)
29167
+ * @param investedCost - Total invested cost from `getPositionInvestedCost` (e.g. 300)
29168
+ * @returns Percentage of the position to close (0–100)
29169
+ *
29170
+ * @example
29171
+ * const percent = investedCostToPercent(150, 300); // 50
29172
+ * await commitPartialProfit("BTCUSDT", percent);
29173
+ */
29174
+ const investedCostToPercent = (dollarAmount, investedCost) => {
29175
+ return (dollarAmount / investedCost) * 100;
29176
+ };
29177
+
28908
29178
  const CANCEL_SCHEDULED_METHOD_NAME = "strategy.commitCancelScheduled";
28909
29179
  const CLOSE_PENDING_METHOD_NAME = "strategy.commitClosePending";
28910
29180
  const PARTIAL_PROFIT_METHOD_NAME = "strategy.commitPartialProfit";
28911
29181
  const PARTIAL_LOSS_METHOD_NAME = "strategy.commitPartialLoss";
29182
+ const PARTIAL_PROFIT_COST_METHOD_NAME = "strategy.commitPartialProfitCost";
29183
+ const PARTIAL_LOSS_COST_METHOD_NAME = "strategy.commitPartialLossCost";
28912
29184
  const TRAILING_STOP_METHOD_NAME = "strategy.commitTrailingStop";
28913
29185
  const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
28914
29186
  const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
@@ -28919,6 +29191,13 @@ const GET_TOTAL_COST_CLOSED_METHOD_NAME = "strategy.getTotalCostClosed";
28919
29191
  const GET_PENDING_SIGNAL_METHOD_NAME = "strategy.getPendingSignal";
28920
29192
  const GET_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.getScheduledSignal";
28921
29193
  const GET_BREAKEVEN_METHOD_NAME = "strategy.getBreakeven";
29194
+ const GET_POSITION_AVERAGE_PRICE_METHOD_NAME = "strategy.getPositionAveragePrice";
29195
+ const GET_POSITION_INVESTED_COUNT_METHOD_NAME = "strategy.getPositionInvestedCount";
29196
+ const GET_POSITION_INVESTED_COST_METHOD_NAME = "strategy.getPositionInvestedCost";
29197
+ const GET_POSITION_PNL_PERCENT_METHOD_NAME = "strategy.getPositionPnlPercent";
29198
+ const GET_POSITION_PNL_COST_METHOD_NAME = "strategy.getPositionPnlCost";
29199
+ const GET_POSITION_LEVELS_METHOD_NAME = "strategy.getPositionLevels";
29200
+ const GET_POSITION_PARTIALS_METHOD_NAME = "strategy.getPositionPartials";
28922
29201
  /**
28923
29202
  * Cancels the scheduled signal without stopping the strategy.
28924
29203
  *
@@ -29477,6 +29756,205 @@ async function getBreakeven(symbol, currentPrice) {
29477
29756
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29478
29757
  return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
29479
29758
  }
29759
+ async function getPositionAveragePrice(symbol) {
29760
+ bt.loggerService.info(GET_POSITION_AVERAGE_PRICE_METHOD_NAME, { symbol });
29761
+ if (!ExecutionContextService.hasContext()) {
29762
+ throw new Error("getPositionAveragePrice requires an execution context");
29763
+ }
29764
+ if (!MethodContextService.hasContext()) {
29765
+ throw new Error("getPositionAveragePrice requires a method context");
29766
+ }
29767
+ const { backtest: isBacktest } = bt.executionContextService.context;
29768
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29769
+ return await bt.strategyCoreService.getPositionAveragePrice(isBacktest, symbol, { exchangeName, frameName, strategyName });
29770
+ }
29771
+ async function getPositionInvestedCount(symbol) {
29772
+ bt.loggerService.info(GET_POSITION_INVESTED_COUNT_METHOD_NAME, { symbol });
29773
+ if (!ExecutionContextService.hasContext()) {
29774
+ throw new Error("getPositionInvestedCount requires an execution context");
29775
+ }
29776
+ if (!MethodContextService.hasContext()) {
29777
+ throw new Error("getPositionInvestedCount requires a method context");
29778
+ }
29779
+ const { backtest: isBacktest } = bt.executionContextService.context;
29780
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29781
+ return await bt.strategyCoreService.getPositionInvestedCount(isBacktest, symbol, { exchangeName, frameName, strategyName });
29782
+ }
29783
+ async function getPositionInvestedCost(symbol) {
29784
+ bt.loggerService.info(GET_POSITION_INVESTED_COST_METHOD_NAME, { symbol });
29785
+ if (!ExecutionContextService.hasContext()) {
29786
+ throw new Error("getPositionInvestedCost requires an execution context");
29787
+ }
29788
+ if (!MethodContextService.hasContext()) {
29789
+ throw new Error("getPositionInvestedCost requires a method context");
29790
+ }
29791
+ const { backtest: isBacktest } = bt.executionContextService.context;
29792
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29793
+ return await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
29794
+ }
29795
+ async function getPositionPnlPercent(symbol) {
29796
+ bt.loggerService.info(GET_POSITION_PNL_PERCENT_METHOD_NAME, { symbol });
29797
+ if (!ExecutionContextService.hasContext()) {
29798
+ throw new Error("getPositionPnlPercent requires an execution context");
29799
+ }
29800
+ if (!MethodContextService.hasContext()) {
29801
+ throw new Error("getPositionPnlPercent requires a method context");
29802
+ }
29803
+ const currentPrice = await getAveragePrice(symbol);
29804
+ const { backtest: isBacktest } = bt.executionContextService.context;
29805
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29806
+ return await bt.strategyCoreService.getPositionPnlPercent(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
29807
+ }
29808
+ /**
29809
+ * Executes partial close at profit level by absolute dollar amount (moving toward TP).
29810
+ *
29811
+ * Convenience wrapper around commitPartialProfit that converts a dollar amount
29812
+ * to a percentage of the invested position cost automatically.
29813
+ * Price must be moving toward take profit (in profit direction).
29814
+ *
29815
+ * Automatically detects backtest/live mode from execution context.
29816
+ * Automatically fetches current price via getAveragePrice.
29817
+ *
29818
+ * @param symbol - Trading pair symbol
29819
+ * @param dollarAmount - Dollar value of position to close (e.g. 150 closes $150 worth)
29820
+ * @returns Promise<boolean> - true if partial close executed, false if skipped or no position
29821
+ *
29822
+ * @throws Error if currentPrice is not in profit direction:
29823
+ * - LONG: currentPrice must be > priceOpen
29824
+ * - SHORT: currentPrice must be < priceOpen
29825
+ *
29826
+ * @example
29827
+ * ```typescript
29828
+ * import { commitPartialProfitCost } from "backtest-kit";
29829
+ *
29830
+ * // Close $150 of a $300 position (50%) at profit
29831
+ * const success = await commitPartialProfitCost("BTCUSDT", 150);
29832
+ * if (success) {
29833
+ * console.log('Partial profit executed');
29834
+ * }
29835
+ * ```
29836
+ */
29837
+ async function commitPartialProfitCost(symbol, dollarAmount) {
29838
+ bt.loggerService.info(PARTIAL_PROFIT_COST_METHOD_NAME, { symbol, dollarAmount });
29839
+ if (!ExecutionContextService.hasContext()) {
29840
+ throw new Error("commitPartialProfitCost requires an execution context");
29841
+ }
29842
+ if (!MethodContextService.hasContext()) {
29843
+ throw new Error("commitPartialProfitCost requires a method context");
29844
+ }
29845
+ const currentPrice = await getAveragePrice(symbol);
29846
+ const { backtest: isBacktest } = bt.executionContextService.context;
29847
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29848
+ const investedCost = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
29849
+ if (investedCost === null)
29850
+ return false;
29851
+ const percentToClose = investedCostToPercent(dollarAmount, investedCost);
29852
+ return await bt.strategyCoreService.partialProfit(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
29853
+ }
29854
+ /**
29855
+ * Executes partial close at loss level by absolute dollar amount (moving toward SL).
29856
+ *
29857
+ * Convenience wrapper around commitPartialLoss that converts a dollar amount
29858
+ * to a percentage of the invested position cost automatically.
29859
+ * Price must be moving toward stop loss (in loss direction).
29860
+ *
29861
+ * Automatically detects backtest/live mode from execution context.
29862
+ * Automatically fetches current price via getAveragePrice.
29863
+ *
29864
+ * @param symbol - Trading pair symbol
29865
+ * @param dollarAmount - Dollar value of position to close (e.g. 100 closes $100 worth)
29866
+ * @returns Promise<boolean> - true if partial close executed, false if skipped or no position
29867
+ *
29868
+ * @throws Error if currentPrice is not in loss direction:
29869
+ * - LONG: currentPrice must be < priceOpen
29870
+ * - SHORT: currentPrice must be > priceOpen
29871
+ *
29872
+ * @example
29873
+ * ```typescript
29874
+ * import { commitPartialLossCost } from "backtest-kit";
29875
+ *
29876
+ * // Close $100 of a $300 position (~33%) at loss
29877
+ * const success = await commitPartialLossCost("BTCUSDT", 100);
29878
+ * if (success) {
29879
+ * console.log('Partial loss executed');
29880
+ * }
29881
+ * ```
29882
+ */
29883
+ async function commitPartialLossCost(symbol, dollarAmount) {
29884
+ bt.loggerService.info(PARTIAL_LOSS_COST_METHOD_NAME, { symbol, dollarAmount });
29885
+ if (!ExecutionContextService.hasContext()) {
29886
+ throw new Error("commitPartialLossCost requires an execution context");
29887
+ }
29888
+ if (!MethodContextService.hasContext()) {
29889
+ throw new Error("commitPartialLossCost requires a method context");
29890
+ }
29891
+ const currentPrice = await getAveragePrice(symbol);
29892
+ const { backtest: isBacktest } = bt.executionContextService.context;
29893
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29894
+ const investedCost = await bt.strategyCoreService.getPositionInvestedCost(isBacktest, symbol, { exchangeName, frameName, strategyName });
29895
+ if (investedCost === null)
29896
+ return false;
29897
+ const percentToClose = investedCostToPercent(dollarAmount, investedCost);
29898
+ return await bt.strategyCoreService.partialLoss(isBacktest, symbol, percentToClose, currentPrice, { exchangeName, frameName, strategyName });
29899
+ }
29900
+ async function getPositionPnlCost(symbol) {
29901
+ bt.loggerService.info(GET_POSITION_PNL_COST_METHOD_NAME, { symbol });
29902
+ if (!ExecutionContextService.hasContext()) {
29903
+ throw new Error("getPositionPnlCost requires an execution context");
29904
+ }
29905
+ if (!MethodContextService.hasContext()) {
29906
+ throw new Error("getPositionPnlCost requires a method context");
29907
+ }
29908
+ const currentPrice = await getAveragePrice(symbol);
29909
+ const { backtest: isBacktest } = bt.executionContextService.context;
29910
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29911
+ return await bt.strategyCoreService.getPositionPnlCost(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
29912
+ }
29913
+ /**
29914
+ * Returns the list of DCA entry prices for the current pending signal.
29915
+ *
29916
+ * The first element is always the original priceOpen (initial entry).
29917
+ * Each subsequent element is a price added by commitAverageBuy().
29918
+ *
29919
+ * Returns null if no pending signal exists.
29920
+ * Returns a single-element array [priceOpen] if no DCA entries were made.
29921
+ *
29922
+ * @param symbol - Trading pair symbol
29923
+ * @returns Promise resolving to array of entry prices or null
29924
+ *
29925
+ * @example
29926
+ * ```typescript
29927
+ * import { getPositionLevels } from "backtest-kit";
29928
+ *
29929
+ * const levels = await getPositionLevels("BTCUSDT");
29930
+ * // No DCA: [43000]
29931
+ * // One DCA: [43000, 42000]
29932
+ * ```
29933
+ */
29934
+ async function getPositionLevels(symbol) {
29935
+ bt.loggerService.info(GET_POSITION_LEVELS_METHOD_NAME, { symbol });
29936
+ if (!ExecutionContextService.hasContext()) {
29937
+ throw new Error("getPositionLevels requires an execution context");
29938
+ }
29939
+ if (!MethodContextService.hasContext()) {
29940
+ throw new Error("getPositionLevels requires a method context");
29941
+ }
29942
+ const { backtest: isBacktest } = bt.executionContextService.context;
29943
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29944
+ return await bt.strategyCoreService.getPositionLevels(isBacktest, symbol, { exchangeName, frameName, strategyName });
29945
+ }
29946
+ async function getPositionPartials(symbol) {
29947
+ bt.loggerService.info(GET_POSITION_PARTIALS_METHOD_NAME, { symbol });
29948
+ if (!ExecutionContextService.hasContext()) {
29949
+ throw new Error("getPositionPartials requires an execution context");
29950
+ }
29951
+ if (!MethodContextService.hasContext()) {
29952
+ throw new Error("getPositionPartials requires a method context");
29953
+ }
29954
+ const { backtest: isBacktest } = bt.executionContextService.context;
29955
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29956
+ return await bt.strategyCoreService.getPositionPartials(isBacktest, symbol, { exchangeName, frameName, strategyName });
29957
+ }
29480
29958
 
29481
29959
  const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
29482
29960
  /**
@@ -30668,11 +31146,20 @@ const BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "BacktestUtils.getTotalPer
30668
31146
  const BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED = "BacktestUtils.getTotalCostClosed";
30669
31147
  const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
30670
31148
  const BACKTEST_METHOD_NAME_GET_BREAKEVEN = "BacktestUtils.getBreakeven";
31149
+ const BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE = "BacktestUtils.getPositionAveragePrice";
31150
+ const BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT = "BacktestUtils.getPositionInvestedCount";
31151
+ const BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST = "BacktestUtils.getPositionInvestedCost";
31152
+ const BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT = "BacktestUtils.getPositionPnlPercent";
31153
+ const BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST = "BacktestUtils.getPositionPnlCost";
31154
+ const BACKTEST_METHOD_NAME_GET_POSITION_LEVELS = "BacktestUtils.getPositionLevels";
31155
+ const BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS = "BacktestUtils.getPositionPartials";
30671
31156
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
30672
31157
  const BACKTEST_METHOD_NAME_CANCEL_SCHEDULED = "Backtest.commitCancelScheduled";
30673
31158
  const BACKTEST_METHOD_NAME_CLOSE_PENDING = "Backtest.commitClosePending";
30674
31159
  const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.commitPartialProfit";
30675
31160
  const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.commitPartialLoss";
31161
+ const BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST = "BacktestUtils.commitPartialProfitCost";
31162
+ const BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST = "BacktestUtils.commitPartialLossCost";
30676
31163
  const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.commitTrailingStop";
30677
31164
  const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.commitTrailingTake";
30678
31165
  const BACKTEST_METHOD_NAME_ACTIVATE_SCHEDULED = "Backtest.commitActivateScheduled";
@@ -31209,6 +31696,134 @@ class BacktestUtils {
31209
31696
  }
31210
31697
  return await bt.strategyCoreService.getBreakeven(true, symbol, currentPrice, context);
31211
31698
  };
31699
+ this.getPositionAveragePrice = async (symbol, context) => {
31700
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, {
31701
+ symbol,
31702
+ context,
31703
+ });
31704
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
31705
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
31706
+ {
31707
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31708
+ riskName &&
31709
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
31710
+ riskList &&
31711
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
31712
+ actions &&
31713
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
31714
+ }
31715
+ return await bt.strategyCoreService.getPositionAveragePrice(true, symbol, context);
31716
+ };
31717
+ this.getPositionInvestedCount = async (symbol, context) => {
31718
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT, {
31719
+ symbol,
31720
+ context,
31721
+ });
31722
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
31723
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
31724
+ {
31725
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31726
+ riskName &&
31727
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
31728
+ riskList &&
31729
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
31730
+ actions &&
31731
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
31732
+ }
31733
+ return await bt.strategyCoreService.getPositionInvestedCount(true, symbol, context);
31734
+ };
31735
+ this.getPositionInvestedCost = async (symbol, context) => {
31736
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST, {
31737
+ symbol,
31738
+ context,
31739
+ });
31740
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST);
31741
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST);
31742
+ {
31743
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31744
+ riskName &&
31745
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST);
31746
+ riskList &&
31747
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST));
31748
+ actions &&
31749
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_INVESTED_COST));
31750
+ }
31751
+ return await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
31752
+ };
31753
+ this.getPositionPnlPercent = async (symbol, currentPrice, context) => {
31754
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT, {
31755
+ symbol,
31756
+ currentPrice,
31757
+ context,
31758
+ });
31759
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT);
31760
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT);
31761
+ {
31762
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31763
+ riskName &&
31764
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT);
31765
+ riskList &&
31766
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT));
31767
+ actions &&
31768
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_PERCENT));
31769
+ }
31770
+ return await bt.strategyCoreService.getPositionPnlPercent(true, symbol, currentPrice, context);
31771
+ };
31772
+ this.getPositionPnlCost = async (symbol, currentPrice, context) => {
31773
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST, {
31774
+ symbol,
31775
+ currentPrice,
31776
+ context,
31777
+ });
31778
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST);
31779
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST);
31780
+ {
31781
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31782
+ riskName &&
31783
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST);
31784
+ riskList &&
31785
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST));
31786
+ actions &&
31787
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PNL_COST));
31788
+ }
31789
+ return await bt.strategyCoreService.getPositionPnlCost(true, symbol, currentPrice, context);
31790
+ };
31791
+ this.getPositionLevels = async (symbol, context) => {
31792
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_LEVELS, {
31793
+ symbol,
31794
+ context,
31795
+ });
31796
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS);
31797
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS);
31798
+ {
31799
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31800
+ riskName &&
31801
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS);
31802
+ riskList &&
31803
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS));
31804
+ actions &&
31805
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_LEVELS));
31806
+ }
31807
+ return await bt.strategyCoreService.getPositionLevels(true, symbol, context);
31808
+ };
31809
+ this.getPositionPartials = async (symbol, context) => {
31810
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS, {
31811
+ symbol,
31812
+ context,
31813
+ });
31814
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS);
31815
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS);
31816
+ {
31817
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31818
+ riskName &&
31819
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS);
31820
+ riskList &&
31821
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS));
31822
+ actions &&
31823
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_POSITION_PARTIALS));
31824
+ }
31825
+ return await bt.strategyCoreService.getPositionPartials(true, symbol, context);
31826
+ };
31212
31827
  /**
31213
31828
  * Stops the strategy from generating new signals.
31214
31829
  *
@@ -31430,6 +32045,114 @@ class BacktestUtils {
31430
32045
  }
31431
32046
  return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
31432
32047
  };
32048
+ /**
32049
+ * Executes partial close at profit level by absolute dollar amount (moving toward TP).
32050
+ *
32051
+ * Convenience wrapper around commitPartialProfit that converts a dollar amount
32052
+ * to a percentage of the invested position cost automatically.
32053
+ * Price must be moving toward take profit (in profit direction).
32054
+ *
32055
+ * @param symbol - Trading pair symbol
32056
+ * @param dollarAmount - Dollar value of position to close (e.g. 150 closes $150 worth)
32057
+ * @param currentPrice - Current market price for this partial close
32058
+ * @param context - Execution context with strategyName, exchangeName, and frameName
32059
+ * @returns Promise<boolean> - true if partial close executed, false if skipped or no position
32060
+ *
32061
+ * @throws Error if currentPrice is not in profit direction:
32062
+ * - LONG: currentPrice must be > priceOpen
32063
+ * - SHORT: currentPrice must be < priceOpen
32064
+ *
32065
+ * @example
32066
+ * ```typescript
32067
+ * // Close $150 of a $300 position (50%) at profit
32068
+ * const success = await Backtest.commitPartialProfitCost("BTCUSDT", 150, 45000, {
32069
+ * exchangeName: "binance",
32070
+ * frameName: "frame1",
32071
+ * strategyName: "my-strategy"
32072
+ * });
32073
+ * if (success) {
32074
+ * console.log('Partial profit executed');
32075
+ * }
32076
+ * ```
32077
+ */
32078
+ this.commitPartialProfitCost = async (symbol, dollarAmount, currentPrice, context) => {
32079
+ bt.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST, {
32080
+ symbol,
32081
+ dollarAmount,
32082
+ currentPrice,
32083
+ context,
32084
+ });
32085
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST);
32086
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST);
32087
+ {
32088
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
32089
+ riskName &&
32090
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST);
32091
+ riskList &&
32092
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST));
32093
+ actions &&
32094
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT_COST));
32095
+ }
32096
+ const investedCost = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
32097
+ if (investedCost === null)
32098
+ return false;
32099
+ const percentToClose = (dollarAmount / investedCost) * 100;
32100
+ return await bt.strategyCoreService.partialProfit(true, symbol, percentToClose, currentPrice, context);
32101
+ };
32102
+ /**
32103
+ * Executes partial close at loss level by absolute dollar amount (moving toward SL).
32104
+ *
32105
+ * Convenience wrapper around commitPartialLoss that converts a dollar amount
32106
+ * to a percentage of the invested position cost automatically.
32107
+ * Price must be moving toward stop loss (in loss direction).
32108
+ *
32109
+ * @param symbol - Trading pair symbol
32110
+ * @param dollarAmount - Dollar value of position to close (e.g. 100 closes $100 worth)
32111
+ * @param currentPrice - Current market price for this partial close
32112
+ * @param context - Execution context with strategyName, exchangeName, and frameName
32113
+ * @returns Promise<boolean> - true if partial close executed, false if skipped or no position
32114
+ *
32115
+ * @throws Error if currentPrice is not in loss direction:
32116
+ * - LONG: currentPrice must be < priceOpen
32117
+ * - SHORT: currentPrice must be > priceOpen
32118
+ *
32119
+ * @example
32120
+ * ```typescript
32121
+ * // Close $100 of a $300 position (~33%) at loss
32122
+ * const success = await Backtest.commitPartialLossCost("BTCUSDT", 100, 38000, {
32123
+ * exchangeName: "binance",
32124
+ * frameName: "frame1",
32125
+ * strategyName: "my-strategy"
32126
+ * });
32127
+ * if (success) {
32128
+ * console.log('Partial loss executed');
32129
+ * }
32130
+ * ```
32131
+ */
32132
+ this.commitPartialLossCost = async (symbol, dollarAmount, currentPrice, context) => {
32133
+ bt.loggerService.info(BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST, {
32134
+ symbol,
32135
+ dollarAmount,
32136
+ currentPrice,
32137
+ context,
32138
+ });
32139
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST);
32140
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST);
32141
+ {
32142
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
32143
+ riskName &&
32144
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST);
32145
+ riskList &&
32146
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST));
32147
+ actions &&
32148
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_PARTIAL_LOSS_COST));
32149
+ }
32150
+ const investedCost = await bt.strategyCoreService.getPositionInvestedCost(true, symbol, context);
32151
+ if (investedCost === null)
32152
+ return false;
32153
+ const percentToClose = (dollarAmount / investedCost) * 100;
32154
+ return await bt.strategyCoreService.partialLoss(true, symbol, percentToClose, currentPrice, context);
32155
+ };
31433
32156
  /**
31434
32157
  * Adjusts the trailing stop-loss distance for an active pending signal.
31435
32158
  *
@@ -31851,11 +32574,20 @@ const LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "LiveUtils.getTotalPercentClos
31851
32574
  const LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED = "LiveUtils.getTotalCostClosed";
31852
32575
  const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
31853
32576
  const LIVE_METHOD_NAME_GET_BREAKEVEN = "LiveUtils.getBreakeven";
32577
+ const LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE = "LiveUtils.getPositionAveragePrice";
32578
+ const LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT = "LiveUtils.getPositionInvestedCount";
32579
+ const LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST = "LiveUtils.getPositionInvestedCost";
32580
+ const LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT = "LiveUtils.getPositionPnlPercent";
32581
+ const LIVE_METHOD_NAME_GET_POSITION_PNL_COST = "LiveUtils.getPositionPnlCost";
32582
+ const LIVE_METHOD_NAME_GET_POSITION_LEVELS = "LiveUtils.getPositionLevels";
32583
+ const LIVE_METHOD_NAME_GET_POSITION_PARTIALS = "LiveUtils.getPositionPartials";
31854
32584
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
31855
32585
  const LIVE_METHOD_NAME_CANCEL_SCHEDULED = "Live.cancelScheduled";
31856
32586
  const LIVE_METHOD_NAME_CLOSE_PENDING = "Live.closePending";
31857
32587
  const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.commitPartialProfit";
31858
32588
  const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.commitPartialLoss";
32589
+ const LIVE_METHOD_NAME_PARTIAL_PROFIT_COST = "LiveUtils.commitPartialProfitCost";
32590
+ const LIVE_METHOD_NAME_PARTIAL_LOSS_COST = "LiveUtils.commitPartialLossCost";
31859
32591
  const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.commitTrailingStop";
31860
32592
  const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.commitTrailingTake";
31861
32593
  const LIVE_METHOD_NAME_ACTIVATE_SCHEDULED = "Live.commitActivateScheduled";
@@ -32364,6 +33096,118 @@ class LiveUtils {
32364
33096
  frameName: "",
32365
33097
  });
32366
33098
  };
33099
+ this.getPositionAveragePrice = async (symbol, context) => {
33100
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE, { symbol, context });
33101
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
33102
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
33103
+ {
33104
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33105
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE);
33106
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
33107
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_AVERAGE_PRICE));
33108
+ }
33109
+ return await bt.strategyCoreService.getPositionAveragePrice(false, symbol, {
33110
+ strategyName: context.strategyName,
33111
+ exchangeName: context.exchangeName,
33112
+ frameName: "",
33113
+ });
33114
+ };
33115
+ this.getPositionInvestedCount = async (symbol, context) => {
33116
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT, { symbol, context });
33117
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
33118
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
33119
+ {
33120
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33121
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT);
33122
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
33123
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COUNT));
33124
+ }
33125
+ return await bt.strategyCoreService.getPositionInvestedCount(false, symbol, {
33126
+ strategyName: context.strategyName,
33127
+ exchangeName: context.exchangeName,
33128
+ frameName: "",
33129
+ });
33130
+ };
33131
+ this.getPositionInvestedCost = async (symbol, context) => {
33132
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST, { symbol, context });
33133
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
33134
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
33135
+ {
33136
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33137
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST);
33138
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
33139
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_INVESTED_COST));
33140
+ }
33141
+ return await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
33142
+ strategyName: context.strategyName,
33143
+ exchangeName: context.exchangeName,
33144
+ frameName: "",
33145
+ });
33146
+ };
33147
+ this.getPositionPnlPercent = async (symbol, currentPrice, context) => {
33148
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT, { symbol, currentPrice, context });
33149
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
33150
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
33151
+ {
33152
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33153
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT);
33154
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
33155
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_PERCENT));
33156
+ }
33157
+ return await bt.strategyCoreService.getPositionPnlPercent(false, symbol, currentPrice, {
33158
+ strategyName: context.strategyName,
33159
+ exchangeName: context.exchangeName,
33160
+ frameName: "",
33161
+ });
33162
+ };
33163
+ this.getPositionPnlCost = async (symbol, currentPrice, context) => {
33164
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PNL_COST, { symbol, currentPrice, context });
33165
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
33166
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
33167
+ {
33168
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33169
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST);
33170
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
33171
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PNL_COST));
33172
+ }
33173
+ return await bt.strategyCoreService.getPositionPnlCost(false, symbol, currentPrice, {
33174
+ strategyName: context.strategyName,
33175
+ exchangeName: context.exchangeName,
33176
+ frameName: "",
33177
+ });
33178
+ };
33179
+ this.getPositionLevels = async (symbol, context) => {
33180
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_LEVELS, { symbol, context });
33181
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
33182
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
33183
+ {
33184
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33185
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS);
33186
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
33187
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_LEVELS));
33188
+ }
33189
+ return await bt.strategyCoreService.getPositionLevels(false, symbol, {
33190
+ strategyName: context.strategyName,
33191
+ exchangeName: context.exchangeName,
33192
+ frameName: "",
33193
+ });
33194
+ };
33195
+ this.getPositionPartials = async (symbol, context) => {
33196
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_POSITION_PARTIALS, { symbol, context });
33197
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
33198
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
33199
+ {
33200
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33201
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS);
33202
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
33203
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_POSITION_PARTIALS));
33204
+ }
33205
+ return await bt.strategyCoreService.getPositionPartials(false, symbol, {
33206
+ strategyName: context.strategyName,
33207
+ exchangeName: context.exchangeName,
33208
+ frameName: "",
33209
+ });
33210
+ };
32367
33211
  /**
32368
33212
  * Stops the strategy from generating new signals.
32369
33213
  *
@@ -32582,6 +33426,112 @@ class LiveUtils {
32582
33426
  frameName: "",
32583
33427
  });
32584
33428
  };
33429
+ /**
33430
+ * Executes partial close at profit level by absolute dollar amount (moving toward TP).
33431
+ *
33432
+ * Convenience wrapper around commitPartialProfit that converts a dollar amount
33433
+ * to a percentage of the invested position cost automatically.
33434
+ * Price must be moving toward take profit (in profit direction).
33435
+ *
33436
+ * @param symbol - Trading pair symbol
33437
+ * @param dollarAmount - Dollar value of position to close (e.g. 150 closes $150 worth)
33438
+ * @param currentPrice - Current market price for this partial close
33439
+ * @param context - Execution context with strategyName and exchangeName
33440
+ * @returns Promise<boolean> - true if partial close executed, false if skipped or no position
33441
+ *
33442
+ * @throws Error if currentPrice is not in profit direction:
33443
+ * - LONG: currentPrice must be > priceOpen
33444
+ * - SHORT: currentPrice must be < priceOpen
33445
+ *
33446
+ * @example
33447
+ * ```typescript
33448
+ * // Close $150 of a $300 position (50%) at profit
33449
+ * const success = await Live.commitPartialProfitCost("BTCUSDT", 150, 45000, {
33450
+ * exchangeName: "binance",
33451
+ * strategyName: "my-strategy"
33452
+ * });
33453
+ * if (success) {
33454
+ * console.log('Partial profit executed');
33455
+ * }
33456
+ * ```
33457
+ */
33458
+ this.commitPartialProfitCost = async (symbol, dollarAmount, currentPrice, context) => {
33459
+ bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_PROFIT_COST, { symbol, dollarAmount, currentPrice, context });
33460
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
33461
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
33462
+ {
33463
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33464
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST);
33465
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
33466
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_PROFIT_COST));
33467
+ }
33468
+ const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
33469
+ strategyName: context.strategyName,
33470
+ exchangeName: context.exchangeName,
33471
+ frameName: "",
33472
+ });
33473
+ if (investedCost === null)
33474
+ return false;
33475
+ const percentToClose = (dollarAmount / investedCost) * 100;
33476
+ return await bt.strategyCoreService.partialProfit(false, symbol, percentToClose, currentPrice, {
33477
+ strategyName: context.strategyName,
33478
+ exchangeName: context.exchangeName,
33479
+ frameName: "",
33480
+ });
33481
+ };
33482
+ /**
33483
+ * Executes partial close at loss level by absolute dollar amount (moving toward SL).
33484
+ *
33485
+ * Convenience wrapper around commitPartialLoss that converts a dollar amount
33486
+ * to a percentage of the invested position cost automatically.
33487
+ * Price must be moving toward stop loss (in loss direction).
33488
+ *
33489
+ * @param symbol - Trading pair symbol
33490
+ * @param dollarAmount - Dollar value of position to close (e.g. 100 closes $100 worth)
33491
+ * @param currentPrice - Current market price for this partial close
33492
+ * @param context - Execution context with strategyName and exchangeName
33493
+ * @returns Promise<boolean> - true if partial close executed, false if skipped or no position
33494
+ *
33495
+ * @throws Error if currentPrice is not in loss direction:
33496
+ * - LONG: currentPrice must be < priceOpen
33497
+ * - SHORT: currentPrice must be > priceOpen
33498
+ *
33499
+ * @example
33500
+ * ```typescript
33501
+ * // Close $100 of a $300 position (~33%) at loss
33502
+ * const success = await Live.commitPartialLossCost("BTCUSDT", 100, 38000, {
33503
+ * exchangeName: "binance",
33504
+ * strategyName: "my-strategy"
33505
+ * });
33506
+ * if (success) {
33507
+ * console.log('Partial loss executed');
33508
+ * }
33509
+ * ```
33510
+ */
33511
+ this.commitPartialLossCost = async (symbol, dollarAmount, currentPrice, context) => {
33512
+ bt.loggerService.info(LIVE_METHOD_NAME_PARTIAL_LOSS_COST, { symbol, dollarAmount, currentPrice, context });
33513
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
33514
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
33515
+ {
33516
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
33517
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST);
33518
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
33519
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_PARTIAL_LOSS_COST));
33520
+ }
33521
+ const investedCost = await bt.strategyCoreService.getPositionInvestedCost(false, symbol, {
33522
+ strategyName: context.strategyName,
33523
+ exchangeName: context.exchangeName,
33524
+ frameName: "",
33525
+ });
33526
+ if (investedCost === null)
33527
+ return false;
33528
+ const percentToClose = (dollarAmount / investedCost) * 100;
33529
+ return await bt.strategyCoreService.partialLoss(false, symbol, percentToClose, currentPrice, {
33530
+ strategyName: context.strategyName,
33531
+ exchangeName: context.exchangeName,
33532
+ frameName: "",
33533
+ });
33534
+ };
32585
33535
  /**
32586
33536
  * Adjusts the trailing stop-loss distance for an active pending signal.
32587
33537
  *
@@ -40102,6 +41052,27 @@ const set = (object, path, value) => {
40102
41052
  }
40103
41053
  };
40104
41054
 
41055
+ /**
41056
+ * Calculate the percentage difference between two numbers.
41057
+ * @param {number} a - The first number.
41058
+ * @param {number} b - The second number.
41059
+ * @returns {number} The percentage difference between the two numbers.
41060
+ */
41061
+ const percentDiff = (a = 1, b = 2) => {
41062
+ const result = 100 / (Math.min(a, b) / Math.max(a, b)) - 100;
41063
+ return Number.isFinite(result) ? result : 100;
41064
+ };
41065
+
41066
+ /**
41067
+ * Calculate the percentage change from yesterday's value to today's value.
41068
+ * @param {number} yesterdayValue - The value from yesterday.
41069
+ * @param {number} todayValue - The value from today.
41070
+ * @returns {number} The percentage change from yesterday to today.
41071
+ */
41072
+ const percentValue = (yesterdayValue, todayValue) => {
41073
+ return yesterdayValue / todayValue - 1;
41074
+ };
41075
+
40105
41076
  exports.ActionBase = ActionBase;
40106
41077
  exports.Backtest = Backtest;
40107
41078
  exports.Breakeven = Breakeven;
@@ -40157,7 +41128,9 @@ exports.commitBreakeven = commitBreakeven;
40157
41128
  exports.commitCancelScheduled = commitCancelScheduled;
40158
41129
  exports.commitClosePending = commitClosePending;
40159
41130
  exports.commitPartialLoss = commitPartialLoss;
41131
+ exports.commitPartialLossCost = commitPartialLossCost;
40160
41132
  exports.commitPartialProfit = commitPartialProfit;
41133
+ exports.commitPartialProfitCost = commitPartialProfitCost;
40161
41134
  exports.commitTrailingStop = commitTrailingStop;
40162
41135
  exports.commitTrailingTake = commitTrailingTake;
40163
41136
  exports.dumpMessages = dumpMessages;
@@ -40184,6 +41157,13 @@ exports.getMode = getMode;
40184
41157
  exports.getNextCandles = getNextCandles;
40185
41158
  exports.getOrderBook = getOrderBook;
40186
41159
  exports.getPendingSignal = getPendingSignal;
41160
+ exports.getPositionAveragePrice = getPositionAveragePrice;
41161
+ exports.getPositionInvestedCost = getPositionInvestedCost;
41162
+ exports.getPositionInvestedCount = getPositionInvestedCount;
41163
+ exports.getPositionLevels = getPositionLevels;
41164
+ exports.getPositionPartials = getPositionPartials;
41165
+ exports.getPositionPnlCost = getPositionPnlCost;
41166
+ exports.getPositionPnlPercent = getPositionPnlPercent;
40187
41167
  exports.getRawCandles = getRawCandles;
40188
41168
  exports.getRiskSchema = getRiskSchema;
40189
41169
  exports.getScheduledSignal = getScheduledSignal;
@@ -40196,6 +41176,7 @@ exports.getTotalCostClosed = getTotalCostClosed;
40196
41176
  exports.getTotalPercentClosed = getTotalPercentClosed;
40197
41177
  exports.getWalkerSchema = getWalkerSchema;
40198
41178
  exports.hasTradeContext = hasTradeContext;
41179
+ exports.investedCostToPercent = investedCostToPercent;
40199
41180
  exports.lib = backtest;
40200
41181
  exports.listExchangeSchema = listExchangeSchema;
40201
41182
  exports.listFrameSchema = listFrameSchema;
@@ -40246,6 +41227,8 @@ exports.overrideSizingSchema = overrideSizingSchema;
40246
41227
  exports.overrideStrategySchema = overrideStrategySchema;
40247
41228
  exports.overrideWalkerSchema = overrideWalkerSchema;
40248
41229
  exports.parseArgs = parseArgs;
41230
+ exports.percentDiff = percentDiff;
41231
+ exports.percentValue = percentValue;
40249
41232
  exports.roundTicks = roundTicks;
40250
41233
  exports.set = set;
40251
41234
  exports.setColumns = setColumns;