backtest-kit 3.6.0 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -443,6 +443,14 @@ const GLOBAL_CONFIG = {
443
443
  * Default: true (mutex locking enabled for candle fetching)
444
444
  */
445
445
  CC_ENABLE_CANDLE_FETCH_MUTEX: true,
446
+ /**
447
+ * Enables DCA (Dollar-Cost Averaging) logic even if antirecord is not broken.
448
+ * Allows to commitAverageBuy if currentPrice is not the lowest price since entry, but still lower than priceOpen.
449
+ * 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.
450
+ *
451
+ * Default: true (DCA logic enabled everywhere, not just when antirecord is broken)
452
+ */
453
+ CC_ENABLE_DCA_EVERYWHERE: false,
446
454
  };
447
455
  const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
448
456
 
@@ -781,6 +789,11 @@ const PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA = "PersistLogUtils.writeLogData";
781
789
  const PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON = "PersistLogUtils.useJson";
782
790
  const PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY = "PersistLogUtils.useDummy";
783
791
  const PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER = "PersistLogUtils.usePersistLogAdapter";
792
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA = "PersistMeasureUtils.readMeasureData";
793
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA = "PersistMeasureUtils.writeMeasureData";
794
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON = "PersistMeasureUtils.useJson";
795
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY = "PersistMeasureUtils.useDummy";
796
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER = "PersistMeasureUtils.usePersistMeasureAdapter";
784
797
  const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
785
798
  const BASE_UNLINK_RETRY_COUNT = 5;
786
799
  const BASE_UNLINK_RETRY_DELAY = 1000;
@@ -2051,6 +2064,92 @@ class PersistLogUtils {
2051
2064
  * Used by LogPersistUtils for log entry persistence.
2052
2065
  */
2053
2066
  const PersistLogAdapter = new PersistLogUtils();
2067
+ /**
2068
+ * Utility class for managing external API response cache persistence.
2069
+ *
2070
+ * Features:
2071
+ * - Memoized storage instances per cache bucket (aligned timestamp + symbol)
2072
+ * - Custom adapter support
2073
+ * - Atomic read/write operations
2074
+ * - Crash-safe cache state management
2075
+ *
2076
+ * Used by Cache.file for persistent caching of external API responses.
2077
+ */
2078
+ class PersistMeasureUtils {
2079
+ constructor() {
2080
+ this.PersistMeasureFactory = PersistBase;
2081
+ this.getMeasureStorage = memoize(([bucket]) => bucket, (bucket) => Reflect.construct(this.PersistMeasureFactory, [
2082
+ bucket,
2083
+ `./dump/data/measure/`,
2084
+ ]));
2085
+ /**
2086
+ * Reads cached measure data for a given bucket and key.
2087
+ *
2088
+ * @param bucket - Storage bucket (e.g. aligned timestamp + symbol)
2089
+ * @param key - Dynamic cache key within the bucket
2090
+ * @returns Promise resolving to cached value or null if not found
2091
+ */
2092
+ this.readMeasureData = async (bucket, key) => {
2093
+ bt.loggerService.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, {
2094
+ bucket,
2095
+ key,
2096
+ });
2097
+ const isInitial = !this.getMeasureStorage.has(bucket);
2098
+ const stateStorage = this.getMeasureStorage(bucket);
2099
+ await stateStorage.waitForInit(isInitial);
2100
+ if (await stateStorage.hasValue(key)) {
2101
+ return await stateStorage.readValue(key);
2102
+ }
2103
+ return null;
2104
+ };
2105
+ /**
2106
+ * Writes measure data to disk with atomic file writes.
2107
+ *
2108
+ * @param data - Data to cache
2109
+ * @param bucket - Storage bucket (e.g. aligned timestamp + symbol)
2110
+ * @param key - Dynamic cache key within the bucket
2111
+ * @returns Promise that resolves when write is complete
2112
+ */
2113
+ this.writeMeasureData = async (data, bucket, key) => {
2114
+ bt.loggerService.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, {
2115
+ bucket,
2116
+ key,
2117
+ });
2118
+ const isInitial = !this.getMeasureStorage.has(bucket);
2119
+ const stateStorage = this.getMeasureStorage(bucket);
2120
+ await stateStorage.waitForInit(isInitial);
2121
+ await stateStorage.writeValue(key, data);
2122
+ };
2123
+ }
2124
+ /**
2125
+ * Registers a custom persistence adapter.
2126
+ *
2127
+ * @param Ctor - Custom PersistBase constructor
2128
+ */
2129
+ usePersistMeasureAdapter(Ctor) {
2130
+ bt.loggerService.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
2131
+ this.PersistMeasureFactory = Ctor;
2132
+ }
2133
+ /**
2134
+ * Switches to the default JSON persist adapter.
2135
+ */
2136
+ useJson() {
2137
+ bt.loggerService.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON);
2138
+ this.usePersistMeasureAdapter(PersistBase);
2139
+ }
2140
+ /**
2141
+ * Switches to a dummy persist adapter that discards all writes.
2142
+ */
2143
+ useDummy() {
2144
+ bt.loggerService.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY);
2145
+ this.usePersistMeasureAdapter(PersistDummy);
2146
+ }
2147
+ }
2148
+ /**
2149
+ * Global singleton instance of PersistMeasureUtils.
2150
+ * Used by Cache.file for persistent caching of external API responses.
2151
+ */
2152
+ const PersistMeasureAdapter = new PersistMeasureUtils();
2054
2153
 
2055
2154
  var _a$3, _b$3;
2056
2155
  const BUSY_DELAY = 100;
@@ -3083,75 +3182,90 @@ class ExchangeConnectionService {
3083
3182
  }
3084
3183
  }
3085
3184
 
3185
+ const COST_BASIS_PER_ENTRY$3 = 100;
3086
3186
  /**
3087
3187
  * Returns the effective entry price for price calculations.
3088
3188
  *
3089
- * When the _entry array exists and has at least one element, returns
3090
- * the simple arithmetic mean of all entry prices (DCA average).
3091
- * Otherwise returns the original signal.priceOpen.
3189
+ * Uses harmonic mean (correct for fixed-dollar DCA: $100 per entry).
3092
3190
  *
3093
- * This mirrors the _trailingPriceStopLoss pattern: original price is preserved
3094
- * in signal.priceOpen (for identity/tracking), while calculations use the
3095
- * effective averaged price returned by this function.
3191
+ * When partial closes exist, replays the partial sequence to reconstruct
3192
+ * the running cost basis at each partial — no extra stored fields needed.
3096
3193
  *
3097
- * @param signal - Signal row (ISignalRow or IScheduledSignalRow)
3098
- * @returns Effective entry price for distance and PNL calculations
3194
+ * Cost basis replay:
3195
+ * costBasis starts at 0
3196
+ * for each partial[i]:
3197
+ * newEntries = entryCountAtClose[i] - entryCountAtClose[i-1] (or entryCountAtClose[0] for i=0)
3198
+ * costBasis += newEntries × $100 ← add DCA entries up to this partial
3199
+ * positionCostBasisAtClose[i] = costBasis ← snapshot BEFORE close
3200
+ * costBasis × = (1 - percent[i] / 100) ← reduce after close
3201
+ *
3202
+ * @param signal - Signal row
3203
+ * @returns Effective entry price for PNL calculations
3099
3204
  */
3100
3205
  const getEffectivePriceOpen = (signal) => {
3101
- if (signal._entry && signal._entry.length > 0) {
3102
- return signal._entry.reduce((sum, e) => sum + e.price, 0) / signal._entry.length;
3103
- }
3104
- return signal.priceOpen;
3206
+ if (!signal._entry || signal._entry.length === 0)
3207
+ return signal.priceOpen;
3208
+ const entries = signal._entry;
3209
+ const partials = signal._partial ?? [];
3210
+ // No partial exits — pure harmonic mean of all entries
3211
+ if (partials.length === 0) {
3212
+ return harmonicMean(entries.map((e) => e.price));
3213
+ }
3214
+ // Replay cost basis through all partials to get snapshot at the last one
3215
+ let costBasis = 0;
3216
+ for (let i = 0; i < partials.length; i++) {
3217
+ const prevCount = i === 0 ? 0 : partials[i - 1].entryCountAtClose;
3218
+ const newEntryCount = partials[i].entryCountAtClose - prevCount;
3219
+ costBasis += newEntryCount * COST_BASIS_PER_ENTRY$3;
3220
+ // costBasis is now positionCostBasisAtClose for partials[i]
3221
+ if (i < partials.length - 1) {
3222
+ costBasis *= 1 - partials[i].percent / 100;
3223
+ }
3224
+ }
3225
+ const lastPartial = partials[partials.length - 1];
3226
+ // Dollar cost basis remaining after the last partial close
3227
+ const remainingCostBasis = costBasis * (1 - lastPartial.percent / 100);
3228
+ // Coins remaining from the old position
3229
+ const oldCoins = remainingCostBasis / lastPartial.effectivePrice;
3230
+ // New DCA entries added AFTER the last partial close
3231
+ const newEntries = entries.slice(lastPartial.entryCountAtClose);
3232
+ // Coins from new DCA entries (each costs $100)
3233
+ const newCoins = newEntries.reduce((sum, e) => sum + 100 / e.price, 0);
3234
+ const totalCoins = oldCoins + newCoins;
3235
+ if (totalCoins === 0)
3236
+ return lastPartial.effectivePrice;
3237
+ const totalCost = remainingCostBasis + newEntries.length * 100;
3238
+ return totalCost / totalCoins;
3239
+ };
3240
+ const harmonicMean = (prices) => {
3241
+ if (prices.length === 0)
3242
+ return 0;
3243
+ return prices.length / prices.reduce((sum, p) => sum + 1 / p, 0);
3105
3244
  };
3106
3245
 
