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.cjs CHANGED
@@ -463,6 +463,14 @@ const GLOBAL_CONFIG = {
463
463
  * Default: true (mutex locking enabled for candle fetching)
464
464
  */
465
465
  CC_ENABLE_CANDLE_FETCH_MUTEX: true,
466
+ /**
467
+ * Enables DCA (Dollar-Cost Averaging) logic even if antirecord is not broken.
468
+ * Allows to commitAverageBuy if currentPrice is not the lowest price since entry, but still lower than priceOpen.
469
+ * 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.
470
+ *
471
+ * Default: true (DCA logic enabled everywhere, not just when antirecord is broken)
472
+ */
473
+ CC_ENABLE_DCA_EVERYWHERE: false,
466
474
  };
467
475
  const DEFAULT_CONFIG = Object.freeze({ ...GLOBAL_CONFIG });
468
476
 
@@ -801,6 +809,11 @@ const PERSIST_LOG_UTILS_METHOD_NAME_WRITE_DATA = "PersistLogUtils.writeLogData";
801
809
  const PERSIST_LOG_UTILS_METHOD_NAME_USE_JSON = "PersistLogUtils.useJson";
802
810
  const PERSIST_LOG_UTILS_METHOD_NAME_USE_DUMMY = "PersistLogUtils.useDummy";
803
811
  const PERSIST_LOG_UTILS_METHOD_NAME_USE_PERSIST_LOG_ADAPTER = "PersistLogUtils.usePersistLogAdapter";
812
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA = "PersistMeasureUtils.readMeasureData";
813
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA = "PersistMeasureUtils.writeMeasureData";
814
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON = "PersistMeasureUtils.useJson";
815
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY = "PersistMeasureUtils.useDummy";
816
+ const PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER = "PersistMeasureUtils.usePersistMeasureAdapter";
804
817
  const BASE_WAIT_FOR_INIT_FN_METHOD_NAME = "PersistBase.waitForInitFn";
805
818
  const BASE_UNLINK_RETRY_COUNT = 5;
806
819
  const BASE_UNLINK_RETRY_DELAY = 1000;
@@ -2071,6 +2084,92 @@ class PersistLogUtils {
2071
2084
  * Used by LogPersistUtils for log entry persistence.
2072
2085
  */
2073
2086
  const PersistLogAdapter = new PersistLogUtils();
2087
+ /**
2088
+ * Utility class for managing external API response cache persistence.
2089
+ *
2090
+ * Features:
2091
+ * - Memoized storage instances per cache bucket (aligned timestamp + symbol)
2092
+ * - Custom adapter support
2093
+ * - Atomic read/write operations
2094
+ * - Crash-safe cache state management
2095
+ *
2096
+ * Used by Cache.file for persistent caching of external API responses.
2097
+ */
2098
+ class PersistMeasureUtils {
2099
+ constructor() {
2100
+ this.PersistMeasureFactory = PersistBase;
2101
+ this.getMeasureStorage = functoolsKit.memoize(([bucket]) => bucket, (bucket) => Reflect.construct(this.PersistMeasureFactory, [
2102
+ bucket,
2103
+ `./dump/data/measure/`,
2104
+ ]));
2105
+ /**
2106
+ * Reads cached measure data for a given bucket and key.
2107
+ *
2108
+ * @param bucket - Storage bucket (e.g. aligned timestamp + symbol)
2109
+ * @param key - Dynamic cache key within the bucket
2110
+ * @returns Promise resolving to cached value or null if not found
2111
+ */
2112
+ this.readMeasureData = async (bucket, key) => {
2113
+ bt.loggerService.info(PERSIST_MEASURE_UTILS_METHOD_NAME_READ_DATA, {
2114
+ bucket,
2115
+ key,
2116
+ });
2117
+ const isInitial = !this.getMeasureStorage.has(bucket);
2118
+ const stateStorage = this.getMeasureStorage(bucket);
2119
+ await stateStorage.waitForInit(isInitial);
2120
+ if (await stateStorage.hasValue(key)) {
2121
+ return await stateStorage.readValue(key);
2122
+ }
2123
+ return null;
2124
+ };
2125
+ /**
2126
+ * Writes measure data to disk with atomic file writes.
2127
+ *
2128
+ * @param data - Data to cache
2129
+ * @param bucket - Storage bucket (e.g. aligned timestamp + symbol)
2130
+ * @param key - Dynamic cache key within the bucket
2131
+ * @returns Promise that resolves when write is complete
2132
+ */
2133
+ this.writeMeasureData = async (data, bucket, key) => {
2134
+ bt.loggerService.info(PERSIST_MEASURE_UTILS_METHOD_NAME_WRITE_DATA, {
2135
+ bucket,
2136
+ key,
2137
+ });
2138
+ const isInitial = !this.getMeasureStorage.has(bucket);
2139
+ const stateStorage = this.getMeasureStorage(bucket);
2140
+ await stateStorage.waitForInit(isInitial);
2141
+ await stateStorage.writeValue(key, data);
2142
+ };
2143
+ }
2144
+ /**
2145
+ * Registers a custom persistence adapter.
2146
+ *
2147
+ * @param Ctor - Custom PersistBase constructor
2148
+ */
2149
+ usePersistMeasureAdapter(Ctor) {
2150
+ bt.loggerService.info(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_PERSIST_MEASURE_ADAPTER);
2151
+ this.PersistMeasureFactory = Ctor;
2152
+ }
2153
+ /**
2154
+ * Switches to the default JSON persist adapter.
2155
+ */
2156
+ useJson() {
2157
+ bt.loggerService.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_JSON);
2158
+ this.usePersistMeasureAdapter(PersistBase);
2159
+ }
2160
+ /**
2161
+ * Switches to a dummy persist adapter that discards all writes.
2162
+ */
2163
+ useDummy() {
2164
+ bt.loggerService.log(PERSIST_MEASURE_UTILS_METHOD_NAME_USE_DUMMY);
2165
+ this.usePersistMeasureAdapter(PersistDummy);
2166
+ }
2167
+ }
2168
+ /**
2169
+ * Global singleton instance of PersistMeasureUtils.
2170
+ * Used by Cache.file for persistent caching of external API responses.
2171
+ */
2172
+ const PersistMeasureAdapter = new PersistMeasureUtils();
2074
2173
 
2075
2174
  var _a$3, _b$3;
2076
2175
  const BUSY_DELAY = 100;
@@ -3103,75 +3202,90 @@ class ExchangeConnectionService {
3103
3202
  }
3104
3203
  }
3105
3204
 
3205
+ const COST_BASIS_PER_ENTRY$3 = 100;
3106
3206
  /**
3107
3207
  * Returns the effective entry price for price calculations.
3108
3208
  *
3109
- * When the _entry array exists and has at least one element, returns
3110
- * the simple arithmetic mean of all entry prices (DCA average).
3111
- * Otherwise returns the original signal.priceOpen.
3209
+ * Uses harmonic mean (correct for fixed-dollar DCA: $100 per entry).
3112
3210
  *
3113
- * This mirrors the _trailingPriceStopLoss pattern: original price is preserved
3114
- * in signal.priceOpen (for identity/tracking), while calculations use the
3115
- * effective averaged price returned by this function.
3211
+ * When partial closes exist, replays the partial sequence to reconstruct
3212
+ * the running cost basis at each partial — no extra stored fields needed.
3116
3213
  *
3117
- * @param signal - Signal row (ISignalRow or IScheduledSignalRow)
3118
- * @returns Effective entry price for distance and PNL calculations
3214
+ * Cost basis replay:
3215
+ * costBasis starts at 0
3216
+ * for each partial[i]:
3217
+ * newEntries = entryCountAtClose[i] - entryCountAtClose[i-1] (or entryCountAtClose[0] for i=0)
3218
+ * costBasis += newEntries × $100 ← add DCA entries up to this partial
3219
+ * positionCostBasisAtClose[i] = costBasis ← snapshot BEFORE close
3220
+ * costBasis × = (1 - percent[i] / 100) ← reduce after close
3221
+ *
3222
+ * @param signal - Signal row
3223
+ * @returns Effective entry price for PNL calculations
3119
3224
  */
3120
3225
  const getEffectivePriceOpen = (signal) => {
3121
- if (signal._entry && signal._entry.length > 0) {
3122
- return signal._entry.reduce((sum, e) => sum + e.price, 0) / signal._entry.length;
3123
- }
3124
- return signal.priceOpen;
3226
+ if (!signal._entry || signal._entry.length === 0)
3227
+ return signal.priceOpen;
3228
+ const entries = signal._entry;
3229
+ const partials = signal._partial ?? [];
3230
+ // No partial exits — pure harmonic mean of all entries
3231
+ if (partials.length === 0) {
3232
+ return harmonicMean(entries.map((e) => e.price));
3233
+ }
3234
+ // Replay cost basis through all partials to get snapshot at the last one
3235
+ let costBasis = 0;
3236
+ for (let i = 0; i < partials.length; i++) {
3237
+ const prevCount = i === 0 ? 0 : partials[i - 1].entryCountAtClose;
3238
+ const newEntryCount = partials[i].entryCountAtClose - prevCount;
3239
+ costBasis += newEntryCount * COST_BASIS_PER_ENTRY$3;
3240
+ // costBasis is now positionCostBasisAtClose for partials[i]
3241
+ if (i < partials.length - 1) {
3242
+ costBasis *= 1 - partials[i].percent / 100;
3243
+ }
3244
+ }
3245
+ const lastPartial = partials[partials.length - 1];
3246
+ // Dollar cost basis remaining after the last partial close
3247
+ const remainingCostBasis = costBasis * (1 - lastPartial.percent / 100);
3248
+ // Coins remaining from the old position
3249
+ const oldCoins = remainingCostBasis / lastPartial.effectivePrice;
3250
+ // New DCA entries added AFTER the last partial close
3251
+ const newEntries = entries.slice(lastPartial.entryCountAtClose);
3252
+ // Coins from new DCA entries (each costs $100)
3253
+ const newCoins = newEntries.reduce((sum, e) => sum + 100 / e.price, 0);
3254
+ const totalCoins = oldCoins + newCoins;
3255
+ if (totalCoins === 0)
3256
+ return lastPartial.effectivePrice;
3257
+ const totalCost = remainingCostBasis + newEntries.length * 100;
3258
+ return totalCost / totalCoins;
3259
+ };
3260
+ const harmonicMean = (prices) => {
3261
+ if (prices.length === 0)
3262
+ return 0;
3263
+ return prices.length / prices.reduce((sum, p) => sum + 1 / p, 0);
3125
3264
  };
3126
3265
 
