backtest-kit 9.6.0 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -36127,72 +36127,41 @@ const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
36127
36127
  }
36128
36128
  };
36129
36129
  /**
36130
- * Checks cached candle timestamps for correct interval alignment.
36131
- * Reads JSON files directly from persist storage without using abstractions.
36130
+ * Checks cached candle presence via the persist adapter.
36131
+ * Issues one ranged read; adapter-side `hasValue` covers each expected timestamp,
36132
+ * so a single missing or unaligned candle yields a miss without loading the whole dataset.
36132
36133
  *
36133
36134
  * @param params - Validation parameters
36134
36135
  */
36135
36136
  async function checkCandles(params) {
36136
- const { symbol, exchangeName, interval, from, to, baseDir = "./dump/data/candle" } = params;
36137
+ const { symbol, exchangeName, interval, from, to } = params;
36137
36138
  backtest.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
36138
36139
  const step = INTERVAL_MINUTES$3[interval];
36139
36140
  if (!step) {
36140
36141
  throw new Error(`checkCandles: unsupported interval=${interval}`);
36141
36142
  }
36142
36143
  const stepMs = step * MS_PER_MINUTE$3;
36143
- const dir = path.join(baseDir, exchangeName, symbol, interval);
36144
36144
  const fromTs = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
36145
36145
  const toTs = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
36146
- try {
36147
- await fs.stat(dir);
36148
- }
36149
- catch {
36150
- throw new Error(`checkCandles: cache directory not found: ${dir}`);
36151
- }
36152
- // Collect only filenames (strings) in range via async iterator — no full readdir in memory
36153
- const files = [];
36154
- for await (const entry of await fs.opendir(dir)) {
36155
- if (!entry.isFile() || !entry.name.endsWith(".json")) {
36156
- continue;
36157
- }
36158
- const ts = Number(entry.name.replace(".json", ""));
36159
- if (ts >= fromTs && ts < toTs) {
36160
- files.push(entry.name);
36161
- }
36162
- }
36163
- if (files.length === 0) {
36164
- throw new Error(`checkCandles: no cached candles in range [${fromTs}, ${toTs}) in ${dir}`);
36165
- }
36166
- files.sort();
36167
- let prevTimestamp = null;
36168
- let prevName = null;
36169
- PRINT_PROGRESS_FN(0, files.length, symbol, interval);
36170
- for (let i = 0; i < files.length; i++) {
36171
- const filePath = path.join(dir, files[i]);
36172
- const raw = await fs.readFile(filePath, "utf-8");
36173
- let candle;
36174
- try {
36175
- candle = JSON.parse(raw);
36176
- }
36177
- catch {
36178
- throw new Error(`checkCandles: ${files[i]} contains invalid JSON`);
36179
- }
36180
- const { timestamp } = candle;
36181
- const aligned = ALIGN_TO_INTERVAL_FN(timestamp, step);
36182
- if (timestamp !== aligned) {
36183
- throw new Error(`checkCandles: ${files[i]} timestamp not aligned to ${interval} boundary (actual=${timestamp}, expected=${aligned})`);
36184
- }
36185
- if (prevTimestamp !== null) {
36186
- const gap = timestamp - prevTimestamp;
36187
- if (gap !== stepMs) {
36188
- throw new Error(`checkCandles: gap between ${prevName} and ${files[i]} (actual=${gap}ms, expected=${stepMs}ms)`);
36189
- }
36190
- }
36191
- prevTimestamp = timestamp;
36192
- prevName = files[i];
36193
- PRINT_PROGRESS_FN(i + 1, files.length, symbol, interval);
36194
- }
36195
- console.log(`checkCandles: OK ${files.length} candles ${symbol} ${interval}`);
36146
+ const totalCandles = Math.floor((toTs - fromTs) / stepMs);
36147
+ if (totalCandles <= 0) {
36148
+ throw new Error(`checkCandles: empty range [${fromTs}, ${toTs}) for ${symbol} ${interval}`);
36149
+ }
36150
+ let checked = 0;
36151
+ let currentSince = fromTs;
36152
+ PRINT_PROGRESS_FN(checked, totalCandles, symbol, interval);
36153
+ while (checked < totalCandles) {
36154
+ const chunkLimit = Math.min(totalCandles - checked, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
36155
+ const chunkUntil = currentSince + chunkLimit * stepMs;
36156
+ const candles = await PersistCandleAdapter.readCandlesData(symbol, interval, exchangeName, chunkLimit, currentSince, chunkUntil);
36157
+ if (!candles) {
36158
+ throw new Error(`checkCandles: cache miss for ${symbol} ${interval} [${currentSince}, ${chunkUntil})`);
36159
+ }
36160
+ checked += chunkLimit;
36161
+ currentSince = chunkUntil;
36162
+ PRINT_PROGRESS_FN(checked, totalCandles, symbol, interval);
36163
+ }
36164
+ console.log(`checkCandles: OK ${totalCandles} candles ${symbol} ${interval}`);
36196
36165
  }
36197
36166
  /**
36198
36167
  * Pre-caches candles for a date range into persist storage.
package/build/index.mjs CHANGED
@@ -3,7 +3,7 @@ import { scoped } from 'di-scoped';
3
3
  import { singleton } from 'di-singleton';
4
4
  import { Subject, makeExtendable, singleshot, getErrorMessage, memoize, not, errorData, trycatch, retry, queued, sleep, randomString, str, isObject, ToolRegistry, typo, and, Source, resolveDocuments, timeout, TIMEOUT_SYMBOL as TIMEOUT_SYMBOL$1, compose, BehaviorSubject, waitForNext, singlerun } from 'functools-kit';
5
5
  import * as fs from 'fs/promises';
6
- import fs__default, { stat, opendir, readFile } from 'fs/promises';
6
+ import fs__default from 'fs/promises';
7
7
  import path, { join, dirname } from 'path';
8
8
  import crypto from 'crypto';
9
9
  import os from 'os';
@@ -36107,72 +36107,41 @@ const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
36107
36107
  }
36108
36108
  };
36109
36109
  /**
36110
- * Checks cached candle timestamps for correct interval alignment.
36111
- * Reads JSON files directly from persist storage without using abstractions.
36110
+ * Checks cached candle presence via the persist adapter.
36111
+ * Issues one ranged read; adapter-side `hasValue` covers each expected timestamp,
36112
+ * so a single missing or unaligned candle yields a miss without loading the whole dataset.
36112
36113
  *
36113
36114
  * @param params - Validation parameters
36114
36115
  */
36115
36116
  async function checkCandles(params) {
36116
- const { symbol, exchangeName, interval, from, to, baseDir = "./dump/data/candle" } = params;
36117
+ const { symbol, exchangeName, interval, from, to } = params;
36117
36118
  backtest.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
36118
36119
  const step = INTERVAL_MINUTES$3[interval];
36119
36120
  if (!step) {
36120
36121
  throw new Error(`checkCandles: unsupported interval=${interval}`);
36121
36122
  }
36122
36123
  const stepMs = step * MS_PER_MINUTE$3;
36123
- const dir = join(baseDir, exchangeName, symbol, interval);
36124
36124
  const fromTs = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
36125
36125
  const toTs = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
36126
- try {
36127
- await stat(dir);
36128
- }
36129
- catch {
36130
- throw new Error(`checkCandles: cache directory not found: ${dir}`);
36131
- }
36132
- // Collect only filenames (strings) in range via async iterator — no full readdir in memory
36133
- const files = [];
36134
- for await (const entry of await opendir(dir)) {
36135
- if (!entry.isFile() || !entry.name.endsWith(".json")) {
36136
- continue;
36137
- }
36138
- const ts = Number(entry.name.replace(".json", ""));
36139
- if (ts >= fromTs && ts < toTs) {
36140
- files.push(entry.name);
36141
- }
36142
- }
36143
- if (files.length === 0) {
36144
- throw new Error(`checkCandles: no cached candles in range [${fromTs}, ${toTs}) in ${dir}`);
36145
- }
36146
- files.sort();
36147
- let prevTimestamp = null;
36148
- let prevName = null;
36149
- PRINT_PROGRESS_FN(0, files.length, symbol, interval);
36150
- for (let i = 0; i < files.length; i++) {
36151
- const filePath = join(dir, files[i]);
36152
- const raw = await readFile(filePath, "utf-8");
36153
- let candle;
36154
- try {
36155
- candle = JSON.parse(raw);
36156
- }
36157
- catch {
36158
- throw new Error(`checkCandles: ${files[i]} contains invalid JSON`);
36159
- }
36160
- const { timestamp } = candle;
36161
- const aligned = ALIGN_TO_INTERVAL_FN(timestamp, step);
36162
- if (timestamp !== aligned) {
36163
- throw new Error(`checkCandles: ${files[i]} timestamp not aligned to ${interval} boundary (actual=${timestamp}, expected=${aligned})`);
36164
- }
36165
- if (prevTimestamp !== null) {
36166
- const gap = timestamp - prevTimestamp;
36167
- if (gap !== stepMs) {
36168
- throw new Error(`checkCandles: gap between ${prevName} and ${files[i]} (actual=${gap}ms, expected=${stepMs}ms)`);
36169
- }
36170
- }
36171
- prevTimestamp = timestamp;
36172
- prevName = files[i];
36173
- PRINT_PROGRESS_FN(i + 1, files.length, symbol, interval);
36174
- }
36175
- console.log(`checkCandles: OK ${files.length} candles ${symbol} ${interval}`);
36126
+ const totalCandles = Math.floor((toTs - fromTs) / stepMs);
36127
+ if (totalCandles <= 0) {
36128
+ throw new Error(`checkCandles: empty range [${fromTs}, ${toTs}) for ${symbol} ${interval}`);
36129
+ }
36130
+ let checked = 0;
36131
+ let currentSince = fromTs;
36132
+ PRINT_PROGRESS_FN(checked, totalCandles, symbol, interval);
36133
+ while (checked < totalCandles) {
36134
+ const chunkLimit = Math.min(totalCandles - checked, GLOBAL_CONFIG.CC_MAX_CANDLES_PER_REQUEST);
36135
+ const chunkUntil = currentSince + chunkLimit * stepMs;
36136
+ const candles = await PersistCandleAdapter.readCandlesData(symbol, interval, exchangeName, chunkLimit, currentSince, chunkUntil);
36137
+ if (!candles) {
36138
+ throw new Error(`checkCandles: cache miss for ${symbol} ${interval} [${currentSince}, ${chunkUntil})`);
36139
+ }
36140
+ checked += chunkLimit;
36141
+ currentSince = chunkUntil;
36142
+ PRINT_PROGRESS_FN(checked, totalCandles, symbol, interval);
36143
+ }
36144
+ console.log(`checkCandles: OK ${totalCandles} candles ${symbol} ${interval}`);
36176
36145
  }
36177
36146
  /**
36178
36147
  * Pre-caches candles for a date range into persist storage.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "9.6.0",
3
+ "version": "9.8.0",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -4406,8 +4406,8 @@ interface ICacheCandlesParams {
4406
4406
  to: Date;
4407
4407
  }
4408
4408
  /**
4409
- * Parameters for validating cached candle timestamps.
4410
- * Reads JSON files directly from persist storage directory.
4409
+ * Parameters for validating cached candle presence.
4410
+ * Queries persist storage adapter without scanning files.
4411
4411
  */
4412
4412
  interface ICheckCandlesParams {
4413
4413
  /** Trading pair symbol (e.g., "BTCUSDT") */
@@ -4420,12 +4420,11 @@ interface ICheckCandlesParams {
4420
4420
  from: Date;
4421
4421
  /** End date of the validation range (inclusive) */
4422
4422
  to: Date;
4423
- /** Base directory of candle persist storage (default: "./dump/data/candle") */
4424
- baseDir?: string;
4425
4423
  }
4426
4424
  /**
4427
- * Checks cached candle timestamps for correct interval alignment.
4428
- * Reads JSON files directly from persist storage without using abstractions.
4425
+ * Checks cached candle presence via the persist adapter.
4426
+ * Issues one ranged read; adapter-side `hasValue` covers each expected timestamp,
4427
+ * so a single missing or unaligned candle yields a miss without loading the whole dataset.
4429
4428
  *
4430
4429
  * @param params - Validation parameters
4431
4430
  */