3246
+ const COST_BASIS_PER_ENTRY$2 = 100;
3107
3247
  /**
3108
3248
  * Calculates profit/loss for a closed signal with slippage and fees.
3109
3249
  *
3110
3250
  * For signals with partial closes:
3111
- * - Calculates weighted PNL: Σ(percent_i × pnl_i) for each partial + (remaining% × final_pnl)
3112
- * - Each partial close has its own slippage
3113
- * - Open fee is charged once; close fees are proportional to each partial's size
3114
- * - Total fees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partial% / 100) × (closeWithSlip / openWithSlip)
3115
- *
3116
- * Formula breakdown:
3117
- * 1. Apply slippage to open/close prices (worse execution)
3118
- * - LONG: buy higher (+slippage), sell lower (-slippage)
3119
- * - SHORT: sell lower (-slippage), buy higher (+slippage)
3120
- * 2. Calculate raw PNL percentage
3121
- * - LONG: ((closePrice - openPrice) / openPrice) * 100
3122
- * - SHORT: ((openPrice - closePrice) / openPrice) * 100
3123
- * 3. Subtract total fees: open fee + close fee adjusted for slippage-affected execution price
3251
+ * - Weights are calculated by ACTUAL DOLLAR VALUE of each partial relative to total invested.
3252
+ * This correctly handles DCA entries that occur before or after partial closes.
3253
+ *
3254
+ * Cost basis is reconstructed by replaying the partial sequence via entryCountAtClose + percent:
3255
+ * costBasis = 0
3256
+ * for each partial[i]:
3257
+ * costBasis += (entryCountAtClose[i] - entryCountAtClose[i-1]) × $100
3258
+ * partialDollarValue[i] = (percent[i] / 100) × costBasis
3259
+ * weight[i] = partialDollarValue[i] / totalInvested
3260
+ * costBasis *= (1 - percent[i] / 100)
3261
+ *
3262
+ * Fee structure:
3263
+ * - Open fee: CC_PERCENT_FEE (charged once)
3264
+ * - Close fee: CC_PERCENT_FEE × weight × (closeWithSlip / openWithSlip) per partial/remaining
3124
3265
  *
3125
3266
  * @param signal - Closed signal with position details and optional partial history
3126
3267
  * @param priceClose - Actual close price at final exit
3127
3268
  * @returns PNL data with percentage and prices
3128
- *
3129
- * @example
3130
- * ```typescript
3131
- * // Signal without partial closes
3132
- * const pnl = toProfitLossDto(
3133
- * {
3134
- * position: "long",
3135
- * priceOpen: 100,
3136
- * },
3137
- * 110 // close at +10%
3138
- * );
3139
- * console.log(pnl.pnlPercentage); // ~9.6% (after slippage and fees)
3140
- *
3141
- * // Signal with partial closes
3142
- * const pnlPartial = toProfitLossDto(
3143
- * {
3144
- * position: "long",
3145
- * priceOpen: 100,
3146
- * _partial: [
3147
- * { type: "profit", percent: 30, price: 120 }, // +20% on 30%
3148
- * { type: "profit", percent: 40, price: 115 }, // +15% on 40%
3149
- * ],
3150
- * },
3151
- * 105 // final close at +5% for remaining 30%
3152
- * );
3153
- * // Weighted PNL = 30% × 20% + 40% × 15% + 30% × 5% = 6% + 6% + 1.5% = 13.5% (before fees)
3154
- * ```
3155
3269
  */