3266
+ const COST_BASIS_PER_ENTRY$2 = 100;
3127
3267
  /**
3128
3268
  * Calculates profit/loss for a closed signal with slippage and fees.
3129
3269
  *
3130
3270
  * For signals with partial closes:
3131
- * - Calculates weighted PNL: Σ(percent_i × pnl_i) for each partial + (remaining% × final_pnl)
3132
- * - Each partial close has its own slippage
3133
- * - Open fee is charged once; close fees are proportional to each partial's size
3134
- * - Total fees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partial% / 100) × (closeWithSlip / openWithSlip)
3135
- *
3136
- * Formula breakdown:
3137
- * 1. Apply slippage to open/close prices (worse execution)
3138
- * - LONG: buy higher (+slippage), sell lower (-slippage)
3139
- * - SHORT: sell lower (-slippage), buy higher (+slippage)
3140
- * 2. Calculate raw PNL percentage
3141
- * - LONG: ((closePrice - openPrice) / openPrice) * 100
3142
- * - SHORT: ((openPrice - closePrice) / openPrice) * 100
3143
- * 3. Subtract total fees: open fee + close fee adjusted for slippage-affected execution price
3271
+ * - Weights are calculated by ACTUAL DOLLAR VALUE of each partial relative to total invested.
3272
+ * This correctly handles DCA entries that occur before or after partial closes.
3273
+ *
3274
+ * Cost basis is reconstructed by replaying the partial sequence via entryCountAtClose + percent:
3275
+ * costBasis = 0
3276
+ * for each partial[i]:
3277
+ * costBasis += (entryCountAtClose[i] - entryCountAtClose[i-1]) × $100
3278
+ * partialDollarValue[i] = (percent[i] / 100) × costBasis
3279
+ * weight[i] = partialDollarValue[i] / totalInvested
3280
+ * costBasis *= (1 - percent[i] / 100)
3281
+ *
3282
+ * Fee structure:
3283
+ * - Open fee: CC_PERCENT_FEE (charged once)
3284
+ * - Close fee: CC_PERCENT_FEE × weight × (closeWithSlip / openWithSlip) per partial/remaining
3144
3285
  *
3145
3286
  * @param signal - Closed signal with position details and optional partial history
3146
3287
  * @param priceClose - Actual close price at final exit
3147
3288
  * @returns PNL data with percentage and prices
3148
- *
3149
- * @example
3150
- * ```typescript
3151
- * // Signal without partial closes
3152
- * const pnl = toProfitLossDto(
3153
- * {
3154
- * position: "long",
3155
- * priceOpen: 100,
3156
- * },
3157
- * 110 // close at +10%
3158
- * );
3159
- * console.log(pnl.pnlPercentage); // ~9.6% (after slippage and fees)
3160
- *
3161
- * // Signal with partial closes
3162
- * const pnlPartial = toProfitLossDto(
3163
- * {
3164
- * position: "long",
3165
- * priceOpen: 100,
3166
- * _partial: [
3167
- * { type: "profit", percent: 30, price: 120 }, // +20% on 30%
3168
- * { type: "profit", percent: 40, price: 115 }, // +15% on 40%
3169
- * ],
3170
- * },
3171
- * 105 // final close at +5% for remaining 30%
3172
- * );
3173
- * // Weighted PNL = 30% × 20% + 40% × 15% + 30% × 5% = 6% + 6% + 1.5% = 13.5% (before fees)
3174
- * ```
3175
3289
  */
3176
3290
  const toProfitLossDto = (signal, priceClose) => {
3177
3291
  const priceOpen = getEffectivePriceOpen(signal);
@@ -3180,47 +3294,65 @@ const toProfitLossDto = (signal, priceClose) => {
3180
3294
  let totalWeightedPnl = 0;
3181
3295
  // Open fee is paid once for the whole position
3182
3296
  let totalFees = GLOBAL_CONFIG.CC_PERCENT_FEE;
3183
- // priceOpenWithSlippage is the same for all partials compute once
3184
- const priceOpenWithSlippage = signal.position === "long"
3185
- ? priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3186
- : priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3297
+ // Total invested capital = number of DCA entries × $100 per entry
3298
+ const totalInvested = signal._entry ? signal._entry.length * 100 : 100;
3299
+ let closedDollarValue = 0;
3300
+ // Running cost basis replayed from entryCountAtClose + percent
3301
+ let costBasis = 0;
3187
3302
  // Calculate PNL for each partial close
3188
- for (const partial of signal._partial) {
3189
- const partialPercent = partial.percent;
3303
+ for (let i = 0; i < signal._partial.length; i++) {
3304
+ const partial = signal._partial[i];
3305
+ // Add DCA entries that existed at this partial but not at the previous one
3306
+ const prevCount = i === 0 ? 0 : signal._partial[i - 1].entryCountAtClose;
3307
+ const newEntryCount = partial.entryCountAtClose - prevCount;
3308
+ costBasis += newEntryCount * COST_BASIS_PER_ENTRY$2;
3309
+ // Real dollar value sold in this partial
3310
+ const partialDollarValue = (partial.percent / 100) * costBasis;
3311
+ // Weight relative to total invested capital
3312
+ const weight = partialDollarValue / totalInvested;
3313
+ closedDollarValue += partialDollarValue;
3314
+ // Reduce cost basis after close
3315
+ costBasis *= 1 - partial.percent / 100;
3316
+ // Use the effective entry price snapshot captured at the time of this partial close
3317
+ const priceOpenWithSlippage = signal.position === "long"
3318
+ ? partial.effectivePrice * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3319
+ : partial.effectivePrice * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3190
3320
  const priceCloseWithSlippage = signal.position === "long"
3191
3321
  ? partial.price * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3192
3322
  : partial.price * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3193
- // Calculate PNL for this partial
3194
3323
  const partialPnl = signal.position === "long"
3195
3324
  ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
3196
3325
  : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
3197
- // Weight by percentage of position closed
3198
- totalWeightedPnl += (partialPercent / 100) * partialPnl;
3199
- // Close fee is proportional to the size of this partial and adjusted for slippage
3200
- totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (partialPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
3201
- }
3202
- // Calculate PNL for remaining position (if any)
3203
- // Compute totalClosed from _partial array
3204
- const totalClosed = signal._partial.reduce((sum, p) => sum + p.percent, 0);
3205
- if (totalClosed > 100) {
3206
- throw new Error(`Partial closes exceed 100%: ${totalClosed}% (signal id: ${signal.id})`);
3207
- }
3208
- const remainingPercent = 100 - totalClosed;
3209
- if (remainingPercent > 0) {
3326
+ totalWeightedPnl += weight * partialPnl;
3327
+ // Close fee proportional to real dollar weight
3328
+ totalFees +=
3329
+ GLOBAL_CONFIG.CC_PERCENT_FEE *
3330
+ weight *
3331
+ (priceCloseWithSlippage / priceOpenWithSlippage);
3332
+ }
3333
+ if (closedDollarValue > totalInvested + 0.001) {
3334
+ throw new Error(`Partial closes dollar value (${closedDollarValue.toFixed(4)}) exceeds total invested (${totalInvested}) — signal id: ${signal.id}`);
3335
+ }
3336
+ // Remaining position
3337
+ const remainingDollarValue = totalInvested - closedDollarValue;
3338
+ const remainingWeight = remainingDollarValue / totalInvested;
3339
+ if (remainingWeight > 0) {
3340
+ // Use current effective price — reflects all DCA including post-partial entries
3341
+ const remainingOpenWithSlippage = signal.position === "long"
3342
+ ? priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3343
+ : priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3210
3344
  const priceCloseWithSlippage = signal.position === "long"
3211
3345
  ? priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100)
3212
3346
  : priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3213
- // Calculate PNL for remaining
3214
3347
  const remainingPnl = signal.position === "long"
3215
- ? ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100
3216
- : ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
3217
- // Weight by remaining percentage
3218
- totalWeightedPnl += (remainingPercent / 100) * remainingPnl;
3219
- // Close fee is proportional to the remaining size and adjusted for slippage
3220
- totalFees += GLOBAL_CONFIG.CC_PERCENT_FEE * (remainingPercent / 100) * (priceCloseWithSlippage / priceOpenWithSlippage);
3348
+ ? ((priceCloseWithSlippage - remainingOpenWithSlippage) / remainingOpenWithSlippage) * 100
3349
+ : ((remainingOpenWithSlippage - priceCloseWithSlippage) / remainingOpenWithSlippage) * 100;
3350
+ totalWeightedPnl += remainingWeight * remainingPnl;
3351
+ totalFees +=
3352
+ GLOBAL_CONFIG.CC_PERCENT_FEE *
3353
+ remainingWeight *
3354
+ (priceCloseWithSlippage / remainingOpenWithSlippage);
3221
3355
  }
3222
- // Subtract total fees from weighted PNL
3223
- // totalFees = CC_PERCENT_FEE (open) + Σ CC_PERCENT_FEE × (partialPercent/100) × (closeWithSlip/openWithSlip)
3224
3356
  const pnlPercentage = totalWeightedPnl - totalFees;
3225
3357
  return {
3226
3358
  pnlPercentage,
@@ -3232,33 +3364,24 @@ const toProfitLossDto = (signal, priceClose) => {
3232
3364
  let priceOpenWithSlippage;
3233
3365
  let priceCloseWithSlippage;
3234
3366
  if (signal.position === "long") {
3235
- // LONG: покупаем дороже, продаем дешевле
3236
3367
  priceOpenWithSlippage = priceOpen * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3237
3368
  priceCloseWithSlippage = priceClose * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3238
3369
  }
3239
3370
  else {
3240
- // SHORT: продаем дешевле, покупаем дороже
3241
3371
  priceOpenWithSlippage = priceOpen * (1 - GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3242
3372
  priceCloseWithSlippage = priceClose * (1 + GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE / 100);
3243
3373
  }
3244
- // Открытие: комиссия от цены входа; закрытие: комиссия от фактической цены выхода (с учётом slippage)
3245
- const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE * (1 + priceCloseWithSlippage / priceOpenWithSlippage);
3374
+ const totalFee = GLOBAL_CONFIG.CC_PERCENT_FEE *
3375
+ (1 + priceCloseWithSlippage / priceOpenWithSlippage);
3246
3376
  let pnlPercentage;
3247
3377
  if (signal.position === "long") {
3248
- // LONG: прибыль при росте цены
3249
3378
  pnlPercentage =
3250
- ((priceCloseWithSlippage - priceOpenWithSlippage) /
3251
- priceOpenWithSlippage) *
3252
- 100;
3379
+ ((priceCloseWithSlippage - priceOpenWithSlippage) / priceOpenWithSlippage) * 100;
3253
3380
  }
3254
3381
  else {
3255
- // SHORT: прибыль при падении цены
3256
3382
  pnlPercentage =
3257
- ((priceOpenWithSlippage - priceCloseWithSlippage) /
3258
- priceOpenWithSlippage) *
3259
- 100;
3383
+ ((priceOpenWithSlippage - priceCloseWithSlippage) / priceOpenWithSlippage) * 100;
3260
3384
  }
3261
- // Вычитаем комиссии
3262
3385
  pnlPercentage -= totalFee;
3263
3386
  return {
3264
3387
  pnlPercentage,
@@ -3314,6 +3437,54 @@ const toPlainString = (content) => {
3314
3437
  return text.trim();
3315
3438
  };
3316
3439
 
3440
+ const COST_BASIS_PER_ENTRY$1 = 100;
3441
+ /**
3442
+ * Returns the total closed state of a position using cost-basis replay.
3443
+ *
3444
+ * Correctly accounts for DCA entries added between partial closes via averageBuy().
3445
+ * Simple percent summation (sum of _partial[i].percent) is INCORRECT when averageBuy()
3446
+ * is called between partials — this function uses the same cost-basis replay as
3447
+ * toProfitLossDto to compute the true dollar-weighted closed fraction.
3448
+ *
3449
+ * Cost-basis replay:
3450
+ * costBasis = 0
3451
+ * for each partial[i]:
3452
+ * costBasis += (entryCountAtClose[i] - entryCountAtClose[i-1]) × $100
3453
+ * closedDollar += (percent[i] / 100) × costBasis
3454
+ * costBasis ×= (1 - percent[i] / 100)
3455
+ * // then add entries added AFTER the last partial
3456
+ * costBasis += (currentEntryCount - lastPartialEntryCount) × $100
3457
+ *
3458
+ * @param signal - Signal row with _partial and _entry arrays
3459
+ * @returns Object with totalClosedPercent (0–100+) and remainingCostBasis (dollar value still open)
3460
+ */
3461
+ const getTotalClosed = (signal) => {
3462
+ const partials = signal._partial ?? [];
3463
+ const currentEntryCount = signal._entry?.length ?? 1;
3464
+ const totalInvested = currentEntryCount * COST_BASIS_PER_ENTRY$1;
3465
+ if (partials.length === 0) {
3466
+ return {
3467
+ totalClosedPercent: 0,
3468
+ remainingCostBasis: totalInvested,
3469
+ };
3470
+ }
3471
+ let costBasis = 0;
3472
+ let closedDollarValue = 0;
3473
+ for (let i = 0; i < partials.length; i++) {
3474
+ const prevCount = i === 0 ? 0 : partials[i - 1].entryCountAtClose;
3475
+ costBasis += (partials[i].entryCountAtClose - prevCount) * COST_BASIS_PER_ENTRY$1;
3476
+ closedDollarValue += (partials[i].percent / 100) * costBasis;
3477
+ costBasis *= 1 - partials[i].percent / 100;
3478
+ }
3479
+ // Add entries added AFTER the last partial (not yet accounted for in the loop)
3480
+ const lastEntryCount = partials[partials.length - 1].entryCountAtClose;
3481
+ costBasis += (currentEntryCount - lastEntryCount) * COST_BASIS_PER_ENTRY$1;
3482
+ return {
3483
+ totalClosedPercent: totalInvested > 0 ? (closedDollarValue / totalInvested) * 100 : 0,
3484
+ remainingCostBasis: costBasis,
3485
+ };
3486
+ };
3487
+
3317
3488
  /**
3318
3489
  * Wraps a function to execute it outside of the current execution context if one exists.
3319
3490
  *
@@ -3356,6 +3527,19 @@ const beginTime = (run) => (...args) => {
3356
3527
  return fn();
3357
3528
  };
3358
3529
 
3530
+ /**
3531
+ * Retrieves the current timestamp for debugging purposes.
3532
+ * 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.
3533
+ * Can be empty (undefined) if not called from strategy async context, as it's intended for debugging and not critical for logic.
3534
+ * @return {number | undefined} The current timestamp in milliseconds from the execution context, or undefined if not available.
3535
+ */
3536
+ const getDebugTimestamp = () => {
3537
+ if (ExecutionContextService.hasContext()) {
3538
+ return bt.executionContextService.context.when.getTime();
3539
+ }
3540
+ return undefined;
3541
+ };
3542
+
3359
3543
  const INTERVAL_MINUTES$6 = {
3360
3544
  "1m": 1,
3361
3545
  "3m": 3,
@@ -3364,6 +3548,7 @@ const INTERVAL_MINUTES$6 = {
3364
3548
  "30m": 30,
3365
3549
  "1h": 60,
3366
3550
  };
3551
+ const COST_BASIS_PER_ENTRY = 100;
3367
3552
  /**
3368
3553
  * Mock value for scheduled signal pendingAt timestamp.
3369
3554
  * Used to indicate that the actual pendingAt will be set upon activation.
@@ -3965,7 +4150,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
3965
4150
  scheduledAt: currentTime,
3966
4151
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
3967
4152
  _isScheduled: false,
3968
- _entry: [{ price: signal.priceOpen }],
4153
+ _entry: [{ price: signal.priceOpen, debugTimestamp: currentTime }],
3969
4154
  };
3970
4155
  // Валидируем сигнал перед возвратом
3971
4156
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -3987,7 +4172,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
3987
4172
  scheduledAt: currentTime,
3988
4173
  pendingAt: SCHEDULED_SIGNAL_PENDING_MOCK, // Временно, обновится при активации
3989
4174
  _isScheduled: true,
3990
- _entry: [{ price: signal.priceOpen }],
4175
+ _entry: [{ price: signal.priceOpen, debugTimestamp: currentTime }],
3991
4176
  };
3992
4177
  // Валидируем сигнал перед возвратом
3993
4178
  VALIDATE_SIGNAL_FN(scheduledSignalRow, currentPrice, true);
@@ -4005,7 +4190,7 @@ const GET_SIGNAL_FN = functoolsKit.trycatch(async (self) => {
4005
4190
  scheduledAt: currentTime,
4006
4191
  pendingAt: currentTime, // Для immediate signal оба времени одинаковые
4007
4192
  _isScheduled: false,
4008
- _entry: [{ price: currentPrice }],
4193
+ _entry: [{ price: currentPrice, debugTimestamp: currentTime }],
4009
4194
  };
4010
4195
  // Валидируем сигнал перед возвратом
4011
4196
  VALIDATE_SIGNAL_FN(signalRow, currentPrice, false);
@@ -4079,37 +4264,39 @@ const PARTIAL_PROFIT_FN = (self, signal, percentToClose, currentPrice) => {
4079
4264
  // Initialize partial array if not present
4080
4265
  if (!signal._partial)
4081
4266
  signal._partial = [];
4082
- // Calculate current totals (computed values)
4083
- const tpClosed = signal._partial
4084
- .filter((p) => p.type === "profit")
4085
- .reduce((sum, p) => sum + p.percent, 0);
4086
- const slClosed = signal._partial
4087
- .filter((p) => p.type === "loss")
4088
- .reduce((sum, p) => sum + p.percent, 0);
4089
- const totalClosed = tpClosed + slClosed;
4090
- // Check if would exceed 100% total closed
4091
- const newTotalClosed = totalClosed + percentToClose;
4092
- if (newTotalClosed > 100) {
4093
- self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed, skipping", {
4267
+ // Check if would exceed 100% total closed (dollar-basis, DCA-aware)
4268
+ const { totalClosedPercent, remainingCostBasis } = getTotalClosed(signal);
4269
+ const totalInvested = (signal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
4270
+ const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
4271
+ const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
4272
+ if (newTotalClosedDollar > totalInvested) {
4273
+ self.params.logger.warn("PARTIAL_PROFIT_FN: would exceed 100% closed (dollar basis), skipping", {
4094
4274
  signalId: signal.id,
4095
- currentTotalClosed: totalClosed,
4275
+ totalClosedPercent,
4276
+ remainingCostBasis,
4096
4277
  percentToClose,
4097
- newTotalClosed,
4278
+ newPartialDollar,
4279
+ totalInvested,
4098
4280
  });
4099
4281
  return false;
4100
4282
  }
4283
+ // Capture effective entry price at the moment of partial close (for DCA-aware PNL)
4284
+ const effectivePrice = getEffectivePriceOpen(signal);
4285
+ const entryCountAtClose = signal._entry ? signal._entry.length : 1;
4101
4286
  // Add new partial close entry
4102
4287
  signal._partial.push({
4103
4288
  type: "profit",
4104
4289
  percent: percentToClose,
4290
+ entryCountAtClose,
4105
4291
  price: currentPrice,
4292
+ debugTimestamp: getDebugTimestamp(),
4293
+ effectivePrice,
4106
4294
  });
4107
4295
  self.params.logger.info("PARTIAL_PROFIT_FN executed", {
4108
4296
  signalId: signal.id,
4109
4297
  percentClosed: percentToClose,
4110
- totalClosed: newTotalClosed,
4298
+ totalClosedPercent: totalClosedPercent + (newPartialDollar / totalInvested) * 100,
4111
4299
  currentPrice,
4112
- tpClosed: tpClosed + percentToClose,
4113
4300
  });
4114
4301
  return true;
4115
4302
  };
@@ -4117,37 +4304,39 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
4117
4304
  // Initialize partial array if not present
4118
4305
  if (!signal._partial)
4119
4306
  signal._partial = [];
4120
- // Calculate current totals (computed values)
4121
- const tpClosed = signal._partial
4122
- .filter((p) => p.type === "profit")
4123
- .reduce((sum, p) => sum + p.percent, 0);
4124
- const slClosed = signal._partial
4125
- .filter((p) => p.type === "loss")
4126
- .reduce((sum, p) => sum + p.percent, 0);
4127
- const totalClosed = tpClosed + slClosed;
4128
- // Check if would exceed 100% total closed
4129
- const newTotalClosed = totalClosed + percentToClose;
4130
- if (newTotalClosed > 100) {
4131
- self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed, skipping", {
4307
+ // Check if would exceed 100% total closed (dollar-basis, DCA-aware)
4308
+ const { totalClosedPercent, remainingCostBasis } = getTotalClosed(signal);
4309
+ const totalInvested = (signal._entry?.length ?? 1) * COST_BASIS_PER_ENTRY;
4310
+ const newPartialDollar = (percentToClose / 100) * remainingCostBasis;
4311
+ const newTotalClosedDollar = (totalClosedPercent / 100) * totalInvested + newPartialDollar;
4312
+ if (newTotalClosedDollar > totalInvested) {
4313
+ self.params.logger.warn("PARTIAL_LOSS_FN: would exceed 100% closed (dollar basis), skipping", {
4132
4314
  signalId: signal.id,
4133
- currentTotalClosed: totalClosed,
4315
+ totalClosedPercent,
4316
+ remainingCostBasis,
4134
4317
  percentToClose,
4135
- newTotalClosed,
4318
+ newPartialDollar,
4319
+ totalInvested,
4136
4320
  });
4137
4321
  return false;
4138
4322
  }
4323
+ // Capture effective entry price at the moment of partial close (for DCA-aware PNL)
4324
+ const effectivePrice = getEffectivePriceOpen(signal);
4325
+ const entryCountAtClose = signal._entry ? signal._entry.length : 1;
4139
4326
  // Add new partial close entry
4140
4327
  signal._partial.push({
4141
4328
  type: "loss",
4142
4329
  percent: percentToClose,
4143
4330
  price: currentPrice,
4331
+ entryCountAtClose,
4332
+ effectivePrice,
4333
+ debugTimestamp: getDebugTimestamp(),
4144
4334
  });
4145
4335
  self.params.logger.warn("PARTIAL_LOSS_FN executed", {
4146
4336
  signalId: signal.id,
4147
4337
  percentClosed: percentToClose,
4148
- totalClosed: newTotalClosed,
4338
+ totalClosedPercent: totalClosedPercent + (newPartialDollar / totalInvested) * 100,
4149
4339
  currentPrice,
4150
- slClosed: slClosed + percentToClose,
4151
4340
  });
4152
4341
  return true;
4153
4342
  };
@@ -4541,12 +4730,12 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
4541
4730
  const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
4542
4731
  // Ensure _entry is initialized (handles signals loaded from disk without _entry)
4543
4732
  if (!signal._entry || signal._entry.length === 0) {
4544
- signal._entry = [{ price: signal.priceOpen }];
4733
+ signal._entry = [{ price: signal.priceOpen, debugTimestamp: getDebugTimestamp() }];
4545
4734
  }
4546
4735
  const lastEntry = signal._entry[signal._entry.length - 1];
4547
4736
  if (signal.position === "long") {
4548
4737
  // LONG: averaging down = currentPrice must be strictly lower than last entry
4549
- if (currentPrice >= lastEntry.price) {
4738
+ if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice >= lastEntry.price) {
4550
4739
  self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice >= last entry (LONG)", {
4551
4740
  signalId: signal.id,
4552
4741
  position: signal.position,
@@ -4559,7 +4748,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
4559
4748
  }
4560
4749
  else {
4561
4750
  // SHORT: averaging down = currentPrice must be strictly higher than last entry
4562
- if (currentPrice <= lastEntry.price) {
4751
+ if (!GLOBAL_CONFIG.CC_ENABLE_DCA_EVERYWHERE && currentPrice <= lastEntry.price) {
4563
4752
  self.params.logger.debug("AVERAGE_BUY_FN: rejected — currentPrice <= last entry (SHORT)", {
4564
4753
  signalId: signal.id,
4565
4754
  position: signal.position,
@@ -4570,7 +4759,7 @@ const AVERAGE_BUY_FN = (self, signal, currentPrice) => {
4570
4759
  return false;
4571
4760
  }
4572
4761
  }
4573
- signal._entry.push({ price: currentPrice });
4762
+ signal._entry.push({ price: currentPrice, debugTimestamp: getDebugTimestamp() });
4574
4763
  self.params.logger.info("AVERAGE_BUY_FN executed", {
4575
4764
  signalId: signal.id,
4576
4765
  position: signal.position,
@@ -6035,6 +6224,52 @@ class ClientStrategy {
6035
6224
  });
6036
6225
  return this._isStopped;
6037
6226
  }
6227
+ /**
6228
+ * Returns how much of the position is still held, as a percentage of totalInvested.
6229
+ *
6230
+ * Uses dollar-basis cost-basis replay (DCA-aware).
6231
+ * 100% means nothing was closed yet. Decreases with each partial close.
6232
+ *
6233
+ * Example: 1 entry $100, partialProfit(30%) → returns 70
6234
+ * Example: 2 entries $200, partialProfit(50%) → returns 50
6235
+ *
6236
+ * Returns 100 if no pending signal or no partial closes.
6237
+ *
6238
+ * @param symbol - Trading pair symbol
6239
+ * @returns Promise resolving to held percentage (0–100)
6240
+ */
6241
+ async getTotalPercentClosed(symbol) {
6242
+ this.params.logger.debug("ClientStrategy getTotalPercentClosed", { symbol });
6243
+ if (!this._pendingSignal) {
6244
+ return null;
6245
+ }
6246
+ const { totalClosedPercent } = getTotalClosed(this._pendingSignal);
6247
+ return 100 - totalClosedPercent;
6248
+ }
6249
+ /**
6250
+ * Returns how many dollars of cost basis are still held (not yet closed by partials).
6251
+ *
6252
+ * Equal to remainingCostBasis from getTotalClosed.
6253
+ * Full position open: equals totalInvested (entries × $100).
6254
+ * Decreases with each partial close, increases with each averageBuy().
6255
+ *
6256
+ * Example: 1 entry $100, no partials → returns 100
6257
+ * Example: 1 entry $100, partialProfit(30%) → returns 70
6258
+ * Example: 2 entries $200, partialProfit(50%) → returns 100
6259
+ *
6260
+ * Returns totalInvested if no pending signal or no partial closes.
6261
+ *
6262
+ * @param symbol - Trading pair symbol
6263
+ * @returns Promise resolving to held cost basis in dollars
6264
+ */
6265
+ async getTotalCostClosed(symbol) {
6266
+ this.params.logger.debug("ClientStrategy getTotalCostClosed", { symbol });
6267
+ if (!this._pendingSignal) {
6268
+ return null;
6269
+ }
6270
+ const { remainingCostBasis } = getTotalClosed(this._pendingSignal);
6271
+ return remainingCostBasis;
6272
+ }
6038
6273
  /**
6039
6274
  * Performs a single tick of strategy execution.
6040
6275
  *
@@ -7495,14 +7730,6 @@ class ClientStrategy {
7495
7730
  if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
7496
7731
  throw new Error(`ClientStrategy averageBuy: currentPrice must be a positive finite number, got ${currentPrice}`);
7497
7732
  }
7498
- // Reject if any partial closes have already been executed
7499
- if (this._pendingSignal._partial && this._pendingSignal._partial.length > 0) {
7500
- this.params.logger.debug("ClientStrategy averageBuy: rejected — partial closes already executed", {
7501
- symbol,
7502
- partialCount: this._pendingSignal._partial.length,
7503
- });
7504
- return false;
7505
- }
7506
7733
  // Execute averaging logic
7507
7734
  const result = AVERAGE_BUY_FN(this, this._pendingSignal, currentPrice);
7508
7735
  if (!result) {
@@ -8181,6 +8408,43 @@ class StrategyConnectionService {
8181
8408
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8182
8409
  return await strategy.getPendingSignal(symbol);
8183
8410
  };
8411
+ /**
8412
+ * Returns the percentage of the position currently held (not closed).
8413
+ * 100 = nothing has been closed (full position), 0 = fully closed.
8414
+ * Correctly accounts for DCA entries between partial closes.
8415
+ *
8416
+ * @param backtest - Whether running in backtest mode
8417
+ * @param symbol - Trading pair symbol
8418
+ * @param context - Execution context with strategyName, exchangeName, frameName
8419
+ * @returns Promise<number> - held percentage (0–100)
8420
+ */
8421
+ this.getTotalPercentClosed = async (backtest, symbol, context) => {
8422
+ this.loggerService.log("strategyConnectionService getTotalPercentClosed", {
8423
+ symbol,
8424
+ context,
8425
+ backtest,
8426
+ });
8427
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8428
+ return await strategy.getTotalPercentClosed(symbol);
8429
+ };
8430
+ /**
8431
+ * Returns the cost basis in dollars of the position currently held (not closed).
8432
+ * Correctly accounts for DCA entries between partial closes.
8433
+ *
8434
+ * @param backtest - Whether running in backtest mode
8435
+ * @param symbol - Trading pair symbol
8436
+ * @param context - Execution context with strategyName, exchangeName, frameName
8437
+ * @returns Promise<number> - held cost basis in dollars
8438
+ */
8439
+ this.getTotalCostClosed = async (backtest, symbol, context) => {
8440
+ this.loggerService.log("strategyConnectionService getTotalCostClosed", {
8441
+ symbol,
8442
+ context,
8443
+ backtest,
8444
+ });
8445
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
8446
+ return await strategy.getTotalCostClosed(symbol);
8447
+ };
8184
8448
  /**
8185
8449
  * Retrieves the currently active scheduled signal for the strategy.
8186
8450
  * If no scheduled signal exists, returns null.
@@ -10371,7 +10635,7 @@ class ActionBase {
10371
10635
  * @example
10372
10636
  * ```typescript
10373
10637
  * pingScheduled(event: SchedulePingContract) {
10374
- * const waitTime = Date.now() - event.data.timestampScheduled;
10638
+ * const waitTime = getTimestamp() - event.data.timestampScheduled;
10375
10639
  * const waitMinutes = Math.floor(waitTime / 60000);
10376
10640
  * console.log(`Scheduled signal waiting ${waitMinutes} minutes`);
10377
10641
  * }
@@ -10400,7 +10664,7 @@ class ActionBase {
10400
10664
  * @example
10401
10665
  * ```typescript
10402
10666
  * pingActive(event: ActivePingContract) {
10403
- * const holdTime = Date.now() - event.data.pendingAt;
10667
+ * const holdTime = getTimestamp() - event.data.pendingAt;
10404
10668
  * const holdMinutes = Math.floor(holdTime / 60000);
10405
10669
  * console.log(`Active signal holding ${holdMinutes} minutes`);
10406
10670
  * }
@@ -11584,6 +11848,41 @@ class StrategyCoreService {
11584
11848
  await this.validate(context);
11585
11849
  return await this.strategyConnectionService.getPendingSignal(backtest, symbol, context);
11586
11850
  };
11851
+ /**
11852
+ * Returns the percentage of the position currently held (not closed).
11853
+ * 100 = nothing has been closed (full position), 0 = fully closed.
11854
+ * Correctly accounts for DCA entries between partial closes.
11855
+ *
11856
+ * @param backtest - Whether running in backtest mode
11857
+ * @param symbol - Trading pair symbol
11858
+ * @param context - Execution context with strategyName, exchangeName, frameName
11859
+ * @returns Promise<number> - held percentage (0–100)
11860
+ */
11861
+ this.getTotalPercentClosed = async (backtest, symbol, context) => {
11862
+ this.loggerService.log("strategyCoreService getTotalPercentClosed", {
11863
+ symbol,
11864
+ context,
11865
+ });
11866
+ await this.validate(context);
11867
+ return await this.strategyConnectionService.getTotalPercentClosed(backtest, symbol, context);
11868
+ };
11869
+ /**
11870
+ * Returns the cost basis in dollars of the position currently held (not closed).
11871
+ * Correctly accounts for DCA entries between partial closes.
11872
+ *
11873
+ * @param backtest - Whether running in backtest mode
11874
+ * @param symbol - Trading pair symbol
11875
+ * @param context - Execution context with strategyName, exchangeName, frameName
11876
+ * @returns Promise<number> - held cost basis in dollars
11877
+ */
11878
+ this.getTotalCostClosed = async (backtest, symbol, context) => {
11879
+ this.loggerService.log("strategyCoreService getTotalCostClosed", {
11880
+ symbol,
11881
+ context,
11882
+ });
11883
+ await this.validate(context);
11884
+ return await this.strategyConnectionService.getTotalCostClosed(backtest, symbol, context);
11885
+ };
11587
11886
  /**
11588
11887
  * Retrieves the currently active scheduled signal for the symbol.
11589
11888
  * If no scheduled signal exists, returns null.
@@ -16102,6 +16401,20 @@ const COLUMN_CONFIG = {
16102
16401
  */
16103
16402
  const DEFAULT_COLUMNS = Object.freeze({ ...COLUMN_CONFIG });
16104
16403
 
16404
+ /**
16405
+ * Retrieves the current timestamp based on the execution context.
16406
+ * 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.
16407
+ * If no execution context is active (e.g., during live operation), it returns the current real-world timestamp.
16408
+ * This function helps maintain accurate timing for logs, metrics, and other time-sensitive operations across both live and backtest modes.
16409
+ * @return {number} The current timestamp in milliseconds, either from the execution context or the real-world clock.
16410
+ */
16411
+ const getContextTimestamp = () => {
16412
+ if (ExecutionContextService.hasContext()) {
16413
+ return bt.executionContextService.context.when.getTime();
16414
+ }
16415
+ return Date.now();
16416
+ };
16417
+
16105
16418
  var _a$2, _b$2;
16106
16419
  const MARKDOWN_METHOD_NAME_ENABLE = "MarkdownUtils.enable";
16107
16420
  const MARKDOWN_METHOD_NAME_DISABLE = "MarkdownUtils.disable";
@@ -16235,7 +16548,7 @@ class MarkdownFileBase {
16235
16548
  markdownName: this.markdownName,
16236
16549
  data,
16237
16550
  ...searchFlags,
16238
- timestamp: Date.now(),
16551
+ timestamp: getContextTimestamp(),
16239
16552
  }) + "\n";
16240
16553
  const status = await this[WRITE_SAFE_SYMBOL$2](line);
16241
16554
  if (status === functoolsKit.TIMEOUT_SYMBOL) {
@@ -16779,7 +17092,7 @@ let ReportStorage$7 = class ReportStorage {
16779
17092
  */
16780
17093
  async dump(strategyName, path = "./dump/backtest", columns = COLUMN_CONFIG.backtest_columns) {
16781
17094
  const markdown = await this.getReport(strategyName, columns);
16782
- const timestamp = Date.now();
17095
+ const timestamp = getContextTimestamp();
16783
17096
  const filename = CREATE_FILE_NAME_FN$9(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
16784
17097
  await Markdown.writeData("backtest", markdown, {
16785
17098
  path,
@@ -17114,7 +17427,7 @@ let ReportStorage$6 = class ReportStorage {
17114
17427
  */
17115
17428
  addIdleEvent(currentPrice) {
17116
17429
  const newEvent = {
17117
- timestamp: Date.now(),
17430
+ timestamp: getContextTimestamp(),
17118
17431
  action: "idle",
17119
17432
  currentPrice,
17120
17433
  };
@@ -17170,7 +17483,7 @@ let ReportStorage$6 = class ReportStorage {
17170
17483
  */
17171
17484
  addActiveEvent(data) {
17172
17485
  const newEvent = {
17173
- timestamp: Date.now(),
17486
+ timestamp: getContextTimestamp(),
17174
17487
  action: "active",
17175
17488
  symbol: data.signal.symbol,
17176
17489
  signalId: data.signal.id,
@@ -17272,7 +17585,7 @@ let ReportStorage$6 = class ReportStorage {
17272
17585
  */
17273
17586
  addWaitingEvent(data) {
17274
17587
  const newEvent = {
17275
- timestamp: Date.now(),
17588
+ timestamp: getContextTimestamp(),
17276
17589
  action: "waiting",
17277
17590
  symbol: data.signal.symbol,
17278
17591
  signalId: data.signal.id,
@@ -17466,7 +17779,7 @@ let ReportStorage$6 = class ReportStorage {
17466
17779
  */
17467
17780
  async dump(strategyName, path = "./dump/live", columns = COLUMN_CONFIG.live_columns) {
17468
17781
  const markdown = await this.getReport(strategyName, columns);
17469
- const timestamp = Date.now();
17782
+ const timestamp = getContextTimestamp();
17470
17783
  const filename = CREATE_FILE_NAME_FN$8(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
17471
17784
  await Markdown.writeData("live", markdown, {
17472
17785
  path,
@@ -17998,7 +18311,7 @@ let ReportStorage$5 = class ReportStorage {
17998
18311
  */
17999
18312
  async dump(strategyName, path = "./dump/schedule", columns = COLUMN_CONFIG.schedule_columns) {
18000
18313
  const markdown = await this.getReport(strategyName, columns);
18001
- const timestamp = Date.now();
18314
+ const timestamp = getContextTimestamp();
18002
18315
  const filename = CREATE_FILE_NAME_FN$7(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
18003
18316
  await Markdown.writeData("schedule", markdown, {
18004
18317
  path,
@@ -18463,7 +18776,7 @@ class PerformanceStorage {
18463
18776
  */
18464
18777
  async dump(strategyName, path = "./dump/performance", columns = COLUMN_CONFIG.performance_columns) {
18465
18778
  const markdown = await this.getReport(strategyName, columns);
18466
- const timestamp = Date.now();
18779
+ const timestamp = getContextTimestamp();
18467
18780
  const filename = CREATE_FILE_NAME_FN$6(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
18468
18781
  await Markdown.writeData("performance", markdown, {
18469
18782
  path,
@@ -18914,7 +19227,7 @@ let ReportStorage$4 = class ReportStorage {
18914
19227
  */
18915
19228
  async dump(symbol, metric, context, path = "./dump/walker", strategyColumns = COLUMN_CONFIG.walker_strategy_columns, pnlColumns = COLUMN_CONFIG.walker_pnl_columns) {
18916
19229
  const markdown = await this.getReport(symbol, metric, context, strategyColumns, pnlColumns);
18917
- const timestamp = Date.now();
19230
+ const timestamp = getContextTimestamp();
18918
19231
  const filename = CREATE_FILE_NAME_FN$5(this.walkerName, timestamp);
18919
19232
  await Markdown.writeData("walker", markdown, {
18920
19233
  path,
@@ -19468,7 +19781,7 @@ class HeatmapStorage {
19468
19781
  */
19469
19782
  async dump(strategyName, path = "./dump/heatmap", columns = COLUMN_CONFIG.heat_columns) {
19470
19783
  const markdown = await this.getReport(strategyName, columns);
19471
- const timestamp = Date.now();
19784
+ const timestamp = getContextTimestamp();
19472
19785
  const filename = CREATE_FILE_NAME_FN$4(strategyName, this.exchangeName, this.frameName, timestamp);
19473
19786
  await Markdown.writeData("heat", markdown, {
19474
19787
  path,
@@ -21148,7 +21461,7 @@ let ReportStorage$3 = class ReportStorage {
21148
21461
  */
21149
21462
  async dump(symbol, strategyName, path = "./dump/partial", columns = COLUMN_CONFIG.partial_columns) {
21150
21463
  const markdown = await this.getReport(symbol, strategyName, columns);
21151
- const timestamp = Date.now();
21464
+ const timestamp = getContextTimestamp();
21152
21465
  const filename = CREATE_FILE_NAME_FN$3(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
21153
21466
  await Markdown.writeData("partial", markdown, {
21154
21467
  path,
@@ -22241,7 +22554,7 @@ let ReportStorage$2 = class ReportStorage {
22241
22554
  */
22242
22555
  async dump(symbol, strategyName, path = "./dump/breakeven", columns = COLUMN_CONFIG.breakeven_columns) {
22243
22556
  const markdown = await this.getReport(symbol, strategyName, columns);
22244
- const timestamp = Date.now();
22557
+ const timestamp = getContextTimestamp();
22245
22558
  const filename = CREATE_FILE_NAME_FN$2(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
22246
22559
  await Markdown.writeData("breakeven", markdown, {
22247
22560
  path,
@@ -22909,7 +23222,7 @@ let ReportStorage$1 = class ReportStorage {
22909
23222
  */
22910
23223
  async dump(symbol, strategyName, path = "./dump/risk", columns = COLUMN_CONFIG.risk_columns) {
22911
23224
  const markdown = await this.getReport(symbol, strategyName, columns);
22912
- const timestamp = Date.now();
23225
+ const timestamp = getContextTimestamp();
22913
23226
  const filename = CREATE_FILE_NAME_FN$1(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
22914
23227
  await Markdown.writeData("risk", markdown, {
22915
23228
  path,
@@ -23393,7 +23706,7 @@ class ReportBase {
23393
23706
  reportName: this.reportName,
23394
23707
  data,
23395
23708
  ...searchFlags,
23396
- timestamp: Date.now(),
23709
+ timestamp: getContextTimestamp(),
23397
23710
  }) + "\n";
23398
23711
  const status = await this[WRITE_SAFE_SYMBOL$1](line);
23399
23712
  if (status === functoolsKit.TIMEOUT_SYMBOL) {
@@ -23732,7 +24045,7 @@ class BacktestReportService {
23732
24045
  this.tick = async (data) => {
23733
24046
  this.loggerService.log(BACKTEST_REPORT_METHOD_NAME_TICK, { data });
23734
24047
  const baseEvent = {
23735
- timestamp: Date.now(),
24048
+ timestamp: getContextTimestamp(),
23736
24049
  action: data.action,
23737
24050
  symbol: data.symbol,
23738
24051
  strategyName: data.strategyName,
@@ -23911,7 +24224,7 @@ class LiveReportService {
23911
24224
  this.tick = async (data) => {
23912
24225
  this.loggerService.log(LIVE_REPORT_METHOD_NAME_TICK, { data });
23913
24226
  const baseEvent = {
23914
- timestamp: Date.now(),
24227
+ timestamp: getContextTimestamp(),
23915
24228
  action: data.action,
23916
24229
  symbol: data.symbol,
23917
24230
  strategyName: data.strategyName,
@@ -24434,7 +24747,7 @@ class WalkerReportService {
24434
24747
  this.tick = async (data) => {
24435
24748
  this.loggerService.log(WALKER_REPORT_METHOD_NAME_TICK, { data });
24436
24749
  await Report.writeData("walker", {
24437
- timestamp: Date.now(),
24750
+ timestamp: getContextTimestamp(),
24438
24751
  walkerName: data.walkerName,
24439
24752
  symbol: data.symbol,
24440
24753
  exchangeName: data.exchangeName,
@@ -24561,7 +24874,7 @@ class HeatReportService {
24561
24874
  return;
24562
24875
  }
24563
24876
  await Report.writeData("heat", {
24564
- timestamp: Date.now(),
24877
+ timestamp: getContextTimestamp(),
24565
24878
  action: data.action,
24566
24879
  symbol: data.symbol,
24567
24880
  strategyName: data.strategyName,
@@ -25883,7 +26196,7 @@ class ReportStorage {
25883
26196
  */
25884
26197
  async dump(symbol, strategyName, path = "./dump/strategy", columns = COLUMN_CONFIG.strategy_columns) {
25885
26198
  const markdown = await this.getReport(symbol, strategyName, columns);
25886
- const timestamp = Date.now();
26199
+ const timestamp = getContextTimestamp();
25887
26200
  const filename = CREATE_FILE_NAME_FN(this.symbol, strategyName, this.exchangeName, this.frameName, timestamp);
25888
26201
  await Markdown.writeData("strategy", markdown, {
25889
26202
  path,
@@ -28199,6 +28512,7 @@ const GET_AVERAGE_PRICE_METHOD_NAME = "exchange.getAveragePrice";
28199
28512
  const FORMAT_PRICE_METHOD_NAME = "exchange.formatPrice";
28200
28513
  const FORMAT_QUANTITY_METHOD_NAME = "exchange.formatQuantity";
28201
28514
  const GET_DATE_METHOD_NAME = "exchange.getDate";
28515
+ const GET_TIMESTAMP_METHOD_NAME = "exchange.getTimestamp";
28202
28516
  const GET_MODE_METHOD_NAME = "exchange.getMode";
28203
28517
  const GET_SYMBOL_METHOD_NAME = "exchange.getSymbol";
28204
28518
  const GET_CONTEXT_METHOD_NAME = "exchange.getContext";
@@ -28364,6 +28678,26 @@ async function getDate() {
28364
28678
  const { when } = bt.executionContextService.context;
28365
28679
  return new Date(when.getTime());
28366
28680
  }
28681
+ /**
28682
+ * Gets the current timestamp from execution context.
28683
+ *
28684
+ * In backtest mode: returns the current timeframe timestamp being processed
28685
+ * In live mode: returns current real-time timestamp
28686
+ *
28687
+ * @returns Promise resolving to current execution context timestamp in milliseconds
28688
+ * @example
28689
+ * ```typescript
28690
+ * const timestamp = await getTimestamp();
28691
+ * console.log(timestamp); // 1700000000000
28692
+ * ```
28693
+ */
28694
+ async function getTimestamp() {
28695
+ bt.loggerService.info(GET_TIMESTAMP_METHOD_NAME);
28696
+ if (!ExecutionContextService.hasContext()) {
28697
+ throw new Error("getTimestamp requires an execution context");
28698
+ }
28699
+ return getContextTimestamp();
28700
+ }
28367
28701
  /**
28368
28702
  * Gets the current execution mode.
28369
28703
  *
@@ -28580,6 +28914,11 @@ const TRAILING_PROFIT_METHOD_NAME = "strategy.commitTrailingTake";
28580
28914
  const BREAKEVEN_METHOD_NAME = "strategy.commitBreakeven";
28581
28915
  const ACTIVATE_SCHEDULED_METHOD_NAME = "strategy.commitActivateScheduled";
28582
28916
  const AVERAGE_BUY_METHOD_NAME = "strategy.commitAverageBuy";
28917
+ const GET_TOTAL_PERCENT_CLOSED_METHOD_NAME = "strategy.getTotalPercentClosed";
28918
+ const GET_TOTAL_COST_CLOSED_METHOD_NAME = "strategy.getTotalCostClosed";
28919
+ const GET_PENDING_SIGNAL_METHOD_NAME = "strategy.getPendingSignal";
28920
+ const GET_SCHEDULED_SIGNAL_METHOD_NAME = "strategy.getScheduledSignal";
28921
+ const GET_BREAKEVEN_METHOD_NAME = "strategy.getBreakeven";
28583
28922
  /**
28584
28923
  * Cancels the scheduled signal without stopping the strategy.
28585
28924
  *
@@ -28971,6 +29310,173 @@ async function commitAverageBuy(symbol) {
28971
29310
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
28972
29311
  return await bt.strategyCoreService.averageBuy(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
28973
29312
  }
29313
+ /**
29314
+ * Returns the percentage of the position currently held (not closed).
29315
+ * 100 = nothing has been closed (full position), 0 = fully closed.
29316
+ * Correctly accounts for DCA entries between partial closes.
29317
+ *
29318
+ * Automatically detects backtest/live mode from execution context.
29319
+ *
29320
+ * @param symbol - Trading pair symbol
29321
+ * @returns Promise<number> - held percentage (0–100)
29322
+ *
29323
+ * @example
29324
+ * ```typescript
29325
+ * import { getTotalPercentClosed } from "backtest-kit";
29326
+ *
29327
+ * const heldPct = await getTotalPercentClosed("BTCUSDT");
29328
+ * console.log(`Holding ${heldPct}% of position`);
29329
+ * ```
29330
+ */
29331
+ async function getTotalPercentClosed(symbol) {
29332
+ bt.loggerService.info(GET_TOTAL_PERCENT_CLOSED_METHOD_NAME, {
29333
+ symbol,
29334
+ });
29335
+ if (!ExecutionContextService.hasContext()) {
29336
+ throw new Error("getTotalPercentClosed requires an execution context");
29337
+ }
29338
+ if (!MethodContextService.hasContext()) {
29339
+ throw new Error("getTotalPercentClosed requires a method context");
29340
+ }
29341
+ const { backtest: isBacktest } = bt.executionContextService.context;
29342
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29343
+ return await bt.strategyCoreService.getTotalPercentClosed(isBacktest, symbol, { exchangeName, frameName, strategyName });
29344
+ }
29345
+ /**
29346
+ * Returns the cost basis in dollars of the position currently held (not closed).
29347
+ * Correctly accounts for DCA entries between partial closes.
29348
+ *
29349
+ * Automatically detects backtest/live mode from execution context.
29350
+ *
29351
+ * @param symbol - Trading pair symbol
29352
+ * @returns Promise<number> - held cost basis in dollars
29353
+ *
29354
+ * @example
29355
+ * ```typescript
29356
+ * import { getTotalCostClosed } from "backtest-kit";
29357
+ *
29358
+ * const heldCost = await getTotalCostClosed("BTCUSDT");
29359
+ * console.log(`Holding $${heldCost} of position`);
29360
+ * ```
29361
+ */
29362
+ async function getTotalCostClosed(symbol) {
29363
+ bt.loggerService.info(GET_TOTAL_COST_CLOSED_METHOD_NAME, {
29364
+ symbol,
29365
+ });
29366
+ if (!ExecutionContextService.hasContext()) {
29367
+ throw new Error("getTotalCostClosed requires an execution context");
29368
+ }
29369
+ if (!MethodContextService.hasContext()) {
29370
+ throw new Error("getTotalCostClosed requires a method context");
29371
+ }
29372
+ const { backtest: isBacktest } = bt.executionContextService.context;
29373
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29374
+ return await bt.strategyCoreService.getTotalCostClosed(isBacktest, symbol, { exchangeName, frameName, strategyName });
29375
+ }
29376
+ /**
29377
+ * Returns the currently active pending signal for the strategy.
29378
+ * If no active signal exists, returns null.
29379
+ *
29380
+ * Automatically detects backtest/live mode from execution context.
29381
+ *
29382
+ * @param symbol - Trading pair symbol
29383
+ * @returns Promise resolving to pending signal or null
29384
+ *
29385
+ * @example
29386
+ * ```typescript
29387
+ * import { getPendingSignal } from "backtest-kit";
29388
+ *
29389
+ * const pending = await getPendingSignal("BTCUSDT");
29390
+ * if (pending) {
29391
+ * console.log("Active signal:", pending.id);
29392
+ * }
29393
+ * ```
29394
+ */
29395
+ async function getPendingSignal(symbol) {
29396
+ bt.loggerService.info(GET_PENDING_SIGNAL_METHOD_NAME, {
29397
+ symbol,
29398
+ });
29399
+ if (!ExecutionContextService.hasContext()) {
29400
+ throw new Error("getPendingSignal requires an execution context");
29401
+ }
29402
+ if (!MethodContextService.hasContext()) {
29403
+ throw new Error("getPendingSignal requires a method context");
29404
+ }
29405
+ const { backtest: isBacktest } = bt.executionContextService.context;
29406
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29407
+ return await bt.strategyCoreService.getPendingSignal(isBacktest, symbol, { exchangeName, frameName, strategyName });
29408
+ }
29409
+ /**
29410
+ * Returns the currently active scheduled signal for the strategy.
29411
+ * If no scheduled signal exists, returns null.
29412
+ *
29413
+ * Automatically detects backtest/live mode from execution context.
29414
+ *
29415
+ * @param symbol - Trading pair symbol
29416
+ * @returns Promise resolving to scheduled signal or null
29417
+ *
29418
+ * @example
29419
+ * ```typescript
29420
+ * import { getScheduledSignal } from "backtest-kit";
29421
+ *
29422
+ * const scheduled = await getScheduledSignal("BTCUSDT");
29423
+ * if (scheduled) {
29424
+ * console.log("Scheduled signal:", scheduled.id);
29425
+ * }
29426
+ * ```
29427
+ */
29428
+ async function getScheduledSignal(symbol) {
29429
+ bt.loggerService.info(GET_SCHEDULED_SIGNAL_METHOD_NAME, {
29430
+ symbol,
29431
+ });
29432
+ if (!ExecutionContextService.hasContext()) {
29433
+ throw new Error("getScheduledSignal requires an execution context");
29434
+ }
29435
+ if (!MethodContextService.hasContext()) {
29436
+ throw new Error("getScheduledSignal requires a method context");
29437
+ }
29438
+ const { backtest: isBacktest } = bt.executionContextService.context;
29439
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29440
+ return await bt.strategyCoreService.getScheduledSignal(isBacktest, symbol, { exchangeName, frameName, strategyName });
29441
+ }
29442
+ /**
29443
+ * Checks if breakeven threshold has been reached for the current pending signal.
29444
+ *
29445
+ * Returns true if price has moved far enough in profit direction to cover
29446
+ * transaction costs. Threshold is calculated as: (CC_PERCENT_SLIPPAGE + CC_PERCENT_FEE) * 2
29447
+ *
29448
+ * Automatically detects backtest/live mode from execution context.
29449
+ *
29450
+ * @param symbol - Trading pair symbol
29451
+ * @param currentPrice - Current market price to check against threshold
29452
+ * @returns Promise<boolean> - true if breakeven threshold reached, false otherwise
29453
+ *
29454
+ * @example
29455
+ * ```typescript
29456
+ * import { getBreakeven, getAveragePrice } from "backtest-kit";
29457
+ *
29458
+ * const price = await getAveragePrice("BTCUSDT");
29459
+ * const canBreakeven = await getBreakeven("BTCUSDT", price);
29460
+ * if (canBreakeven) {
29461
+ * console.log("Breakeven available");
29462
+ * }
29463
+ * ```
29464
+ */
29465
+ async function getBreakeven(symbol, currentPrice) {
29466
+ bt.loggerService.info(GET_BREAKEVEN_METHOD_NAME, {
29467
+ symbol,
29468
+ currentPrice,
29469
+ });
29470
+ if (!ExecutionContextService.hasContext()) {
29471
+ throw new Error("getBreakeven requires an execution context");
29472
+ }
29473
+ if (!MethodContextService.hasContext()) {
29474
+ throw new Error("getBreakeven requires a method context");
29475
+ }
29476
+ const { backtest: isBacktest } = bt.executionContextService.context;
29477
+ const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
29478
+ return await bt.strategyCoreService.getBreakeven(isBacktest, symbol, currentPrice, { exchangeName, frameName, strategyName });
29479
+ }
28974
29480
 
28975
29481
  const STOP_STRATEGY_METHOD_NAME = "control.stopStrategy";
28976
29482
  /**
@@ -30158,6 +30664,8 @@ const BACKTEST_METHOD_NAME_DUMP = "BacktestUtils.dump";
30158
30664
  const BACKTEST_METHOD_NAME_TASK = "BacktestUtils.task";
30159
30665
  const BACKTEST_METHOD_NAME_GET_STATUS = "BacktestUtils.getStatus";
30160
30666
  const BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL = "BacktestUtils.getPendingSignal";
30667
+ const BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "BacktestUtils.getTotalPercentClosed";
30668
+ const BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED = "BacktestUtils.getTotalCostClosed";
30161
30669
  const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
30162
30670
  const BACKTEST_METHOD_NAME_GET_BREAKEVEN = "BacktestUtils.getBreakeven";
30163
30671
  const BACKTEST_METHOD_NAME_BREAKEVEN = "Backtest.commitBreakeven";
@@ -30559,6 +31067,71 @@ class BacktestUtils {
30559
31067
  }
30560
31068
  return await bt.strategyCoreService.getPendingSignal(true, symbol, context);
30561
31069
  };
31070
+ /**
31071
+ * Returns the percentage of the position currently held (not closed).
31072
+ * 100 = nothing has been closed (full position), 0 = fully closed.
31073
+ * Correctly accounts for DCA entries between partial closes.
31074
+ *
31075
+ * @param symbol - Trading pair symbol
31076
+ * @param context - Context with strategyName, exchangeName, frameName
31077
+ * @returns Promise<number> - held percentage (0–100)
31078
+ *
31079
+ * @example
31080
+ * ```typescript
31081
+ * const heldPct = await Backtest.getTotalPercentClosed("BTCUSDT", { strategyName, exchangeName, frameName });
31082
+ * console.log(`Holding ${heldPct}% of position`);
31083
+ * ```
31084
+ */
31085
+ this.getTotalPercentClosed = async (symbol, context) => {
31086
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED, {
31087
+ symbol,
31088
+ context,
31089
+ });
31090
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
31091
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
31092
+ {
31093
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31094
+ riskName &&
31095
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
31096
+ riskList &&
31097
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
31098
+ actions &&
31099
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
31100
+ }
31101
+ return await bt.strategyCoreService.getTotalPercentClosed(true, symbol, context);
31102
+ };
31103
+ /**
31104
+ * Returns the cost basis in dollars of the position currently held (not closed).
31105
+ * Correctly accounts for DCA entries between partial closes.
31106
+ *
31107
+ * @param symbol - Trading pair symbol
31108
+ * @param context - Context with strategyName, exchangeName, frameName
31109
+ * @returns Promise<number> - held cost basis in dollars
31110
+ *
31111
+ * @example
31112
+ * ```typescript
31113
+ * const heldCost = await Backtest.getTotalCostClosed("BTCUSDT", { strategyName, exchangeName, frameName });
31114
+ * console.log(`Holding $${heldCost} of position`);
31115
+ * ```
31116
+ */
31117
+ this.getTotalCostClosed = async (symbol, context) => {
31118
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED, {
31119
+ symbol,
31120
+ context,
31121
+ });
31122
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
31123
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
31124
+ {
31125
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
31126
+ riskName &&
31127
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED);
31128
+ riskList &&
31129
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED));
31130
+ actions &&
31131
+ actions.forEach((actionName) => bt.actionValidationService.validate(actionName, BACKTEST_METHOD_NAME_GET_TOTAL_COST_CLOSED));
31132
+ }
31133
+ return await bt.strategyCoreService.getTotalCostClosed(true, symbol, context);
31134
+ };
30562
31135
  /**
30563
31136
  * Retrieves the currently active scheduled signal for the strategy.
30564
31137
  * If no scheduled signal exists, returns null.
@@ -31274,6 +31847,8 @@ const LIVE_METHOD_NAME_DUMP = "LiveUtils.dump";
31274
31847
  const LIVE_METHOD_NAME_TASK = "LiveUtils.task";
31275
31848
  const LIVE_METHOD_NAME_GET_STATUS = "LiveUtils.getStatus";
31276
31849
  const LIVE_METHOD_NAME_GET_PENDING_SIGNAL = "LiveUtils.getPendingSignal";
31850
+ const LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED = "LiveUtils.getTotalPercentClosed";
31851
+ const LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED = "LiveUtils.getTotalCostClosed";
31277
31852
  const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
31278
31853
  const LIVE_METHOD_NAME_GET_BREAKEVEN = "LiveUtils.getBreakeven";
31279
31854
  const LIVE_METHOD_NAME_BREAKEVEN = "Live.commitBreakeven";
@@ -31644,6 +32219,73 @@ class LiveUtils {
31644
32219
  frameName: "",
31645
32220
  });
31646
32221
  };
32222
+ /**
32223
+ * Returns the percentage of the position currently held (not closed).
32224
+ * 100 = nothing has been closed (full position), 0 = fully closed.
32225
+ * Correctly accounts for DCA entries between partial closes.
32226
+ *
32227
+ * @param symbol - Trading pair symbol
32228
+ * @param context - Context with strategyName and exchangeName
32229
+ * @returns Promise<number> - held percentage (0–100)
32230
+ *
32231
+ * @example
32232
+ * ```typescript
32233
+ * const heldPct = await Live.getTotalPercentClosed("BTCUSDT", { strategyName, exchangeName });
32234
+ * console.log(`Holding ${heldPct}% of position`);
32235
+ * ```
32236
+ */
32237
+ this.getTotalPercentClosed = async (symbol, context) => {
32238
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED, {
32239
+ symbol,
32240
+ context,
32241
+ });
32242
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
32243
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
32244
+ {
32245
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
32246
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED);
32247
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
32248
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_PERCENT_CLOSED));
32249
+ }
32250
+ return await bt.strategyCoreService.getTotalPercentClosed(false, symbol, {
32251
+ strategyName: context.strategyName,
32252
+ exchangeName: context.exchangeName,
32253
+ frameName: "",
32254
+ });
32255
+ };
32256
+ /**
32257
+ * Returns the cost basis in dollars of the position currently held (not closed).
32258
+ * Correctly accounts for DCA entries between partial closes.
32259
+ *
32260
+ * @param symbol - Trading pair symbol
32261
+ * @param context - Context with strategyName and exchangeName
32262
+ * @returns Promise<number> - held cost basis in dollars
32263
+ *
32264
+ * @example
32265
+ * ```typescript
32266
+ * const heldCost = await Live.getTotalCostClosed("BTCUSDT", { strategyName, exchangeName });
32267
+ * console.log(`Holding $${heldCost} of position`);
32268
+ * ```
32269
+ */
32270
+ this.getTotalCostClosed = async (symbol, context) => {
32271
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED, {
32272
+ symbol,
32273
+ context,
32274
+ });
32275
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
32276
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
32277
+ {
32278
+ const { riskName, riskList, actions } = bt.strategySchemaService.get(context.strategyName);
32279
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED);
32280
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
32281
+ actions && actions.forEach((actionName) => bt.actionValidationService.validate(actionName, LIVE_METHOD_NAME_GET_TOTAL_COST_CLOSED));
32282
+ }
32283
+ return await bt.strategyCoreService.getTotalCostClosed(false, symbol, {
32284
+ strategyName: context.strategyName,
32285
+ exchangeName: context.exchangeName,
32286
+ frameName: "",
32287
+ });
32288
+ };
31647
32289
  /**
31648
32290
  * Retrieves the currently active scheduled signal for the strategy.
31649
32291
  * If no scheduled signal exists, returns null.
@@ -32561,7 +33203,6 @@ const ADD_ACTION_METHOD_NAME = "add.addActionSchema";
32561
33203
  * priceTakeProfit: 51000,
32562
33204
  * priceStopLoss: 49000,
32563
33205
  * minuteEstimatedTime: 60,
32564
- * timestamp: Date.now(),
32565
33206
  * }),
32566
33207
  * callbacks: {
32567
33208
  * onOpen: (symbol, signal, currentPrice, backtest) => console.log("Signal opened"),
@@ -33488,8 +34129,8 @@ const WAIT_FOR_INIT_SYMBOL = Symbol("wait-for-init");
33488
34129
  const WRITE_SAFE_SYMBOL = Symbol("write-safe");
33489
34130
  /**
33490
34131
  * Backtest execution time retrieval function.
33491
- * Returns the 'when' timestamp from the execution context if available, otherwise returns the current time.
33492
- * 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.
34132
+ * Returns the 'when' priority from the execution context if available, otherwise returns the current time.
34133
+ * 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.
33493
34134
  */
33494
34135
  const GET_DATE_FN = () => {
33495
34136
  if (ExecutionContextService.hasContext()) {
@@ -33511,7 +34152,7 @@ const GET_METHOD_CONTEXT_FN = () => {
33511
34152
  /**
33512
34153
  * Execution context retrieval function.
33513
34154
  * Returns the current execution context from ExecutionContextService if available, otherwise returns null.
33514
- * 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.
34155
+ * 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.
33515
34156
  */
33516
34157
  const GET_EXECUTION_CONTEXT_FN = () => {
33517
34158
  if (ExecutionContextService.hasContext()) {
@@ -33541,7 +34182,7 @@ class LogPersistUtils {
33541
34182
  this.waitForInit = functoolsKit.singleshot(async () => {
33542
34183
  bt.loggerService.info(LOG_PERSIST_METHOD_NAME_WAIT_FOR_INIT);
33543
34184
  const list = await PersistLogAdapter.readLogData();
33544
- list.sort((a, b) => a.timestamp - b.timestamp);
34185
+ list.sort((a, b) => a.priority - b.priority);
33545
34186
  this._entries = list.slice(-GLOBAL_CONFIG.CC_MAX_LOG_LINES);
33546
34187
  });
33547
34188
  /**
@@ -33557,7 +34198,8 @@ class LogPersistUtils {
33557
34198
  this._entries.push({
33558
34199
  id: functoolsKit.randomString(),
33559
34200
  type: "log",
33560
- timestamp: Date.now(),
34201
+ priority: Date.now(),
34202
+ timestamp: getContextTimestamp(),
33561
34203
  createdAt: date.toISOString(),
33562
34204
  methodContext: GET_METHOD_CONTEXT_FN(),
33563
34205
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33580,7 +34222,8 @@ class LogPersistUtils {
33580
34222
  this._entries.push({
33581
34223
  id: functoolsKit.randomString(),
33582
34224
  type: "debug",
33583
- timestamp: Date.now(),
34225
+ priority: Date.now(),
34226
+ timestamp: getContextTimestamp(),
33584
34227
  createdAt: date.toISOString(),
33585
34228
  methodContext: GET_METHOD_CONTEXT_FN(),
33586
34229
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33603,7 +34246,8 @@ class LogPersistUtils {
33603
34246
  this._entries.push({
33604
34247
  id: functoolsKit.randomString(),
33605
34248
  type: "info",
33606
- timestamp: Date.now(),
34249
+ priority: Date.now(),
34250
+ timestamp: getContextTimestamp(),
33607
34251
  createdAt: date.toISOString(),
33608
34252
  methodContext: GET_METHOD_CONTEXT_FN(),
33609
34253
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33626,7 +34270,8 @@ class LogPersistUtils {
33626
34270
  this._entries.push({
33627
34271
  id: functoolsKit.randomString(),
33628
34272
  type: "warn",
33629
- timestamp: Date.now(),
34273
+ priority: Date.now(),
34274
+ timestamp: getContextTimestamp(),
33630
34275
  createdAt: date.toISOString(),
33631
34276
  methodContext: GET_METHOD_CONTEXT_FN(),
33632
34277
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33682,7 +34327,8 @@ class LogMemoryUtils {
33682
34327
  this._entries.push({
33683
34328
  id: functoolsKit.randomString(),
33684
34329
  type: "log",
33685
- timestamp: Date.now(),
34330
+ priority: Date.now(),
34331
+ timestamp: getContextTimestamp(),
33686
34332
  createdAt: date.toISOString(),
33687
34333
  methodContext: GET_METHOD_CONTEXT_FN(),
33688
34334
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33703,7 +34349,8 @@ class LogMemoryUtils {
33703
34349
  this._entries.push({
33704
34350
  id: functoolsKit.randomString(),
33705
34351
  type: "debug",
33706
- timestamp: Date.now(),
34352
+ priority: Date.now(),
34353
+ timestamp: getContextTimestamp(),
33707
34354
  createdAt: date.toISOString(),
33708
34355
  methodContext: GET_METHOD_CONTEXT_FN(),
33709
34356
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33724,7 +34371,8 @@ class LogMemoryUtils {
33724
34371
  this._entries.push({
33725
34372
  id: functoolsKit.randomString(),
33726
34373
  type: "info",
33727
- timestamp: Date.now(),
34374
+ priority: Date.now(),
34375
+ timestamp: getContextTimestamp(),
33728
34376
  createdAt: date.toISOString(),
33729
34377
  methodContext: GET_METHOD_CONTEXT_FN(),
33730
34378
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33745,7 +34393,8 @@ class LogMemoryUtils {
33745
34393
  this._entries.push({
33746
34394
  id: functoolsKit.randomString(),
33747
34395
  type: "warn",
33748
- timestamp: Date.now(),
34396
+ priority: Date.now(),
34397
+ timestamp: getContextTimestamp(),
33749
34398
  createdAt: date.toISOString(),
33750
34399
  methodContext: GET_METHOD_CONTEXT_FN(),
33751
34400
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33859,7 +34508,8 @@ class LogJsonlUtils {
33859
34508
  await this._append({
33860
34509
  id: functoolsKit.randomString(),
33861
34510
  type: "log",
33862
- timestamp: Date.now(),
34511
+ priority: Date.now(),
34512
+ timestamp: getContextTimestamp(),
33863
34513
  createdAt: date.toISOString(),
33864
34514
  methodContext: GET_METHOD_CONTEXT_FN(),
33865
34515
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33878,7 +34528,8 @@ class LogJsonlUtils {
33878
34528
  await this._append({
33879
34529
  id: functoolsKit.randomString(),
33880
34530
  type: "debug",
33881
- timestamp: Date.now(),
34531
+ priority: Date.now(),
34532
+ timestamp: getContextTimestamp(),
33882
34533
  createdAt: date.toISOString(),
33883
34534
  methodContext: GET_METHOD_CONTEXT_FN(),
33884
34535
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33897,7 +34548,8 @@ class LogJsonlUtils {
33897
34548
  await this._append({
33898
34549
  id: functoolsKit.randomString(),
33899
34550
  type: "info",
33900
- timestamp: Date.now(),
34551
+ priority: Date.now(),
34552
+ timestamp: getContextTimestamp(),
33901
34553
  createdAt: date.toISOString(),
33902
34554
  methodContext: GET_METHOD_CONTEXT_FN(),
33903
34555
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -33916,7 +34568,8 @@ class LogJsonlUtils {
33916
34568
  await this._append({
33917
34569
  id: functoolsKit.randomString(),
33918
34570
  type: "warn",
33919
- timestamp: Date.now(),
34571
+ priority: Date.now(),
34572
+ timestamp: getContextTimestamp(),
33920
34573
  createdAt: date.toISOString(),
33921
34574
  methodContext: GET_METHOD_CONTEXT_FN(),
33922
34575
  executionContext: GET_EXECUTION_CONTEXT_FN(),
@@ -38343,6 +38996,8 @@ const CACHE_METHOD_NAME_CLEAR = "CacheInstance.clear";
38343
38996
  const CACHE_METHOD_NAME_RUN = "CacheInstance.run";
38344
38997
  const CACHE_METHOD_NAME_GC = "CacheInstance.gc";
38345
38998
  const CACHE_METHOD_NAME_FN = "CacheUtils.fn";
38999
+ const CACHE_METHOD_NAME_FILE = "CacheUtils.file";
39000
+ const CACHE_FILE_INSTANCE_METHOD_NAME_RUN = "CacheFileInstance.run";
38346
39001
  const MS_PER_MINUTE$1 = 60000;
38347
39002
  const INTERVAL_MINUTES$1 = {
38348
39003
  "1m": 1,
@@ -38564,6 +39219,95 @@ class CacheInstance {
38564
39219
  };
38565
39220
  }
38566
39221
  }
39222
+ /**
39223
+ * Instance class for caching async function results in persistent file storage.
39224
+ *
39225
+ * Provides automatic cache invalidation based on candle intervals.
39226
+ * Cache key = `${alignedTimestamp}_${symbol}` (bucket) + dynamic key (file within bucket).
39227
+ * On cache hit reads from disk, on miss calls the function and writes the result.
39228
+ *
39229
+ * @template T - Async function type to cache
39230
+ * @template K - Dynamic key type
39231
+ *
39232
+ * @example
39233
+ * ```typescript
39234
+ * const instance = new CacheFileInstance(fetchFromApi, "1h");
39235
+ * const result = await instance.run("BTCUSDT", extraArg);
39236
+ * ```
39237
+ */
39238
+ class CacheFileInstance {
39239
+ /**
39240
+ * Allocates a new unique index. Called once in the constructor to give each
39241
+ * CacheFileInstance its own namespace in the persistent key space.
39242
+ */
39243
+ static createIndex() {
39244
+ return CacheFileInstance._indexCounter++;
39245
+ }
39246
+ /**
39247
+ * Creates a new CacheFileInstance.
39248
+ *
39249
+ * @param fn - Async function to cache
39250
+ * @param interval - Candle interval for cache invalidation
39251
+ * @param name - Human-readable bucket name used as the directory key (replaces symbol in bucket path)
39252
+ * @param key - Dynamic key generator; receives all args, must return a string.
39253
+ * Default: `([symbol, alignMs]) => \`${symbol}_${alignMs}\``
39254
+ */
39255
+ constructor(fn, interval, name, key = ([symbol, alignMs]) => `${symbol}_${alignMs}`) {
39256
+ this.fn = fn;
39257
+ this.interval = interval;
39258
+ this.name = name;
39259
+ this.key = key;
39260
+ /**
39261
+ * Execute async function with persistent file caching.
39262
+ *
39263
+ * Algorithm:
39264
+ * 1. Build bucket = `${name}_${interval}_${index}` — fixed per instance, used as directory name
39265
+ * 2. Align execution context `when` to interval boundary → `alignedTs`
39266
+ * 3. Build entity key from the key generator (receives `[symbol, alignedTs, ...rest]`)
39267
+ * 4. Try to read from PersistMeasureAdapter using (bucket, entityKey)
39268
+ * 5. On hit — return cached value
39269
+ * 6. On miss — call fn, write result to disk, return result
39270
+ *
39271
+ * Cache invalidation happens through the entity key: the default key embeds `alignedTs`,
39272
+ * so each new candle interval produces a new file name while the bucket directory stays the same.
39273
+ *
39274
+ * Requires active execution context (symbol, when) and method context.
39275
+ *
39276
+ * @param args - Arguments forwarded to the wrapped function
39277
+ * @returns Cached or freshly computed result
39278
+ */
39279
+ this.run = async (...args) => {
39280
+ bt.loggerService.debug(CACHE_FILE_INSTANCE_METHOD_NAME_RUN, { args });
39281
+ const step = INTERVAL_MINUTES$1[this.interval];
39282
+ {
39283
+ if (!MethodContextService.hasContext()) {
39284
+ throw new Error("CacheFileInstance run requires method context");
39285
+ }
39286
+ if (!ExecutionContextService.hasContext()) {
39287
+ throw new Error("CacheFileInstance run requires execution context");
39288
+ }
39289
+ if (!step) {
39290
+ throw new Error(`CacheFileInstance unknown cache ttl interval=${this.interval}`);
39291
+ }
39292
+ }
39293
+ const [symbol, ...rest] = args;
39294
+ const { when } = bt.executionContextService.context;
39295
+ const alignedTs = align(when.getTime(), this.interval);
39296
+ const bucket = `${this.name}_${this.interval}_${this.index}`;
39297
+ const entityKey = this.key([symbol, alignedTs, ...rest]);
39298
+ const cached = await PersistMeasureAdapter.readMeasureData(bucket, entityKey);
39299
+ if (cached !== null) {
39300
+ return cached;
39301
+ }
39302
+ const result = await this.fn.call(null, ...args);
39303
+ await PersistMeasureAdapter.writeMeasureData(result, bucket, entityKey);
39304
+ return result;
39305
+ };
39306
+ this.index = CacheFileInstance.createIndex();
39307
+ }
39308
+ }
39309
+ /** Global counter — incremented once per CacheFileInstance construction */
39310
+ CacheFileInstance._indexCounter = 0;
38567
39311
  /**
38568
39312
  * Utility class for function caching with timeframe-based invalidation.
38569
39313
  *
@@ -38585,7 +39329,12 @@ class CacheUtils {
38585
39329
  * Memoized function to get or create CacheInstance for a function.
38586
39330
  * Each function gets its own isolated cache instance.
38587
39331
  */
38588
- this._getInstance = functoolsKit.memoize(([run]) => run, (run, interval, key) => new CacheInstance(run, interval, key));
39332
+ this._getFnInstance = functoolsKit.memoize(([run]) => run, (run, interval, key) => new CacheInstance(run, interval, key));
39333
+ /**
39334
+ * Memoized function to get or create CacheFileInstance for an async function.
39335
+ * Each function gets its own isolated file-cache instance.
39336
+ */
39337
+ this._getFileInstance = functoolsKit.memoize(([run]) => run, (run, interval, name, key) => new CacheFileInstance(run, interval, name, key));
38589
39338
  /**
38590
39339
  * Wrap a function with caching based on timeframe intervals.
38591
39340
  *
@@ -38624,11 +39373,57 @@ class CacheUtils {
38624
39373
  context,
38625
39374
  });
38626
39375
  const wrappedFn = (...args) => {
38627
- const instance = this._getInstance(run, context.interval, context.key);
39376
+ const instance = this._getFnInstance(run, context.interval, context.key);
38628
39377
  return instance.run(...args).value;
38629
39378
  };
38630
39379
  return wrappedFn;
38631
39380
  };
39381
+ /**
39382
+ * Wrap an async function with persistent file-based caching.
39383
+ *
39384
+ * Returns a wrapped version of the function that reads from disk on cache hit
39385
+ * and writes the result to disk on cache miss. Files are stored under
39386
+ * `./dump/data/measure/{name}_{interval}_{index}/`.
39387
+ *
39388
+ * The `run` function reference is used as the memoization key for the underlying
39389
+ * `CacheFileInstance`, so each unique function reference gets its own isolated instance.
39390
+ * Pass the same function reference each time to reuse the same cache.
39391
+ *
39392
+ * @template T - Async function type to cache
39393
+ * @param run - Async function to wrap with file caching
39394
+ * @param context.interval - Candle interval for cache invalidation
39395
+ * @param context.name - Human-readable bucket name; becomes the directory prefix
39396
+ * @param context.key - Optional entity key generator. Receives `[symbol, alignMs, ...rest]`
39397
+ * where `alignMs` is the timestamp aligned to `interval`.
39398
+ * Default: `([symbol, alignMs]) => \`${symbol}_${alignMs}\``
39399
+ * @returns Wrapped async function with automatic persistent caching
39400
+ *
39401
+ * @example
39402
+ * ```typescript
39403
+ * const fetchData = async (symbol: string, period: number) => {
39404
+ * return await externalApi.fetch(symbol, period);
39405
+ * };
39406
+ *
39407
+ * // Default key — one cache file per symbol per aligned candle
39408
+ * const cachedFetch = Cache.file(fetchData, { interval: "1h", name: "fetchData" });
39409
+ *
39410
+ * // Custom key — one cache file per symbol + period combination
39411
+ * const cachedFetch = Cache.file(fetchData, {
39412
+ * interval: "1h",
39413
+ * name: "fetchData",
39414
+ * key: ([symbol, alignMs, period]) => `${symbol}_${alignMs}_${period}`,
39415
+ * });
39416
+ * const result = await cachedFetch("BTCUSDT", 14);
39417
+ * ```
39418
+ */
39419
+ this.file = (run, context) => {
39420
+ bt.loggerService.info(CACHE_METHOD_NAME_FILE, { context });
39421
+ const wrappedFn = (...args) => {
39422
+ const instance = this._getFileInstance(run, context.interval, context.name, context.key);
39423
+ return instance.run(...args);
39424
+ };
39425
+ return wrappedFn;
39426
+ };
38632
39427
  /**
38633
39428
  * Flush (remove) cached CacheInstance for a specific function or all functions.
38634
39429
  *
@@ -38662,7 +39457,7 @@ class CacheUtils {
38662
39457
  bt.loggerService.info(CACHE_METHOD_NAME_FLUSH, {
38663
39458
  run,
38664
39459
  });
38665
- this._getInstance.clear(run);
39460
+ this._getFnInstance.clear(run);
38666
39461
  };
38667
39462
  /**
38668
39463
  * Clear cached value for current execution context of a specific function.
@@ -38705,7 +39500,7 @@ class CacheUtils {
38705
39500
  console.warn(`${CACHE_METHOD_NAME_CLEAR} called without execution context, skipping clear`);
38706
39501
  return;
38707
39502
  }
38708
- this._getInstance.get(run).clear();
39503
+ this._getFnInstance.get(run).clear();
38709
39504
  };
38710
39505
  /**
38711
39506
  * Garbage collect expired cache entries for a specific function.
@@ -38737,7 +39532,7 @@ class CacheUtils {
38737
39532
  console.warn(`${CACHE_METHOD_NAME_GC} called without execution context, skipping garbage collection`);
38738
39533
  return;
38739
39534
  }
38740
- return this._getInstance.get(run).gc();
39535
+ return this._getFnInstance.get(run).gc();
38741
39536
  };
38742
39537
  }
38743
39538
  }
@@ -39330,6 +40125,7 @@ exports.PersistBase = PersistBase;
39330
40125
  exports.PersistBreakevenAdapter = PersistBreakevenAdapter;
39331
40126
  exports.PersistCandleAdapter = PersistCandleAdapter;
39332
40127
  exports.PersistLogAdapter = PersistLogAdapter;
40128
+ exports.PersistMeasureAdapter = PersistMeasureAdapter;
39333
40129
  exports.PersistNotificationAdapter = PersistNotificationAdapter;
39334
40130
  exports.PersistPartialAdapter = PersistPartialAdapter;
39335
40131
  exports.PersistRiskAdapter = PersistRiskAdapter;
@@ -39373,6 +40169,7 @@ exports.getActionSchema = getActionSchema;
39373
40169
  exports.getAggregatedTrades = getAggregatedTrades;
39374
40170
  exports.getAveragePrice = getAveragePrice;
39375
40171
  exports.getBacktestTimeframe = getBacktestTimeframe;
40172
+ exports.getBreakeven = getBreakeven;
39376
40173
  exports.getCandles = getCandles;
39377
40174
  exports.getColumns = getColumns;
39378
40175
  exports.getConfig = getConfig;
@@ -39380,16 +40177,23 @@ exports.getContext = getContext;
39380
40177
  exports.getDate = getDate;
39381
40178
  exports.getDefaultColumns = getDefaultColumns;
39382
40179
  exports.getDefaultConfig = getDefaultConfig;
40180
+ exports.getEffectivePriceOpen = getEffectivePriceOpen;
39383
40181
  exports.getExchangeSchema = getExchangeSchema;
39384
40182
  exports.getFrameSchema = getFrameSchema;
39385
40183
  exports.getMode = getMode;
39386
40184
  exports.getNextCandles = getNextCandles;
39387
40185
  exports.getOrderBook = getOrderBook;
40186
+ exports.getPendingSignal = getPendingSignal;
39388
40187
  exports.getRawCandles = getRawCandles;
39389
40188
  exports.getRiskSchema = getRiskSchema;
40189
+ exports.getScheduledSignal = getScheduledSignal;
39390
40190
  exports.getSizingSchema = getSizingSchema;
39391
40191
  exports.getStrategySchema = getStrategySchema;
39392
40192
  exports.getSymbol = getSymbol;
40193
+ exports.getTimestamp = getTimestamp;
40194
+ exports.getTotalClosed = getTotalClosed;
40195
+ exports.getTotalCostClosed = getTotalCostClosed;
40196
+ exports.getTotalPercentClosed = getTotalPercentClosed;
39393
40197
  exports.getWalkerSchema = getWalkerSchema;
39394
40198
  exports.hasTradeContext = hasTradeContext;
39395
40199
  exports.lib = backtest;
@@ -39449,6 +40253,7 @@ exports.setConfig = setConfig;
39449
40253
  exports.setLogger = setLogger;
39450
40254
  exports.shutdown = shutdown;
39451
40255
  exports.stopStrategy = stopStrategy;
40256
+ exports.toProfitLossDto = toProfitLossDto;
39452
40257
  exports.validate = validate;
39453
40258
  exports.waitForCandle = waitForCandle;
39454
40259
  exports.warmCandles = warmCandles;