3156
3270
  const toProfitLossDto = (signal, priceClose) => {
3157
3271
  const priceOpen = getEffectivePriceOpen(signal);
@@ -3160,47 +3274,65 @@ const toProfitLossDto = (signal, priceClose) => {
3160
3274
  let totalWeightedPnl = 0;
3161
3275
  // Open fee is paid once for the whole position
3162
3276
  let totalFees = GLOBAL_CONFIG.CC_PERCENT_FEE;
3163
- // priceOpenWithSlippage is the same for all partials compute once
3164
- const priceOpenWithSlippage = signal.position === "long"
3165
- ? priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3166
- : priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3277
+ // Total invested capital = number of DCA entries × $100 per entry
3278
+ const totalInvested = signal._entry ? signal._entry.length * 100 : 100;
3279
+ let closedDollarValue = 0;
3280
+ // Running cost basis replayed from entryCountAtClose + percent
3281
+ let costBasis = 0;
3167
3282
  // Calculate PNL for each partial close
3168
- for (const partial of signal._partial) {
3169
- const partialPercent = partial.percent;
3283
+ for (let i = 0; i < signal._partial.length; i++) {
3284
+ const partial = signal._partial[i];
3285
+ // Add DCA entries that existed at this partial but not at the previous one
3286
+ const prevCount = i === 0 ? 0 : signal._partial[i - 1].entryCountAtClose;
3287
+ const newEntryCount = partial.entryCountAtClose - prevCount;
3288
+ costBasis += newEntryCount * COST_BASIS_PER_ENTRY$2;
3289
+ // Real dollar value sold in this partial
3290
+ const partialDollarValue = (partial.percent / 100) * costBasis;
3291
+ // Weight relative to total invested capital
3292
+ const weight = partialDollarValue / totalInvested;
3293
+ closedDollarValue += partialDollarValue;
3294
+ // Reduce cost basis after close
3295
+ costBasis *= 1 - partial.percent / 100;
3296
+ // Use the effective entry price snapshot captured at the time of this partial close
3297
+ const priceOpenWithSlippage = signal.position === "long"
3298
+ ? partial.effectivePrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3299
+ : partial.effectivePrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3170
3300
  const priceCloseWithSlippage = signal.position === "long"
3171
3301
  ? partial.price * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3172
3302
  : partial.price * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3173
- // Calculate PNL for this partial
3174
3303
  const partialPnl = signal.position === "long"
3175
3304
  ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
3176
3305
  : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
3177
- // Weight by percentage of position closed
3178
- totalWeightedPnl += (partialPercent / 100) * partialPnl;
3179
- // Close fee is proportional to the size of this partial and adjusted for slippage
3180
- totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (partialPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
3181
- }
3182
- // Calculate PNL for remaining position (if any)
3183
- // Compute totalClosed from _partial array
3184
- const totalClosed = signal._partial.reduce((sum, p) => sum + p.percent, 0);
3185
- if (totalClosed > 100) {
3186
- throw new Error(`Partial closes exceed 100%: ${totalClosed}% (signal id: ${signal.id})`);
3187
- }
3188
- const remainingPercent = 100 - totalClosed;
3189
- if (remainingPercent > 0) {
3306
+ totalWeightedPnl += weight * partialPnl;
3307
+ // Close fee proportional to real dollar weight
3308
+ totalFees +=
3309
+ GLOBAL_CONFIG.CC_PERCENT_FEE *
3310
+ weight *
3311
+ (priceCloseWithSlippage / priceOpenWithSlippage);
3312
+ }
3313
+ if (closedDollarValue > totalInvested + 0.001) {
3314
+ throw new Error(`Partial closes dollar value (${closedDollarValue.toFixed(4)}) exceeds total invested (${totalInvested}) — signal id: ${signal.id}`);
3315
+ }
3316
+ // Remaining position
3317
+ const remainingDollarValue = totalInvested - closedDollarValue;
3318
+ const remainingWeight = remainingDollarValue / totalInvested;
3319
+ if (remainingWeight > 0) {
3320
+ // Use current effective price — reflects all DCA including post-partial entries
3321
+ const remainingOpenWithSlippage = signal.position === "long"
3322
+ ? priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3323
+ : priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3190
3324
  const priceCloseWithSlippage = signal.position === "long"
3191
3325
  ? priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3192
3326
  : priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3193
- // Calculate PNL for remaining
3194
3327
  const remainingPnl = signal.position === "long"
3195
- ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
3196
- : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
3197
- // Weight by remaining percentage
3198
- totalWeightedPnl += (remainingPercent / 100) * remainingPnl;
3199
- // Close fee is proportional to the remaining size and adjusted for slippage
3200
- totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (remainingPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
3328
+ ? ((priceCloseWithSlippage - remainingOpenWithSlippage) / remainingOpenWithSlippage) * 100
3329
+ : ((remainingOpenWithSlippage - priceCloseWithSlippage) / remainingOpenWithSlippage) * 100;
3330
+ totalWeightedPnl += remainingWeight * remainingPnl;
3331
+ totalFees +=
3332
+ GLOBAL_CONFIG.CC_PERCENT_FEE *
3333
+ remainingWeight *
3334
+ (priceCloseWithSlippage / remainingOpenWithSlippage);
3201
3335
  }
3202
- // Subtract total fees from weighted PNL
3203
- // totalFees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partialPercent/100) × (closeWithSlip/openWithSlip)
3204
3336
  const pnlPercentage = totalWeightedPnl - totalFees;
3205
3337
  return {
3206
3338
  pnlPercentage,
@@ -3212,33 +3344,24 @@ const toProfitLossDto = (signal, priceClose) => {
3212
3344
  let priceOpenWithSlippage;
3213
3345
  let priceCloseWithSlippage;
3214
3346
  if (signal.position === "long") {
3215
- // LONG: покупаем дороже, продаем дешевле
3216
3347
  priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3217
3348
  priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3218
3349
  }
3219
3350
  else {
3220
- // SHORT: продаем дешевле, покупаем дороже
3221
3351
  priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3222
3352
  priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3223
3353
  }
3224
- // Открытие: комиссия от цены входа; закрытие: комиссия от фактической цены выхода (с учётом slippage)
3225
- const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * (1 + priceCloseWithSlippage / priceOpenWithSlippage);
3354
+ const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE *
3355
+ (1 + priceCloseWithSlippage / priceOpenWithSlippage);
3226
3356
  let pnlPercentage;
3227
3357
  if (signal.position === "long") {
3228
- // LONG: прибыль при росте цены
3229
3358
  pnlPercentage =
3230
- ((priceCloseWithSlippage - priceOpenWithSlippage) /
3231
- priceOpenWithSlippage) *
3232
- 100;
3359
+ ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
3233
3360
  }
3234
3361
  else {
3235
- // SHORT: прибыль при падении цены
3236
3362
  pnlPercentage =
3237
- ((priceOpenWithSlippage - priceCloseWithSlippage) /
3238
- priceOpenWithSlippage) *
3239
- 100;
3363
+ ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
3240
3364
  }
3241
- // Вычитаем комиссии
3242
3365
  pnlPercentage -= totalFee;
3243
3366
  return {
3244
3367
  pnlPercentage,
@@ -3294,6 +3417,54 @@ const toPlainString = (content) => {
3294
3417
  return text.trim();
3295
3418
  };
3296
3419
 
3420
+ const COST_BASIS_PER_ENTRY$1 = 100;
3421
+ /**
3422
+ * Returns the total closed state of a position using cost-basis replay.
3423
+ *
3424
+ * Correctly accounts for DCA entries added between partial closes via averageBuy().
3425
+ * Simple percent summation (sum of _partial[i].percent) is INCORRECT when averageBuy()
3426
+ * is called between partials — this function uses the same cost-basis replay as
3427
+ * toProfitLossDto to compute the true dollar-weighted closed fraction.
3428
+ *
3429
+ * Cost-basis replay:
3430
+ * costBasis = 0
3431
+ * for each partial[i]:
3432
+ * costBasis += (entryCountAtClose[i] - entryCountAtClose[i-1]) × $100
3433
+ * closedDollar += (percent[i] / 100) × costBasis
3434
+ * costBasis ×= (1 - percent[i] / 100)
3435
+ * // then add entries added AFTER the last partial
3436
+ * costBasis += (currentEntryCount - lastPartialEntryCount) × $100
3437
+ *
3438
+ * @param signal - Signal row with _partial and _entry arrays
3439
+ * @returns Object with totalClosedPercent (0–100+) and remainingCostBasis (dollar value still open)
3440
+ */
3441
+ const getTotalClosed = (signal) => {
3442
+ const partials = signal._partial ?? [];
3443
+ const currentEntryCount = signal._entry?.length ?? 1;
3444
+ const totalInvested = currentEntryCount * COST_BASIS_PER_ENTRY$1;
3445
+ if (partials.length === 0) {
3446
+ return {
3447
+ totalClosedPercent: 0,
3448
+ remainingCostBasis: totalInvested,
3449
+ };
3450
+ }
3451
+ let costBasis = 0;
3452
+ let closedDollarValue = 0;
3453
+ for (let i = 0; i < partials.length; i++) {
3454
+ const prevCount = i === 0 ? 0 : partials[i - 1].entryCountAtClose;
3455
+ costBasis += (partials[i].entryCountAtClose - prevCount) * COST_BASIS_PER_ENTRY$1;
3456
+ closedDollarValue += (partials[i].percent / 100) * costBasis;
3457
+ costBasis *= 1 - partials[i].percent / 100;
3458
+ }
3459
+ // Add entries added AFTER the last partial (not yet accounted for in the loop)
3460
+ const lastEntryCount = partials[partials.length - 1].entryCountAtClose;
3461
+ costBasis += (currentEntryCount - lastEntryCount) * COST_BASIS_PER_ENTRY$1;
3462
+ return {
3463
+ totalClosedPercent: totalInvested > 0 ? (closedDollarValue / totalInvested) * 100 : 0,
3464
+ remainingCostBasis: costBasis,
3465
+ };
3466
+ };
3467
+
3297
3468
  /**
3298
3469
  * Wraps a function to execute it outside of the current execution context if one exists.
3299
3470
  *
@@ -3336,6 +3507,19 @@ const beginTime = (run) => (...args) => {
3336
3507
  return fn();
3337
3508
  };
3338
3509
 
3510
+ /**
3511
+ * Retrieves the current timestamp for debugging purposes.
3512
+ * 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.
3513
+ * Can be empty (undefined) if not called from strategy async context, as it's intended for debugging and not critical for logic.
3514
+ * @return {number | undefined} The current timestamp in milliseconds from the execution context, or undefined if not available.
3515
+ */
3516
+ const getDebugTimestamp = () => {
3517
+ if (ExecutionContextService.hasContext()) {
3518
+ return bt.executionContextService.context.when.getTime();
3519
+ }
3520
+ return undefined;
3521
+ };
3522
+
3339
3523
  const INTERVAL_MINUTES$6 = {
3340
3524
  "1m": 1,
3341
3525
  "3m": 3,
@@ -3344,6 +3528,7 @@ const INTERVAL_MINUTES$6 = {
3344
3528
  "30m": 30,
3345
3529
  "1h": 60,
3346
3530
  };
3531
+ const COST_BASIS_PER_ENTRY = 100;
3347
3532
  /**
3348
3533
  * Mock value for scheduled signal pendingAt timestamp.
3349
3534
  * Used to indicate that the actual pendingAt will be set upon activation.
@@ -3945,7 +4130,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
3945
4130
  scheduledAt: currentTime,
3946
4131
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
3947
4132
  _isScheduled: false,
3948
- _entry: [{ price: signal.priceOpen }],
4133
+ _entry: [{ price: signal.priceOpen, debugTimestamp: currentTime }],
3949
4134
  };
3950
4135
  // Валидируем сигнал перед возвратом
3951
4136
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -3967,7 +4152,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
3967
4152
  scheduledAt: currentTime,
3968
4153
  pendingAt: SCHEDULED_SIGNAL_PENDING_MOCK, // Временно, обновится при активации
3969
4154
  _isScheduled: true,
3970
- _entry: [{ price: signal.priceOpen }],
4155
+ _entry: [{ price: signal.priceOpen, debugTimestamp: currentTime }],
3971
4156
  };
3972
4157
  // Валидируем сигнал перед возвратом
3973
4158
  VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
@@ -3985,7 +4170,7 @@ const GET_SIGNAL_FN = trycatch(async (self) => {
3985
4170
  scheduledAt: currentTime,
3986
4171
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
3987
4172
  _isScheduled: false,
3988
- _entry: [{ price: currentPrice }],
4173
+ _entry: [{ price: currentPrice, debugTimestamp: currentTime }],
3989
4174
  };
3990
4175
  // Валидируем сигнал перед возвратом
3991
4176
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -4059,37 +4244,39 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4059
4244
  // Initialize partial array if not present
4060
4245
  if (!signal._partial)
4061
4246
  signal._partial = [];
4062
- // Calculate current totals (computed values)
4063
- const tpClosed = signal._partial
4064
- .filter((p) => p.type === "profit")
4065
- .reduce((sum, p) => sum + p.percent, 0);
4066
- const slClosed = signal._partial
4067
- .filter((p) => p.type === "loss")
4068
- .reduce((sum, p) => sum + p.percent, 0);
4069
- const totalClosed = tpClosed + slClosed;
4070
- // Check if would exceed 100% total closed
4071
- const newTotalClosed = totalClosed + percentToClose;
4072
- if (newTotalClosed > 100) {
4073
- self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed, skipping", {
4247
+ // Check if would exceed 100% total closed (dollar-basis, DCA-aware)
4248
+ const { totalClosedPercent, remainingCostBasis } = getTotalClosed(signal);
4249
+ const totalInvested = (signal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
4250
+ const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
4251
+ const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
4252
+ if (newTotalClosedDollar > totalInvested) {
4253
+ self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed (dollar basis), skipping", {
4074
4254
  signalId: signal.id,
4075
- currentTotalClosed: totalClosed,
4255
+ totalClosedPercent,
4256
+ remainingCostBasis,
4076
4257
  percentToClose,
4077
- newTotalClosed,
4258
+ newPartialDollar,
4259
+ totalInvested,
4078
4260
  });
4079
4261
  return false;
4080
4262
  }
4263
+ // Capture effective entry price at the moment of partial close (for DCA-aware PNL)
4264
+ const effectivePrice = getEffectivePriceOpen(signal);
4265
+ const entryCountAtClose = signal._entry ? signal._entry.length : 1;
4081
4266
  // Add new partial close entry
4082
4267
  signal._partial.push({
4083
4268
  type: "profit",
4084
4269
  percent: percentToClose,
4270
+ entryCountAtClose,
4085
4271
  price: currentPrice,
4272
+ debugTimestamp: getDebugTimestamp(),
4273
+ effectivePrice,
4086
4274
  });
4087
4275
  self.params.logger.info("PARTIAL_PROFIT_FN executed", {
4088
4276
  signalId: signal.id,
4089
4277
  percentClosed: percentToClose,
4090
- totalClosed: newTotalClosed,
4278
+ totalClosedPercent: totalClosedPercent + (newPartialDollar / totalInvested) * 100,
4091
4279
  currentPrice,
4092
- tpClosed: tpClosed + percentToClose,
4093
4280
  });
4094
4281
  return true;
4095
4282
  };
@@ -4097,37 +4284,39 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
4097
4284
  // Initialize partial array if not present
4098
4285
  if (!signal._partial)
4099
4286
  signal._partial = [];
4100
- // Calculate current totals (computed values)
4101
- const tpClosed = signal._partial
4102
- .filter((p) => p.type === "profit")
4103
- .reduce((sum, p) => sum + p.percent, 0);
4104
- const slClosed = signal._partial
4105
- .filter((p) => p.type === "loss")
4106
- .reduce((sum, p) => sum + p.percent, 0);
4107
- const totalClosed = tpClosed + slClosed;
4108
- // Check if would exceed 100% total closed
4109
- const newTotalClosed = totalClosed + percentToClose;
4110
- if (newTotalClosed > 100) {
4111
- self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed, skipping", {
4287
+ // Check if would exceed 100% total closed (dollar-basis, DCA-aware)
4288
+ const { totalClosedPercent, remainingCostBasis } = getTotalClosed(signal);
4289
+ const totalInvested = (signal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
4290
+ const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
4291
+ const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
4292
+ if (newTotalClosedDollar > totalInvested) {
4293
+ self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed (dollar basis), skipping", {
4112
4294
  signalId: signal.id,
4113
- currentTotalClosed: totalClosed,
4295
+ totalClosedPercent,
4296
+ remainingCostBasis,
4114
4297
  percentToClose,
4115
- newTotalClosed,
4298
+ newPartialDollar,
4299
+ totalInvested,
4116
4300
  });
4117
4301
  return false;
4118
4302
  }
4303
+ // Capture effective entry price at the moment of partial close (for DCA-aware PNL)
4304
+ const effectivePrice = getEffectivePriceOpen(signal);
4305
+ const entryCountAtClose = signal._entry ? signal._entry.length : 1;
4119
4306
  // Add new partial close entry
4120
4307
  signal._partial.push({
4121
4308
  type: "loss",
4122
4309
  percent: percentToClose,
4123
4310
  price: currentPrice,
4311
+ entryCountAtClose,
4312
+ effectivePrice,
4313
+ debugTimestamp: getDebugTimestamp(),
4124
4314
  });
4125
4315
  self.params.logger.warn("PARTIAL_LOSS_FN executed", {
4126
4316
  signalId: signal.id,
4127
4317
  percentClosed: percentToClose,
4128
- totalClosed: newTotalClosed,
4318
+ totalClosedPercent: totalClosedPercent + (newPartialDollar / totalInvested) * 100,
4129
4319
  currentPrice,
4130
- slClosed: slClosed + percentToClose,
4131
4320
  });
4132
4321
  return true;
4133
4322
  };
@@ -4521,12 +4710,12 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4521
4710
  const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
4522
4711
  // Ensure _entry is initialized (handles signals loaded from disk without _entry)
4523
4712
  if (!signal._entry || signal._entry.length === 0) {
4524
- signal._entry = [{ price: signal.priceOpen }];
4713
+ signal._entry = [{ price: signal.priceOpen, debugTimestamp: getDebugTimestamp() }];
4525
4714
  }
4526
4715
  const lastEntry = signal._entry[signal._entry.length - 1];
4527
4716
  if (signal.position === "long") {
4528
4717
  // LONG: averaging down = currentPrice must be strictly lower than last entry
4529
- if (currentPrice >= lastEntry.price) {
4718
+ if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice >= lastEntry.price) {
4530
4719
  self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice >= last entry (LONG)", {
4531
4720
  signalId: signal.id,
4532
4721
  position: signal.position,
@@ -4539,7 +4728,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
4539
4728
  }
4540
4729
  else {
4541
4730
  // SHORT: averaging down = currentPrice must be strictly higher than last entry
4542
- if (currentPrice <= lastEntry.price) {
4731
+ if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice <= lastEntry.price) {
4543
4732
  self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice <= last entry (SHORT)", {
4544
4733
  signalId: signal.id,
4545
4734
  position: signal.position,
@@ -4550,7 +4739,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
4550
4739
  return false;
4551
4740
  }
4552
4741
  }
4553
- signal._entry.push({ price: currentPrice });
4742
+ signal._entry.push({ price: currentPrice, debugTimestamp: getDebugTimestamp() });
4554
4743
  self.params.logger.info("AVERAGE_BUY_FN executed", {
4555
4744
  signalId: signal.id,
4556
4745
  position: signal.position,
@@ -6015,6 +6204,52 @@ class ClientStrategy {
6015
6204
  });
6016
6205
  return this._isStopped;
6017
6206
  }
6207
+ /**
6208
+ * Returns how much of the position is still held, as a percentage of totalInvested.
6209
+ *
6210
+ * Uses dollar-basis cost-basis replay (DCA-aware).
6211
+ * 100% means nothing was closed yet. Decreases with each partial close.
6212
+ *
6213
+ * Example: 1 entry $100, partialProfit(30%) → returns 70
6214
+ * Example: 2 entries $200, partialProfit(50%) → returns 50
6215
+ *
6216
+ * Returns 100 if no pending signal or no partial closes.
6217
+ *
6218
+ * @param symbol - Trading pair symbol
6219
+ * @returns Promise resolving to held percentage (0–100)
6220
+ */
6221
+ async getTotalPercentClosed(symbol) {
6222
+ this.params.logger.debug("ClientStrategy getTotalPercentClosed", { symbol });
6223
+ if (!this._pendingSignal) {
6224
+ return null;
6225
+ }
6226
+ const { totalClosedPercent } = getTotalClosed(this._pendingSignal);
6227
+ return 100 - totalClosedPercent;
6228
+ }
6229
+ /**
6230
+ * Returns how many dollars of cost basis are still held (not yet closed by partials).
6231
+ *
6232
+ * Equal to remainingCostBasis from getTotalClosed.
6233
+ * Full position open: equals totalInvested (entries × $100).
6234
+ * Decreases with each partial close, increases with each averageBuy().
6235
+ *
6236
+ * Example: 1 entry $100, no partials → returns 100
6237
+ * Example: 1 entry $100, partialProfit(30%) → returns 70
6238
+ * Example: 2 entries $200, partialProfit(50%) → returns 100
6239
+ *
6240
+ * Returns totalInvested if no pending signal or no partial closes.
6241
+ *
6242
+ * @param symbol - Trading pair symbol
6243
+ * @returns Promise resolving to held cost basis in dollars
6244
+ */
6245
+ async getTotalCostClosed(symbol) {
6246
+ this.params.logger.debug("ClientStrategy getTotalCostClosed", { symbol });
6247
+ if (!this._pendingSignal) {
6248
+ return null;
6249
+ }
6250
+ const { remainingCostBasis } = getTotalClosed(this._pendingSignal);
6251
+ return remainingCostBasis;
6252
+ }
6018
6253
  /**
6019
6254
  * Performs a single tick of strategy execution.
6020
6255
  *
@@ -7475,14 +7710,6 @@ class ClientStrategy {
7475
7710
  if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
7476
7711
  throw new Error(`ClientStrategy averageBuy: currentPrice must be a positive finite number, got ${currentPrice}`);
7477
7712
  }
7478
- // Reject if any partial closes have already been executed
7479
- if (this._pendingSignal._partial && this._pendingSignal._partial.length > 0) {
7480
- this.params.logger.debug("ClientStrategy averageBuy: rejected — partial closes already executed", {
7481
- symbol,
7482
- partialCount: this._pendingSignal._partial.length,
7483
- });
7484
- return false;
7485
- }
7486
7713
  // Execute averaging logic
7487
7714
  const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice);
7488
7715
  if (!result) {
@@ -8161,6 +8388,43 @@ class StrategyConnectionService {
8161
8388
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8162
8389
  return await strategy.getPendingSignal(symbol);
8163
8390
  };
8391
+ /**
8392
+ * Returns the percentage of the position currently held (not closed).
8393
+ * 100 = nothing has been closed (full position), 0 = fully closed.
8394
+ * Correctly accounts for DCA entries between partial closes.
8395
+ *
8396
+ * @param backtest - Whether running in backtest mode
8397
+ * @param symbol - Trading pair symbol
8398
+ * @param context - Execution context with strategyName, exchangeName, frameName
8399
+ * @returns Promise<number> - held percentage (0–100)
8400
+ */
8401
+ this.getTotalPercentClosed = async (backtest, symbol, context) => {
8402
+ this.loggerService.log("strategyConnectionService getTotalPercentClosed", {
8403
+ symbol,
8404
+ context,
8405
+ backtest,
8406
+ });
8407
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8408
+ return await strategy.getTotalPercentClosed(symbol);
8409
+ };
8410
+ /**
8411
+ * Returns the cost basis in dollars of the position currently held (not closed).
8412
+ * Correctly accounts for DCA entries between partial closes.
8413
+ *
8414
+ * @param backtest - Whether running in backtest mode
8415
+ * @param symbol - Trading pair symbol
8416
+ * @param context - Execution context with strategyName, exchangeName, frameName
8417
+ * @returns Promise<number> - held cost basis in dollars
8418
+ */
8419
+ this.getTotalCostClosed = async (backtest, symbol, context) => {
8420
+ this.loggerService.log("strategyConnectionService getTotalCostClosed", {
8421
+ symbol,
8422
+ context,
8423
+ backtest,
8424
+ });
8425
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8426
+ return await strategy.getTotalCostClosed(symbol);
8427
+ };
8164
8428
  /**
8165
8429
  * Retrieves the currently active scheduled signal for the strategy.
8166
8430
  * If no scheduled signal exists, returns null.
@@ -10351,7 +10615,7 @@ class ActionBase {
10351
10615
  * @example
10352
10616
  * ```typescript
10353
10617
  * pingScheduled(event: SchedulePingContract) {
10354
- * const waitTime = Date.now() - event.data.timestampScheduled;
10618
+ * const waitTime = getTimestamp() - event.data.timestampScheduled;
10355
10619
  * const waitMinutes = Math.floor(waitTime / 60000);
10356
10620
  * console.log(`Scheduled signal waiting ${waitMinutes} minutes`);
10357
10621
  * }
@@ -10380,7 +10644,7 @@ class ActionBase {
10380
10644
  * @example
10381
10645
  * ```typescript
10382
10646
  * pingActive(event: ActivePingContract) {
10383
- * const holdTime = Date.now() - event.data.pendingAt;
10647
+ * const holdTime = getTimestamp() - event.data.pendingAt;
10384
10648
  * const holdMinutes = Math.floor(holdTime / 60000);
10385
10649
  * console.log(`Active signal holding ${holdMinutes} minutes`);
10386
10650
  * }
@@ -11564,6 +11828,41 @@ class StrategyCoreService {
11564
11828
  await this.validate(context);
11565
11829
  return await this.strategyConnectionService.getPendingSignal(backtest, symbol, context);
11566
11830
  };
11831
+ /**
11832
+ * Returns the percentage of the position currently held (not closed).
11833
+ * 100 = nothing has been closed (full position), 0 = fully closed.
11834
+ * Correctly accounts for DCA entries between partial closes.
11835
+ *
11836
+ * @param backtest - Whether running in backtest mode
11837
+ * @param symbol - Trading pair symbol
11838
+ * @param context - Execution context with strategyName, exchangeName, frameName
11839
+ * @returns Promise<number> - held percentage (0–100)
11840
+ */
11841
+ this.getTotalPercentClosed = async (backtest, symbol, context) => {
11842
+ this.loggerService.log("strategyCoreService getTotalPercentClosed", {
11843
+ symbol,
11844
+ context,
11845
+ });
11846
+ await this.validate(context);
11847
+ return await this.strategyConnectionService.getTotalPercentClosed(backtest, symbol, context);
11848
+ };
11849
+ /**
11850
+ * Returns the cost basis in dollars of the position currently held (not closed).
11851
+ * Correctly accounts for DCA entries between partial closes.
11852
+ *
11853
+ * @param backtest - Whether running in backtest mode
11854
+ * @param symbol - Trading pair symbol
11855
+ * @param context - Execution context with strategyName, exchangeName, frameName
11856
+ * @returns Promise<number> - held cost basis in dollars
11857
+ */
11858
+ this.getTotalCostClosed = async (backtest, symbol, context) => {
11859
+ this.loggerService.log("strategyCoreService getTotalCostClosed", {
11860
+ symbol,
11861
+ context,
11862
+ });
11863
+ await this.validate(context);
11864
+ return await this.strategyConnectionService.getTotalCostClosed(backtest, symbol, context);
11865
+ };
11567
11866
  /**
11568
11867
  * Retrieves the currently active scheduled signal for the symbol.
11569
11868
  * If no scheduled signal exists, returns null.
@@ -16082,6 +16381,20 @@ const COLUMN_CONFIG = {
16082
16381
  */
16083
16382
  const DEFAULT_COLUMNS = Object.freeze({ ...COLUMN_CONFIG });
16084
16383
 
16384
+ /**
16385
+ * Retrieves the current timestamp based on the execution context.
16386
+ * 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.
16387
+ * If no execution context is active (e.g., during live operation), it returns the current real-world timestamp.
16388
+ * This function helps maintain accurate timing for logs, metrics, and other time-sensitive operations across both live and backtest modes.
16389
+ * @return {number} The current timestamp in milliseconds, either from the execution context or the real-world clock.
16390
+ */
16391
+ const getContextTimestamp = () => {
16392
+ if (ExecutionContextService.hasContext()) {
16393
+ return bt.executionContextService.context.when.getTime();
16394
+ }
16395
+ return Date.now();
16396
+ };
16397
+
16085
16398
  var _a$2, _b$2;
16086
16399
  const MARKDOWN_METHOD_NAME_ENABLE = "MarkdownUtils.enable";
16087
16400
  const MARKDOWN_METHOD_NAME_DISABLE = "MarkdownUtils.disable";
@@ -16215,7 +16528,7 @@ class MarkdownFileBase {
16215
16528
  markdownName: this.markdownName,
16216
16529
  data,
16217
16530
  ...searchFlags,
16218
- timestamp: Date.now(),
16531
+ timestamp: getContextTimestamp(),
16219
16532
  }) + "\n";
16220
16533
  const status = await this[WRITE_SAFE_SYMBOL$2](line);
16221
16534
  if (status === TIMEOUT_SYMBOL$1) {
@@ -16759,7 +17072,7 @@ let ReportStorage$7 = class ReportStorage {
16759
17072
  */
16760
17073
  async dump(strategyName, path = "./dump/backtest", columns = COLUMN_CONFIG.backtest_columns) {
16761
17074
  const markdown = await this.getReport(strategyName, columns);
16762
- const timestamp = Date.now();
17075
+ const timestamp = getContextTimestamp();
16763
17076
  const filename = CREATE_FILE_NAME_FN$9(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
16764
17077
  await Markdown.writeData("backtest", markdown, {
16765
17078
  path,
@@ -17094,7 +17407,7 @@ let ReportStorage$6 = class ReportStorage {
17094
17407
  */
17095
17408
  addIdleEvent(currentPrice) {
17096
17409
  const newEvent = {
17097
- timestamp: Date.now(),
17410
+ timestamp: getContextTimestamp(),
17098
17411
  action: "idle",
17099
17412
  currentPrice,
17100
17413
  };
@@ -17150,7 +17463,7 @@ let ReportStorage$6 = class ReportStorage {
17150
17463
  */
17151
17464
  addActiveEvent(data) {
17152
17465
  const newEvent = {
17153
- timestamp: Date.now(),
17466
+ timestamp: getContextTimestamp(),
17154
17467
  action: "active",
17155
17468
  symbol: data.signal.symbol,
17156
17469
  signalId: data.signal.id,
@@ -17252,7 +17565,7 @@ let ReportStorage$6 = class ReportStorage {
17252
17565
  */
17253
17566
  addWaitingEvent(data) {
17254
17567
  const newEvent = {
17255
- timestamp: Date.now(),
17568
+ timestamp: getContextTimestamp(),
17256
17569
  action: "waiting",
17257
17570
  symbol: data.signal.symbol,
17258
17571
  signalId: data.signal.id,
@@ -17446,7 +17759,7 @@ let ReportStorage$6 = class ReportStorage {
17446
17759
  */
17447
17760
  async dump(strategyName, path = "./dump/live", columns = COLUMN_CONFIG.live_columns) {
17448
17761
  const markdown = await this.getReport(strategyName, columns);
17449
- const timestamp = Date.now();
17762
+ const timestamp = getContextTimestamp();
17450
17763
  const filename = CREATE_FILE_NAME_FN$8(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
17451
17764
  await Markdown.writeData("live", markdown, {
17452
17765
  path,
@@ -17978,7 +18291,7 @@ let ReportStorage$5 = class ReportStorage {
17978
18291
  */
17979
18292
  async dump(strategyName, path = "./dump/schedule", columns = COLUMN_CONFIG.schedule_columns) {
17980
18293
  const markdown = await this.getReport(strategyName, columns);
17981
- const timestamp = Date.now();
18294
+ const timestamp = getContextTimestamp();
17982
18295
  const filename = CREATE_FILE_NAME_FN$7(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
17983
18296
  await Markdown.writeData("schedule", markdown, {
17984
18297
  path,
@@ -18443,7 +18756,7 @@ class PerformanceStorage {
18443
18756
  */
18444
18757
  async dump(strategyName, path = "./dump/performance", columns = COLUMN_CONFIG.performance_columns) {
18445
18758
  const markdown = await this.getReport(strategyName, columns);
18446
- const timestamp = Date.now();
18759
+ const timestamp = getContextTimestamp();
18447
18760
  const filename = CREATE_FILE_NAME_FN$6(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
18448
18761
  await Markdown.writeData("performance", markdown, {
18449
18762
  path,
@@ -18894,7 +19207,7 @@ let ReportStorage$4 = class ReportStorage {
18894
19207
  */
18895
19208
  async dump(symbol, metric, context, path = "./dump/walker", strategyColumns = COLUMN_CONFIG.walker_strategy_columns, pnlColumns = COLUMN_CONFIG.walker_pnl_columns) {
18896
19209
  const markdown = await this.getReport(symbol, metric, context, strategyColumns, pnlColumns);
18897
- const timestamp = Date.now();
19210
+ const timestamp = getContextTimestamp();
18898
19211
  const filename = CREATE_FILE_NAME_FN$5(this.walkerName, timestamp);
18899
19212
  await Markdown.writeData("walker", markdown, {
18900
19213
  path,
@@ -19448,7 +19761,7 @@ class HeatmapStorage {
19448
19761
  */
19449
19762
  async dump(strategyName, path = "./dump/heatmap", columns = COLUMN_CONFIG.heat_columns) {
19450
19763
  const markdown = await this.getReport(strategyName, columns);
19451
- const timestamp = Date.now();
19764
+ const timestamp = getContextTimestamp();
19452
19765
  const filename = CREATE_FILE_NAME_FN$4(strategyName, this.exchangeName, this.frameName, timestamp);
19453
19766
  await Markdown.writeData("heat", markdown, {
19454
19767
  path,
@@ -21128,7 +21441,7 @@ let ReportStorage$3 = class ReportStorage {
21128
21441
  */
21129
21442
  async dump(symbol, strategyName, path = "./dump/partial", columns = COLUMN_CONFIG.partial_columns) {
21130
21443
  const markdown = await this.getReport(symbol, strategyName, columns);
21131
- const timestamp = Date.now();
21444
+ const timestamp = getContextTimestamp();
21132
21445
  const filename = CREATE_FILE_NAME_FN$3(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
21133
21446
  await Markdown.writeData("partial", markdown, {
21134
21447
  path,
@@ -22221,7 +22534,7 @@ let ReportStorage$2 = class ReportStorage {
22221
22534
  */
22222
22535
  async dump(symbol, strategyName, path = "./dump/breakeven", columns = COLUMN_CONFIG.breakeven_columns) {
22223
22536
  const markdown = await this.getReport(symbol, strategyName, columns);
22224
- const timestamp = Date.now();
22537
+ const timestamp = getContextTimestamp();
22225
22538
  const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
22226
22539
  await Markdown.writeData("breakeven", markdown, {
22227
22540
  path,
@@ -22889,7 +23202,7 @@ let ReportStorage$1 = class ReportStorage {
22889
23202
  */
22890
23203
  async dump(symbol, strategyName, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) {
22891
23204
  const markdown = await this.getReport(symbol, strategyName, columns);
22892
- const timestamp = Date.now();
23205
+ const timestamp = getContextTimestamp();
22893
23206
  const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
22894
23207
  await Markdown.writeData("risk", markdown, {
22895
23208
  path,
@@ -23373,7 +23686,7 @@ class ReportBase {
23373
23686
  reportName: this.reportName,
23374
23687
  data,
23375
23688
  ...searchFlags,
23376
- timestamp: Date.now(),
23689
+ timestamp: getContextTimestamp(),
23377
23690
  }) + "\n";
23378
23691
  const status = await this[WRITE_SAFE_SYMBOL$1](line);
23379
23692
  if (status === TIMEOUT_SYMBOL$1) {
@@ -23712,7 +24025,7 @@ class BacktestReportService {
23712
24025
  this.tick = async (data) => {
23713
24026
  this.loggerService.log(BACKTEST_REPORT_METHOD_NAME_TICK, { data });
23714
24027
  const baseEvent = {
23715
- timestamp: Date.now(),
24028
+ timestamp: getContextTimestamp(),
23716
24029
  action: data.action,
23717
24030
  symbol: data.symbol,
23718
24031
  strategyName: data.strategyName,
@@ -23891,7 +24204,7 @@ class LiveReportService {
23891
24204
  this.tick = async (data) => {
23892
24205
  this.loggerService.log(LIVE_REPORT_METHOD_NAME_TICK, { data });
23893
24206
  const baseEvent = {
23894
- timestamp: Date.now(),
24207
+ timestamp: getContextTimestamp(),
23895
24208
  action: data.action,
23896
24209
  symbol: data.symbol,
23897
24210
  strategyName: data.strategyName,
@@ -24414,7 +24727,7 @@ class WalkerReportService {
24414
24727
  this.tick = async (data) => {
24415
24728
  this.loggerService.log(WALKER_REPORT_METHOD_NAME_TICK, { data });
24416
24729
  await Report.writeData("walker", {
24417
- timestamp: Date.now(),
24730
+ timestamp: getContextTimestamp(),
24418
24731
  walkerName: data.walkerName,
24419
24732
  symbol: data.symbol,
24420
24733
  exchangeName: data.exchangeName,
@@ -24541,7 +24854,7 @@ class HeatReportService {
24541
24854
  return;
24542
24855
  }
24543
24856
  await Report.writeData("heat", {
24544
- timestamp: Date.now(),
24857
+ timestamp: getContextTimestamp(),
24545
24858
  action: data.action,
24546
24859
  symbol: data.symbol,
24547
24860
  strategyName: data.strategyName,
@@ -25863,7 +26176,7 @@ class ReportStorage {
25863
26176
  */
25864
26177
  async dump(symbol, strategyName, path = "./dump/strategy", columns = COLUMN_CONFIG.strategy_columns) {
25865
26178
  const markdown = await this.getReport(symbol, strategyName, columns);
25866
- const timestamp = Date.now();
26179
+ const timestamp = getContextTimestamp();
25867
26180
  const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
25868
26181
  await Markdown.writeData("strategy", markdown, {
25869
26182
  path,
@@ -28179,6 +28492,7 @@ const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
28179
28492
  const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
28180
28493
  const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
28181
28494
  const GET_DATE_METHOD_NAME = "exchange.getDate";
28495
+ const GET_TIMESTAMP_METHOD_NAME = "exchange.getTimestamp";
28182
28496
  const GET_MODE_METHOD_NAME = "exchange.getMode";
28183
28497
  const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
28184
28498
  const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
@@ -28344,6 +28658,26 @@ async function getDate() {
28344
28658
  const { when } = bt.executionContextService.context;
28345
28659
  return new Date(when.getTime());
28346
28660
  }
28661
+ /**
28662
+ * Gets the current timestamp from execution context.
28663
+ *
28664
+ * In backtest mode: returns the current timeframe timestamp being processed
28665
+ * In live mode: returns current real-time timestamp
28666
+ *
28667
+ * @returns Promise resolving to current execution context timestamp in milliseconds
28668
+ * @example
28669
+ * ```typescript
28670
+ * const timestamp = await getTimestamp();
28671
+ * console.log(timestamp); // 1700000000000
28672
+ * ```
28673
+ */
28674
+ async function getTimestamp() {
28675
+ bt.loggerService.info(GET_TIMESTAMP_METHOD_NAME);
28676
+ if (!ExecutionContextService.hasContext()) {
28677
+ throw new Error("getTimestamp requires an execution context");
28678
+ }
28679
+ return getContextTimestamp();
28680
+ }
28347
28681
  /**
28348
28682
  * Gets the current execution mode.
28349
28683
  *
@@ -28560,6 +28894,11 @@ const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
28560
28894
  const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
28561
28895
  const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
28562
28896
  const AVERAGE_BUY_METHOD_NAME = "strategy.commitAverageBuy";
28897
+ const GET_TOTAL_PERCENT_CLOSED_METHOD_NAME = "strategy.getTotalPercentClosed";
28898
+ const GET_TOTAL_COST_CLOSED_METHOD_NAME = "strategy.getTotalCostClosed";
28899
+ const GET_PENDING_SIGNAL_METHOD_NAME = "strategy.getPendingSignal";
28900
+ const GET_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.getScheduledSignal";
28901
+ const GET_BREAKEVEN_METHOD_NAME = "strategy.getBreakeven";
28563
28902
  /**
28564
28903
  * Cancels the scheduled signal without stopping the strategy.
28565
28904
  *
@@ -28951,6 +29290,173 @@ async function commitAverageBuy(symbol) {
28951
29290
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
28952
29291
  return await bt.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
28953
29292
  }
29293
+ /**
29294
+ * Returns the percentage of the position currently held (not closed).
29295
+ * 100 = nothing has been closed (full position), 0 = fully closed.
29296
+ * Correctly accounts for DCA entries between partial closes.
29297
+ *
29298
+ * Automatically detects backtest/live mode from execution context.
29299
+ *
29300
+ * @param symbol - Trading pair symbol
29301
+ * @returns Promise<number> - held percentage (0–100)
29302
+ *
29303
+ * @example
29304
+ * ```typescript
29305
+ * import { getTotalPercentClosed } from "backtest-kit";
29306
+ *
29307
+ * const heldPct = await getTotalPercentClosed("BTCUSDT");
29308
+ * console.log(`Holding ${heldPct}% of position`);
29309
+ * ```
29310
+ */
29311
+ async function getTotalPercentClosed(symbol) {
29312
+ bt.loggerService.info(GET_TOTAL_PERCENT_CLOSED_METHOD_NAME, {
29313
+ symbol,
29314
+ });
29315
+ if (!ExecutionContextService.hasContext()) {
29316
+ throw new Error("getTotalPercentClosed requires an execution context");
29317
+ }
29318
+ if (!MethodContextService.hasContext()) {
29319
+ throw new Error("getTotalPercentClosed requires a method context");
29320
+ }
29321
+ const { backtest: isBacktest } = bt.executionContextService.context;
29322
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29323
+ return await bt.strategyCoreService.getTotalPercentClosed(isBacktest, symbol, { exchangeName, frameName, strategyName });
29324
+ }
29325
+ /**
29326
+ * Returns the cost basis in dollars of the position currently held (not closed).
29327
+ * Correctly accounts for DCA entries between partial closes.
29328
+ *
29329
+ * Automatically detects backtest/live mode from execution context.
29330
+ *
29331
+ * @param symbol - Trading pair symbol
29332
+ * @returns Promise<number> - held cost basis in dollars
29333
+ *
29334
+ * @example
29335
+ * ```typescript
29336
+ * import { getTotalCostClosed } from "backtest-kit";
29337
+ *
29338
+ * const heldCost = await getTotalCostClosed("BTCUSDT");
29339
+ * console.log(`Holding $${heldCost} of position`);
29340
+ * ```
29341
+ */
29342
+ async function getTotalCostClosed(symbol) {
29343
+ bt.loggerService.info(GET_TOTAL_COST_CLOSED_METHOD_NAME, {
29344
+ symbol,
29345
+ });
29346
+ if (!ExecutionContextService.hasContext()) {
29347
+ throw new Error("getTotalCostClosed requires an execution context");
29348
+ }
29349
+ if (!MethodContextService.hasContext()) {
29350
+ throw new Error("getTotalCostClosed requires a method context");
29351
+ }
29352
+ const { backtest: isBacktest } = bt.executionContextService.context;
29353
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29354
+ return await bt.strategyCoreService.getTotalCostClosed(isBacktest, symbol, { exchangeName, frameName, strategyName });
29355
+ }
29356
+ /**
29357
+ * Returns the currently active pending signal for the strategy.
29358
+ * If no active signal exists, returns null.
29359
+ *
29360
+ * Automatically detects backtest/live mode from execution context.
29361
+ *
29362
+ * @param symbol - Trading pair symbol
29363
+ * @returns Promise resolving to pending signal or null
29364
+ *
29365
+ * @example
29366
+ * ```typescript
29367
+ * import { getPendingSignal } from "backtest-kit";
29368
+ *
29369
+ * const pending = await getPendingSignal("BTCUSDT");
29370
+ * if (pending) {
29371
+ * console.log("Active signal:", pending.id);
29372
+ * }
29373
+ * ```
29374
+ */
29375
+ async function getPendingSignal(symbol) {
29376
+ bt.loggerService.info(GET_PENDING_SIGNAL_METHOD_NAME, {
29377
+ symbol,
29378
+ });
29379
+ if (!ExecutionContextService.hasContext()) {
29380
+ throw new Error("getPendingSignal requires an execution context");
29381
+ }
29382
+ if (!MethodContextService.hasContext()) {
29383
+ throw new Error("getPendingSignal requires a method context");
29384
+ }
29385
+ const { backtest: isBacktest } = bt.executionContextService.context;
29386
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29387
+ return await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, { exchangeName, frameName, strategyName });
29388
+ }
29389
+ /**
29390
+ * Returns the currently active scheduled signal for the strategy.
29391
+ * If no scheduled signal exists, returns null.
29392
+ *
29393
+ * Automatically detects backtest/live mode from execution context.
29394
+ *
29395
+ * @param symbol - Trading pair symbol
29396
+ * @returns Promise resolving to scheduled signal or null
29397
+ *
29398
+ * @example
29399
+ * ```typescript
29400
+ * import { getScheduledSignal } from "backtest-kit";
29401
+ *
29402
+ * const scheduled = await getScheduledSignal("BTCUSDT");
29403
+ * if (scheduled) {
29404
+ * console.log("Scheduled signal:", scheduled.id);
29405
+ * }
29406
+ * ```
29407
+ */
29408
+ async function getScheduledSignal(symbol) {
29409
+ bt.loggerService.info(GET_SCHEDULED_SIGNAL_METHOD_NAME, {
29410
+ symbol,
29411
+ });
29412
+ if (!ExecutionContextService.hasContext()) {
29413
+ throw new Error("getScheduledSignal requires an execution context");
29414
+ }
29415
+ if (!MethodContextService.hasContext()) {
29416
+ throw new Error("getScheduledSignal requires a method context");
29417
+ }
29418
+ const { backtest: isBacktest } = bt.executionContextService.context;
29419
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29420
+ return await bt.strategyCoreService.getScheduledSignal(isBacktest, symbol, { exchangeName, frameName, strategyName });
29421
+ }
29422
+ /**
29423
+ * Checks if breakeven threshold has been reached for the current pending signal.
29424
+ *
29425
+ * Returns true if price has moved far enough in profit direction to cover
29426
+ * transaction costs. Threshold is calculated as: (CC_PERCENT_SLIPPAGE + CC_PERCENT_FEE) * 2
29427
+ *
29428
+ * Automatically detects backtest/live mode from execution context.
29429
+ *
29430
+ * @param symbol - Trading pair symbol
29431
+ * @param currentPrice - Current market price to check against threshold
29432
+ * @returns Promise<boolean> - true if breakeven threshold reached, false otherwise
29433
+ *
29434
+ * @example
29435
+ * ```typescript
29436
+ * import { getBreakeven, getAveragePrice } from "backtest-kit";
29437
+ *
29438
+ * const price = await getAveragePrice("BTCUSDT");
29439
+ * const canBreakeven = await getBreakeven("BTCUSDT", price);
29440
+ * if (canBreakeven) {
29441
+ * console.log("Breakeven available");
29442
+ * }
29443
+ * ```
29444
+ */
29445
+ async function getBreakeven(symbol, currentPrice) {
29446
+ bt.loggerService.info(GET_BREAKEVEN_METHOD_NAME, {
29447
+ symbol,
29448
+ currentPrice,
29449
+ });
29450
+ if (!ExecutionContextService.hasContext()) {
29451
+ throw new Error("getBreakeven requires an execution context");
29452
+ }
29453
+ if (!MethodContextService.hasContext()) {
29454
+ throw new Error("getBreakeven requires a method context");
29455
+ }
29456
+ const { backtest: isBacktest } = bt.executionContextService.context;
29457
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29458
+ return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
29459
+ }
28954
29460
 
28955
29461
  const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
28956
29462
  /**
@@ -30138,6 +30644,8 @@ const BACKTEST_METHOD_NAME_DUMP = "BacktestUtils.dump";
30138
30644
  const BACKTEST_METHOD_NAME_TASK = "BacktestUtils.task";
30139
30645
  const BACKTEST_METHOD_NAME_GET_STATUS = "BacktestUtils.getStatus";
30140
30646
  const BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL = "BacktestUtils.getPendingSignal";
30647
+ const BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "BacktestUtils.getTotalPercentClosed";
30648
+ const BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED = "BacktestUtils.getTotalCostClosed";
30141
30649
  const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
30142
30650
  const BACKTEST_METHOD_NAME_GET_BREAKEVEN = "BacktestUtils.getBreakeven";
30143
30651
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
@@ -30539,6 +31047,71 @@ class BacktestUtils {
30539
31047
  }
30540
31048
  return await bt.strategyCoreService.getPendingSignal(true, symbol, context);
30541
31049
  };
31050
+ /**
31051
+ * Returns the percentage of the position currently held (not closed).
31052
+ * 100 = nothing has been closed (full position), 0 = fully closed.
31053
+ * Correctly accounts for DCA entries between partial closes.
31054
+ *
31055
+ * @param symbol - Trading pair symbol
31056
+ * @param context - Context with strategyName, exchangeName, frameName
31057
+ * @returns Promise<number> - held percentage (0–100)
31058
+ *
31059
+ * @example
31060
+ * ```typescript
31061
+ * const heldPct = await Backtest.getTotalPercentClosed("BTCUSDT", { strategyName, exchangeName, frameName });
31062
+ * console.log(`Holding ${heldPct}% of position`);
31063
+ * ```
31064
+ */
31065
+ this.getTotalPercentClosed = async (symbol, context) => {
31066
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED, {
31067
+ symbol,
31068
+ context,
31069
+ });
31070
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
31071
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
31072
+ {
31073
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31074
+ riskName &&
31075
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
31076
+ riskList &&
31077
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
31078
+ actions &&
31079
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
31080
+ }
31081
+ return await bt.strategyCoreService.getTotalPercentClosed(true, symbol, context);
31082
+ };
31083
+ /**
31084
+ * Returns the cost basis in dollars of the position currently held (not closed).
31085
+ * Correctly accounts for DCA entries between partial closes.
31086
+ *
31087
+ * @param symbol - Trading pair symbol
31088
+ * @param context - Context with strategyName, exchangeName, frameName
31089
+ * @returns Promise<number> - held cost basis in dollars
31090
+ *
31091
+ * @example
31092
+ * ```typescript
31093
+ * const heldCost = await Backtest.getTotalCostClosed("BTCUSDT", { strategyName, exchangeName, frameName });
31094
+ * console.log(`Holding $${heldCost} of position`);
31095
+ * ```
31096
+ */
31097
+ this.getTotalCostClosed = async (symbol, context) => {
31098
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED, {
31099
+ symbol,
31100
+ context,
31101
+ });
31102
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
31103
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
31104
+ {
31105
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31106
+ riskName &&
31107
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
31108
+ riskList &&
31109
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED));
31110
+ actions &&
31111
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED));
31112
+ }
31113
+ return await bt.strategyCoreService.getTotalCostClosed(true, symbol, context);
31114
+ };
30542
31115
  /**
30543
31116
  * Retrieves the currently active scheduled signal for the strategy.
30544
31117
  * If no scheduled signal exists, returns null.
@@ -31254,6 +31827,8 @@ const LIVE_METHOD_NAME_DUMP = "LiveUtils.dump";
31254
31827
  const LIVE_METHOD_NAME_TASK = "LiveUtils.task";
31255
31828
  const LIVE_METHOD_NAME_GET_STATUS = "LiveUtils.getStatus";
31256
31829
  const LIVE_METHOD_NAME_GET_PENDING_SIGNAL = "LiveUtils.getPendingSignal";
31830
+ const LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "LiveUtils.getTotalPercentClosed";
31831
+ const LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED = "LiveUtils.getTotalCostClosed";
31257
31832
  const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
31258
31833
  const LIVE_METHOD_NAME_GET_BREAKEVEN = "LiveUtils.getBreakeven";
31259
31834
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
@@ -31624,6 +32199,73 @@ class LiveUtils {
31624
32199
  frameName: "",
31625
32200
  });
31626
32201
  };
32202
+ /**
32203
+ * Returns the percentage of the position currently held (not closed).
32204
+ * 100 = nothing has been closed (full position), 0 = fully closed.
32205
+ * Correctly accounts for DCA entries between partial closes.
32206
+ *
32207
+ * @param symbol - Trading pair symbol
32208
+ * @param context - Context with strategyName and exchangeName
32209
+ * @returns Promise<number> - held percentage (0–100)
32210
+ *
32211
+ * @example
32212
+ * ```typescript
32213
+ * const heldPct = await Live.getTotalPercentClosed("BTCUSDT", { strategyName, exchangeName });
32214
+ * console.log(`Holding ${heldPct}% of position`);
32215
+ * ```
32216
+ */
32217
+ this.getTotalPercentClosed = async (symbol, context) => {
32218
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED, {
32219
+ symbol,
32220
+ context,
32221
+ });
32222
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
32223
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
32224
+ {
32225
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
32226
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
32227
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
32228
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
32229
+ }
32230
+ return await bt.strategyCoreService.getTotalPercentClosed(false, symbol, {
32231
+ strategyName: context.strategyName,
32232
+ exchangeName: context.exchangeName,
32233
+ frameName: "",
32234
+ });
32235
+ };
32236
+ /**
32237
+ * Returns the cost basis in dollars of the position currently held (not closed).
32238
+ * Correctly accounts for DCA entries between partial closes.
32239
+ *
32240
+ * @param symbol - Trading pair symbol
32241
+ * @param context - Context with strategyName and exchangeName
32242
+ * @returns Promise<number> - held cost basis in dollars
32243
+ *
32244
+ * @example
32245
+ * ```typescript
32246
+ * const heldCost = await Live.getTotalCostClosed("BTCUSDT", { strategyName, exchangeName });
32247
+ * console.log(`Holding $${heldCost} of position`);
32248
+ * ```
32249
+ */
32250
+ this.getTotalCostClosed = async (symbol, context) => {
32251
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED, {
32252
+ symbol,
32253
+ context,
32254
+ });
32255
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
32256
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
32257
+ {
32258
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
32259
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
32260
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
32261
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
32262
+ }
32263
+ return await bt.strategyCoreService.getTotalCostClosed(false, symbol, {
32264
+ strategyName: context.strategyName,
32265
+ exchangeName: context.exchangeName,
32266
+ frameName: "",
32267
+ });
32268
+ };
31627
32269
  /**
31628
32270
  * Retrieves the currently active scheduled signal for the strategy.
31629
32271
  * If no scheduled signal exists, returns null.
@@ -32541,7 +33183,6 @@ const ADD_ACTION_METHOD_NAME = "add.addActionSchema";
32541
33183
  * priceTakeProfit: 51000,
32542
33184
  * priceStopLoss: 49000,
32543
33185
  * minuteEstimatedTime: 60,
32544
- * timestamp: Date.now(),
32545
33186
  * }),
32546
33187
  * callbacks: {
32547
33188
  * onOpen: (symbol, signal, currentPrice, backtest) => console.log("Signal opened"),
@@ -33468,8 +34109,8 @@ const WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
33468
34109
  const WRITE_SAFE_SYMBOL = Symbol("write-safe");
33469
34110
  /**
33470
34111
  * Backtest execution time retrieval function.
33471
- * Returns the 'when' timestamp from the execution context if available, otherwise returns the current time.
33472
- * This allows log entries to be timestamped according to the backtest timeline rather than real-world time, improving log relevance and user experience during backtest analysis.
34112
+ * Returns the 'when' priority from the execution context if available, otherwise returns the current time.
34113
+ * This allows log entries to be priorityed according to the backtest timeline rather than real-world time, improving log relevance and user experience during backtest analysis.
33473
34114
  */
33474
34115
  const GET_DATE_FN = () => {
33475
34116
  if (ExecutionContextService.hasContext()) {
@@ -33491,7 +34132,7 @@ const GET_METHOD_CONTEXT_FN = () => {
33491
34132
  /**
33492
34133
  * Execution context retrieval function.
33493
34134
  * Returns the current execution context from ExecutionContextService if available, otherwise returns null.
33494
- * This allows log entries to include contextual information about the symbol, timestamp, and backtest mode associated with the log event, providing additional insights into the execution environment when analyzing logs.
34135
+ * This allows log entries to include contextual information about the symbol, priority, and backtest mode associated with the log event, providing additional insights into the execution environment when analyzing logs.
33495
34136
  */
33496
34137
  const GET_EXECUTION_CONTEXT_FN = () => {
33497
34138
  if (ExecutionContextService.hasContext()) {
@@ -33521,7 +34162,7 @@ class LogPersistUtils {
33521
34162
  this.waitForInit = singleshot(async () => {
33522
34163
  bt.loggerService.info(LOG_PERSIST_METHOD_NAME_WAIT_FOR_INIT);
33523
34164
  const list = await PersistLogAdapter.readLogData();
33524
- list.sort((a, b) => a.timestamp - b.timestamp);
34165
+ list.sort((a, b) => a.priority - b.priority);
33525
34166
  this._entries = list.slice(-GLOBAL_CONFIG.CC_MAX_LOG_LINES);
33526
34167
  });
33527
34168
  /**
@@ -33537,7 +34178,8 @@ class LogPersistUtils {
33537
34178
  this._entries.push({
33538
34179
  id: randomString(),
33539
34180
  type: "log",
33540
- timestamp: Date.now(),
34181
+ priority: Date.now(),
34182
+ timestamp: getContextTimestamp(),
33541
34183
  createdAt: date.toISOString(),
33542
34184
  methodContext: GET_METHOD_CONTEXT_FN(),
33543
34185
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33560,7 +34202,8 @@ class LogPersistUtils {
33560
34202
  this._entries.push({
33561
34203
  id: randomString(),
33562
34204
  type: "debug",
33563
- timestamp: Date.now(),
34205
+ priority: Date.now(),
34206
+ timestamp: getContextTimestamp(),
33564
34207
  createdAt: date.toISOString(),
33565
34208
  methodContext: GET_METHOD_CONTEXT_FN(),
33566
34209
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33583,7 +34226,8 @@ class LogPersistUtils {
33583
34226
  this._entries.push({
33584
34227
  id: randomString(),
33585
34228
  type: "info",
33586
- timestamp: Date.now(),
34229
+ priority: Date.now(),
34230
+ timestamp: getContextTimestamp(),
33587
34231
  createdAt: date.toISOString(),
33588
34232
  methodContext: GET_METHOD_CONTEXT_FN(),
33589
34233
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33606,7 +34250,8 @@ class LogPersistUtils {
33606
34250
  this._entries.push({
33607
34251
  id: randomString(),
33608
34252
  type: "warn",
33609
- timestamp: Date.now(),
34253
+ priority: Date.now(),
34254
+ timestamp: getContextTimestamp(),
33610
34255
  createdAt: date.toISOString(),
33611
34256
  methodContext: GET_METHOD_CONTEXT_FN(),
33612
34257
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33662,7 +34307,8 @@ class LogMemoryUtils {
33662
34307
  this._entries.push({
33663
34308
  id: randomString(),
33664
34309
  type: "log",
33665
- timestamp: Date.now(),
34310
+ priority: Date.now(),
34311
+ timestamp: getContextTimestamp(),
33666
34312
  createdAt: date.toISOString(),
33667
34313
  methodContext: GET_METHOD_CONTEXT_FN(),
33668
34314
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33683,7 +34329,8 @@ class LogMemoryUtils {
33683
34329
  this._entries.push({
33684
34330
  id: randomString(),
33685
34331
  type: "debug",
33686
- timestamp: Date.now(),
34332
+ priority: Date.now(),
34333
+ timestamp: getContextTimestamp(),
33687
34334
  createdAt: date.toISOString(),
33688
34335
  methodContext: GET_METHOD_CONTEXT_FN(),
33689
34336
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33704,7 +34351,8 @@ class LogMemoryUtils {
33704
34351
  this._entries.push({
33705
34352
  id: randomString(),
33706
34353
  type: "info",
33707
- timestamp: Date.now(),
34354
+ priority: Date.now(),
34355
+ timestamp: getContextTimestamp(),
33708
34356
  createdAt: date.toISOString(),
33709
34357
  methodContext: GET_METHOD_CONTEXT_FN(),
33710
34358
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33725,7 +34373,8 @@ class LogMemoryUtils {
33725
34373
  this._entries.push({
33726
34374
  id: randomString(),
33727
34375
  type: "warn",
33728
- timestamp: Date.now(),
34376
+ priority: Date.now(),
34377
+ timestamp: getContextTimestamp(),
33729
34378
  createdAt: date.toISOString(),
33730
34379
  methodContext: GET_METHOD_CONTEXT_FN(),
33731
34380
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33839,7 +34488,8 @@ class LogJsonlUtils {
33839
34488
  await this._append({
33840
34489
  id: randomString(),
33841
34490
  type: "log",
33842
- timestamp: Date.now(),
34491
+ priority: Date.now(),
34492
+ timestamp: getContextTimestamp(),
33843
34493
  createdAt: date.toISOString(),
33844
34494
  methodContext: GET_METHOD_CONTEXT_FN(),
33845
34495
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33858,7 +34508,8 @@ class LogJsonlUtils {
33858
34508
  await this._append({
33859
34509
  id: randomString(),
33860
34510
  type: "debug",
33861
- timestamp: Date.now(),
34511
+ priority: Date.now(),
34512
+ timestamp: getContextTimestamp(),
33862
34513
  createdAt: date.toISOString(),
33863
34514
  methodContext: GET_METHOD_CONTEXT_FN(),
33864
34515
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33877,7 +34528,8 @@ class LogJsonlUtils {
33877
34528
  await this._append({
33878
34529
  id: randomString(),
33879
34530
  type: "info",
33880
- timestamp: Date.now(),
34531
+ priority: Date.now(),
34532
+ timestamp: getContextTimestamp(),
33881
34533
  createdAt: date.toISOString(),
33882
34534
  methodContext: GET_METHOD_CONTEXT_FN(),
33883
34535
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33896,7 +34548,8 @@ class LogJsonlUtils {
33896
34548
  await this._append({
33897
34549
  id: randomString(),
33898
34550
  type: "warn",
33899
- timestamp: Date.now(),
34551
+ priority: Date.now(),
34552
+ timestamp: getContextTimestamp(),
33900
34553
  createdAt: date.toISOString(),
33901
34554
  methodContext: GET_METHOD_CONTEXT_FN(),
33902
34555
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -38323,6 +38976,8 @@ const CACHE_METHOD_NAME_CLEAR = "CacheInstance.clear";
38323
38976
  const CACHE_METHOD_NAME_RUN = "CacheInstance.run";
38324
38977
  const CACHE_METHOD_NAME_GC = "CacheInstance.gc";
38325
38978
  const CACHE_METHOD_NAME_FN = "CacheUtils.fn";
38979
+ const CACHE_METHOD_NAME_FILE = "CacheUtils.file";
38980
+ const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
38326
38981
  const MS_PER_MINUTE$1 = 60000;
38327
38982
  const INTERVAL_MINUTES$1 = {
38328
38983
  "1m": 1,
@@ -38544,6 +39199,95 @@ class CacheInstance {
38544
39199
  };
38545
39200
  }
38546
39201
  }
39202
+ /**
39203
+ * Instance class for caching async function results in persistent file storage.
39204
+ *
39205
+ * Provides automatic cache invalidation based on candle intervals.
39206
+ * Cache key = `${alignedTimestamp}_${symbol}` (bucket) + dynamic key (file within bucket).
39207
+ * On cache hit reads from disk, on miss calls the function and writes the result.
39208
+ *
39209
+ * @template T - Async function type to cache
39210
+ * @template K - Dynamic key type
39211
+ *
39212
+ * @example
39213
+ * ```typescript
39214
+ * const instance = new CacheFileInstance(fetchFromApi, "1h");
39215
+ * const result = await instance.run("BTCUSDT", extraArg);
39216
+ * ```
39217
+ */
39218
+ class CacheFileInstance {
39219
+ /**
39220
+ * Allocates a new unique index. Called once in the constructor to give each
39221
+ * CacheFileInstance its own namespace in the persistent key space.
39222
+ */
39223
+ static createIndex() {
39224
+ return CacheFileInstance._indexCounter++;
39225
+ }
39226
+ /**
39227
+ * Creates a new CacheFileInstance.
39228
+ *
39229
+ * @param fn - Async function to cache
39230
+ * @param interval - Candle interval for cache invalidation
39231
+ * @param name - Human-readable bucket name used as the directory key (replaces symbol in bucket path)
39232
+ * @param key - Dynamic key generator; receives all args, must return a string.
39233
+ * Default: `([symbol, alignMs]) => \`${symbol}_${alignMs}\``
39234
+ */
39235
+ constructor(fn, interval, name, key = ([symbol, alignMs]) => `${symbol}_${alignMs}`) {
39236
+ this.fn = fn;
39237
+ this.interval = interval;
39238
+ this.name = name;
39239
+ this.key = key;
39240
+ /**
39241
+ * Execute async function with persistent file caching.
39242
+ *
39243
+ * Algorithm:
39244
+ * 1. Build bucket = `${name}_${interval}_${index}` — fixed per instance, used as directory name
39245
+ * 2. Align execution context `when` to interval boundary → `alignedTs`
39246
+ * 3. Build entity key from the key generator (receives `[symbol, alignedTs, ...rest]`)
39247
+ * 4. Try to read from PersistMeasureAdapter using (bucket, entityKey)
39248
+ * 5. On hit — return cached value
39249
+ * 6. On miss — call fn, write result to disk, return result
39250
+ *
39251
+ * Cache invalidation happens through the entity key: the default key embeds `alignedTs`,
39252
+ * so each new candle interval produces a new file name while the bucket directory stays the same.
39253
+ *
39254
+ * Requires active execution context (symbol, when) and method context.
39255
+ *
39256
+ * @param args - Arguments forwarded to the wrapped function
39257
+ * @returns Cached or freshly computed result
39258
+ */
39259
+ this.run = async (...args) => {
39260
+ bt.loggerService.debug(CACHE_FILE_INSTANCE_METHOD_NAME_RUN, { args });
39261
+ const step = INTERVAL_MINUTES$1[this.interval];
39262
+ {
39263
+ if (!MethodContextService.hasContext()) {
39264
+ throw new Error("CacheFileInstance run requires method context");
39265
+ }
39266
+ if (!ExecutionContextService.hasContext()) {
39267
+ throw new Error("CacheFileInstance run requires execution context");
39268
+ }
39269
+ if (!step) {
39270
+ throw new Error(`CacheFileInstance unknown cache ttl interval=${this.interval}`);
39271
+ }
39272
+ }
39273
+ const [symbol, ...rest] = args;
39274
+ const { when } = bt.executionContextService.context;
39275
+ const alignedTs = align(when.getTime(), this.interval);
39276
+ const bucket = `${this.name}_${this.interval}_${this.index}`;
39277
+ const entityKey = this.key([symbol, alignedTs, ...rest]);
39278
+ const cached = await PersistMeasureAdapter.readMeasureData(bucket, entityKey);
39279
+ if (cached !== null) {
39280
+ return cached;
39281
+ }
39282
+ const result = await this.fn.call(null, ...args);
39283
+ await PersistMeasureAdapter.writeMeasureData(result, bucket, entityKey);
39284
+ return result;
39285
+ };
39286
+ this.index = CacheFileInstance.createIndex();
39287
+ }
39288
+ }
39289
+ /** Global counter — incremented once per CacheFileInstance construction */
39290
+ CacheFileInstance._indexCounter = 0;
38547
39291
  /**
38548
39292
  * Utility class for function caching with timeframe-based invalidation.
38549
39293
  *
@@ -38565,7 +39309,12 @@ class CacheUtils {
38565
39309
  * Memoized function to get or create CacheInstance for a function.
38566
39310
  * Each function gets its own isolated cache instance.
38567
39311
  */
38568
- this._getInstance = memoize(([run]) => run, (run, interval, key) => new CacheInstance(run, interval, key));
39312
+ this._getFnInstance = memoize(([run]) => run, (run, interval, key) => new CacheInstance(run, interval, key));
39313
+ /**
39314
+ * Memoized function to get or create CacheFileInstance for an async function.
39315
+ * Each function gets its own isolated file-cache instance.
39316
+ */
39317
+ this._getFileInstance = memoize(([run]) => run, (run, interval, name, key) => new CacheFileInstance(run, interval, name, key));
38569
39318
  /**
38570
39319
  * Wrap a function with caching based on timeframe intervals.
38571
39320
  *
@@ -38604,11 +39353,57 @@ class CacheUtils {
38604
39353
  context,
38605
39354
  });
38606
39355
  const wrappedFn = (...args) => {
38607
- const instance = this._getInstance(run, context.interval, context.key);
39356
+ const instance = this._getFnInstance(run, context.interval, context.key);
38608
39357
  return instance.run(...args).value;
38609
39358
  };
38610
39359
  return wrappedFn;
38611
39360
  };
39361
+ /**
39362
+ * Wrap an async function with persistent file-based caching.
39363
+ *
39364
+ * Returns a wrapped version of the function that reads from disk on cache hit
39365
+ * and writes the result to disk on cache miss. Files are stored under
39366
+ * `./dump/data/measure/{name}_{interval}_{index}/`.
39367
+ *
39368
+ * The `run` function reference is used as the memoization key for the underlying
39369
+ * `CacheFileInstance`, so each unique function reference gets its own isolated instance.
39370
+ * Pass the same function reference each time to reuse the same cache.
39371
+ *
39372
+ * @template T - Async function type to cache
39373
+ * @param run - Async function to wrap with file caching
39374
+ * @param context.interval - Candle interval for cache invalidation
39375
+ * @param context.name - Human-readable bucket name; becomes the directory prefix
39376
+ * @param context.key - Optional entity key generator. Receives `[symbol, alignMs, ...rest]`
39377
+ * where `alignMs` is the timestamp aligned to `interval`.
39378
+ * Default: `([symbol, alignMs]) => \`${symbol}_${alignMs}\``
39379
+ * @returns Wrapped async function with automatic persistent caching
39380
+ *
39381
+ * @example
39382
+ * ```typescript
39383
+ * const fetchData = async (symbol: string, period: number) => {
39384
+ * return await externalApi.fetch(symbol, period);
39385
+ * };
39386
+ *
39387
+ * // Default key — one cache file per symbol per aligned candle
39388
+ * const cachedFetch = Cache.file(fetchData, { interval: "1h", name: "fetchData" });
39389
+ *
39390
+ * // Custom key — one cache file per symbol + period combination
39391
+ * const cachedFetch = Cache.file(fetchData, {
39392
+ * interval: "1h",
39393
+ * name: "fetchData",
39394
+ * key: ([symbol, alignMs, period]) => `${symbol}_${alignMs}_${period}`,
39395
+ * });
39396
+ * const result = await cachedFetch("BTCUSDT", 14);
39397
+ * ```
39398
+ */
39399
+ this.file = (run, context) => {
39400
+ bt.loggerService.info(CACHE_METHOD_NAME_FILE, { context });
39401
+ const wrappedFn = (...args) => {
39402
+ const instance = this._getFileInstance(run, context.interval, context.name, context.key);
39403
+ return instance.run(...args);
39404
+ };
39405
+ return wrappedFn;
39406
+ };
38612
39407
  /**
38613
39408
  * Flush (remove) cached CacheInstance for a specific function or all functions.
38614
39409
  *
@@ -38642,7 +39437,7 @@ class CacheUtils {
38642
39437
  bt.loggerService.info(CACHE_METHOD_NAME_FLUSH, {
38643
39438
  run,
38644
39439
  });
38645
- this._getInstance.clear(run);
39440
+ this._getFnInstance.clear(run);
38646
39441
  };
38647
39442
  /**
38648
39443
  * Clear cached value for current execution context of a specific function.
@@ -38685,7 +39480,7 @@ class CacheUtils {
38685
39480
  console.warn(`${CACHE_METHOD_NAME_CLEAR} called without execution context, skipping clear`);
38686
39481
  return;
38687
39482
  }
38688
- this._getInstance.get(run).clear();
39483
+ this._getFnInstance.get(run).clear();
38689
39484
  };
38690
39485
  /**
38691
39486
  * Garbage collect expired cache entries for a specific function.
@@ -38717,7 +39512,7 @@ class CacheUtils {
38717
39512
  console.warn(`${CACHE_METHOD_NAME_GC} called without execution context, skipping garbage collection`);
38718
39513
  return;
38719
39514
  }
38720
- return this._getInstance.get(run).gc();
39515
+ return this._getFnInstance.get(run).gc();
38721
39516
  };
38722
39517
  }
38723
39518
  }
@@ -39287,4 +40082,4 @@ const set = (object, path, value) => {
39287
40082
  }
39288
40083
  };
39289
40084
 
39290
- export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getRawCandles, getRiskSchema, getSizingSchema, getStrategySchema, getSymbol, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, shutdown, stopStrategy, validate, waitForCandle, warmCandles };
40085
+ export { ActionBase, Backtest, Breakeven, Cache, Constant, Exchange, ExecutionContextService, Heat, Live, Log, Markdown, MarkdownFileBase, MarkdownFolderBase, MethodContextService, Notification, NotificationBacktest, NotificationLive, Partial, Performance, PersistBase, PersistBreakevenAdapter, PersistCandleAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistNotificationAdapter, PersistPartialAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistSignalAdapter, PersistStorageAdapter, PositionSize, Report, ReportBase, Risk, Schedule, Storage, StorageBacktest, StorageLive, Strategy, Walker, addActionSchema, addExchangeSchema, addFrameSchema, addRiskSchema, addSizingSchema, addStrategySchema, addWalkerSchema, alignToInterval, checkCandles, commitActivateScheduled, commitAverageBuy, commitBreakeven, commitCancelScheduled, commitClosePending, commitPartialLoss, commitPartialProfit, commitTrailingStop, commitTrailingTake, dumpMessages, emitters, formatPrice, formatQuantity, get, getActionSchema, getAggregatedTrades, getAveragePrice, getBacktestTimeframe, getBreakeven, getCandles, getColumns, getConfig, getContext, getDate, getDefaultColumns, getDefaultConfig, getEffectivePriceOpen, getExchangeSchema, getFrameSchema, getMode, getNextCandles, getOrderBook, getPendingSignal, getRawCandles, getRiskSchema, getScheduledSignal, getSizingSchema, getStrategySchema, getSymbol, getTimestamp, getTotalClosed, getTotalCostClosed, getTotalPercentClosed, getWalkerSchema, hasTradeContext, backtest as lib, listExchangeSchema, listFrameSchema, listRiskSchema, listSizingSchema, listStrategySchema, listWalkerSchema, listenActivePing, listenActivePingOnce, listenBacktestProgress, listenBreakevenAvailable, listenBreakevenAvailableOnce, listenDoneBacktest, listenDoneBacktestOnce, listenDoneLive, listenDoneLiveOnce, listenDoneWalker, listenDoneWalkerOnce, listenError, listenExit, listenPartialLossAvailable, listenPartialLossAvailableOnce, listenPartialProfitAvailable, listenPartialProfitAvailableOnce, listenPerformance, listenRisk, listenRiskOnce, listenSchedulePing, listenSchedulePingOnce, listenSignal, listenSignalBacktest, listenSignalBacktestOnce, listenSignalLive, listenSignalLiveOnce, listenSignalOnce, listenStrategyCommit, listenStrategyCommitOnce, listenValidation, listenWalker, listenWalkerComplete, listenWalkerOnce, listenWalkerProgress, overrideActionSchema, overrideExchangeSchema, overrideFrameSchema, overrideRiskSchema, overrideSizingSchema, overrideStrategySchema, overrideWalkerSchema, parseArgs, roundTicks, set, setColumns, setConfig, setLogger, shutdown, stopStrategy, toProfitLossDto, validate, waitForCandle, warmCandles };