backtest-kit 3.0.12 → 3.0.14
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 +36 -27
- package/build/index.mjs +36 -27
- package/package.json +1 -1
- package/types.d.ts +4 -0
package/build/index.cjs
CHANGED
|
@@ -914,11 +914,13 @@ class PersistBase {
|
|
|
914
914
|
entityName: this.entityName,
|
|
915
915
|
});
|
|
916
916
|
try {
|
|
917
|
-
const
|
|
918
|
-
const
|
|
919
|
-
.
|
|
920
|
-
|
|
921
|
-
|
|
917
|
+
const entityIds = [];
|
|
918
|
+
for await (const entry of await fs.opendir(this._directory)) {
|
|
919
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
920
|
+
entityIds.push(entry.name.slice(0, -5));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
entityIds.sort((a, b) => a.localeCompare(b, undefined, {
|
|
922
924
|
numeric: true,
|
|
923
925
|
sensitivity: "base",
|
|
924
926
|
}));
|
|
@@ -26807,7 +26809,7 @@ const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
|
|
|
26807
26809
|
* @param params - Validation parameters
|
|
26808
26810
|
*/
|
|
26809
26811
|
async function checkCandles(params) {
|
|
26810
|
-
const { symbol, exchangeName, interval, baseDir = "./dump/data/candle" } = params;
|
|
26812
|
+
const { symbol, exchangeName, interval, from, to, baseDir = "./dump/data/candle" } = params;
|
|
26811
26813
|
bt.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
|
|
26812
26814
|
const step = INTERVAL_MINUTES$1[interval];
|
|
26813
26815
|
if (!step) {
|
|
@@ -26815,51 +26817,58 @@ async function checkCandles(params) {
|
|
|
26815
26817
|
}
|
|
26816
26818
|
const stepMs = step * MS_PER_MINUTE$1;
|
|
26817
26819
|
const dir = path.join(baseDir, exchangeName, symbol, interval);
|
|
26820
|
+
const fromTs = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
|
|
26821
|
+
const toTs = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
|
|
26818
26822
|
try {
|
|
26819
26823
|
await fs.stat(dir);
|
|
26820
26824
|
}
|
|
26821
26825
|
catch {
|
|
26822
26826
|
throw new Error(`checkCandles: cache directory not found: ${dir}`);
|
|
26823
26827
|
}
|
|
26824
|
-
//
|
|
26825
|
-
|
|
26828
|
+
// Collect only filenames (strings) in range via async iterator — no full readdir in memory
|
|
26829
|
+
const files = [];
|
|
26826
26830
|
for await (const entry of await fs.opendir(dir)) {
|
|
26827
|
-
if (entry.isFile()
|
|
26828
|
-
|
|
26831
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
26832
|
+
continue;
|
|
26833
|
+
}
|
|
26834
|
+
const ts = Number(entry.name.replace(".json", ""));
|
|
26835
|
+
if (ts >= fromTs && ts < toTs) {
|
|
26836
|
+
files.push(entry.name);
|
|
26829
26837
|
}
|
|
26830
26838
|
}
|
|
26831
|
-
if (
|
|
26832
|
-
throw new Error(`checkCandles: no cached candles in ${dir}`);
|
|
26839
|
+
if (files.length === 0) {
|
|
26840
|
+
throw new Error(`checkCandles: no cached candles in range [${fromTs}, ${toTs}) in ${dir}`);
|
|
26833
26841
|
}
|
|
26834
|
-
|
|
26835
|
-
let checkd = 0;
|
|
26842
|
+
files.sort();
|
|
26836
26843
|
let prevTimestamp = null;
|
|
26837
26844
|
let prevName = null;
|
|
26838
|
-
PRINT_PROGRESS_FN(
|
|
26839
|
-
for
|
|
26840
|
-
|
|
26841
|
-
continue;
|
|
26842
|
-
}
|
|
26843
|
-
const filePath = path.join(dir, entry.name);
|
|
26845
|
+
PRINT_PROGRESS_FN(0, files.length, symbol, interval);
|
|
26846
|
+
for (let i = 0; i < files.length; i++) {
|
|
26847
|
+
const filePath = path.join(dir, files[i]);
|
|
26844
26848
|
const raw = await fs.readFile(filePath, "utf-8");
|
|
26845
|
-
|
|
26849
|
+
let candle;
|
|
26850
|
+
try {
|
|
26851
|
+
candle = JSON.parse(raw);
|
|
26852
|
+
}
|
|
26853
|
+
catch {
|
|
26854
|
+
throw new Error(`checkCandles: ${files[i]} contains invalid JSON`);
|
|
26855
|
+
}
|
|
26846
26856
|
const { timestamp } = candle;
|
|
26847
26857
|
const aligned = ALIGN_TO_INTERVAL_FN(timestamp, step);
|
|
26848
26858
|
if (timestamp !== aligned) {
|
|
26849
|
-
throw new Error(`checkCandles: ${
|
|
26859
|
+
throw new Error(`checkCandles: ${files[i]} timestamp not aligned to ${interval} boundary (actual=${timestamp}, expected=${aligned})`);
|
|
26850
26860
|
}
|
|
26851
26861
|
if (prevTimestamp !== null) {
|
|
26852
26862
|
const gap = timestamp - prevTimestamp;
|
|
26853
26863
|
if (gap !== stepMs) {
|
|
26854
|
-
throw new Error(`checkCandles: gap between ${prevName} and ${
|
|
26864
|
+
throw new Error(`checkCandles: gap between ${prevName} and ${files[i]} (actual=${gap}ms, expected=${stepMs}ms)`);
|
|
26855
26865
|
}
|
|
26856
26866
|
}
|
|
26857
26867
|
prevTimestamp = timestamp;
|
|
26858
|
-
prevName =
|
|
26859
|
-
|
|
26860
|
-
PRINT_PROGRESS_FN(checkd, totalFiles, symbol, interval);
|
|
26868
|
+
prevName = files[i];
|
|
26869
|
+
PRINT_PROGRESS_FN(i + 1, files.length, symbol, interval);
|
|
26861
26870
|
}
|
|
26862
|
-
console.log(`checkCandles: OK ${
|
|
26871
|
+
console.log(`checkCandles: OK ${files.length} candles ${symbol} ${interval}`);
|
|
26863
26872
|
}
|
|
26864
26873
|
/**
|
|
26865
26874
|
* Pre-caches candles for a date range into persist storage.
|
package/build/index.mjs
CHANGED
|
@@ -894,11 +894,13 @@ class PersistBase {
|
|
|
894
894
|
entityName: this.entityName,
|
|
895
895
|
});
|
|
896
896
|
try {
|
|
897
|
-
const
|
|
898
|
-
const
|
|
899
|
-
.
|
|
900
|
-
|
|
901
|
-
|
|
897
|
+
const entityIds = [];
|
|
898
|
+
for await (const entry of await fs__default.opendir(this._directory)) {
|
|
899
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
900
|
+
entityIds.push(entry.name.slice(0, -5));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
entityIds.sort((a, b) => a.localeCompare(b, undefined, {
|
|
902
904
|
numeric: true,
|
|
903
905
|
sensitivity: "base",
|
|
904
906
|
}));
|
|
@@ -26787,7 +26789,7 @@ const PRINT_PROGRESS_FN = (fetched, total, symbol, interval) => {
|
|
|
26787
26789
|
* @param params - Validation parameters
|
|
26788
26790
|
*/
|
|
26789
26791
|
async function checkCandles(params) {
|
|
26790
|
-
const { symbol, exchangeName, interval, baseDir = "./dump/data/candle" } = params;
|
|
26792
|
+
const { symbol, exchangeName, interval, from, to, baseDir = "./dump/data/candle" } = params;
|
|
26791
26793
|
bt.loggerService.info(CHECK_CANDLES_METHOD_NAME, params);
|
|
26792
26794
|
const step = INTERVAL_MINUTES$1[interval];
|
|
26793
26795
|
if (!step) {
|
|
@@ -26795,51 +26797,58 @@ async function checkCandles(params) {
|
|
|
26795
26797
|
}
|
|
26796
26798
|
const stepMs = step * MS_PER_MINUTE$1;
|
|
26797
26799
|
const dir = join(baseDir, exchangeName, symbol, interval);
|
|
26800
|
+
const fromTs = ALIGN_TO_INTERVAL_FN(from.getTime(), step);
|
|
26801
|
+
const toTs = ALIGN_TO_INTERVAL_FN(to.getTime(), step);
|
|
26798
26802
|
try {
|
|
26799
26803
|
await stat(dir);
|
|
26800
26804
|
}
|
|
26801
26805
|
catch {
|
|
26802
26806
|
throw new Error(`checkCandles: cache directory not found: ${dir}`);
|
|
26803
26807
|
}
|
|
26804
|
-
//
|
|
26805
|
-
|
|
26808
|
+
// Collect only filenames (strings) in range via async iterator — no full readdir in memory
|
|
26809
|
+
const files = [];
|
|
26806
26810
|
for await (const entry of await opendir(dir)) {
|
|
26807
|
-
if (entry.isFile()
|
|
26808
|
-
|
|
26811
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
26812
|
+
continue;
|
|
26813
|
+
}
|
|
26814
|
+
const ts = Number(entry.name.replace(".json", ""));
|
|
26815
|
+
if (ts >= fromTs && ts < toTs) {
|
|
26816
|
+
files.push(entry.name);
|
|
26809
26817
|
}
|
|
26810
26818
|
}
|
|
26811
|
-
if (
|
|
26812
|
-
throw new Error(`checkCandles: no cached candles in ${dir}`);
|
|
26819
|
+
if (files.length === 0) {
|
|
26820
|
+
throw new Error(`checkCandles: no cached candles in range [${fromTs}, ${toTs}) in ${dir}`);
|
|
26813
26821
|
}
|
|
26814
|
-
|
|
26815
|
-
let checkd = 0;
|
|
26822
|
+
files.sort();
|
|
26816
26823
|
let prevTimestamp = null;
|
|
26817
26824
|
let prevName = null;
|
|
26818
|
-
PRINT_PROGRESS_FN(
|
|
26819
|
-
for
|
|
26820
|
-
|
|
26821
|
-
continue;
|
|
26822
|
-
}
|
|
26823
|
-
const filePath = join(dir, entry.name);
|
|
26825
|
+
PRINT_PROGRESS_FN(0, files.length, symbol, interval);
|
|
26826
|
+
for (let i = 0; i < files.length; i++) {
|
|
26827
|
+
const filePath = join(dir, files[i]);
|
|
26824
26828
|
const raw = await readFile(filePath, "utf-8");
|
|
26825
|
-
|
|
26829
|
+
let candle;
|
|
26830
|
+
try {
|
|
26831
|
+
candle = JSON.parse(raw);
|
|
26832
|
+
}
|
|
26833
|
+
catch {
|
|
26834
|
+
throw new Error(`checkCandles: ${files[i]} contains invalid JSON`);
|
|
26835
|
+
}
|
|
26826
26836
|
const { timestamp } = candle;
|
|
26827
26837
|
const aligned = ALIGN_TO_INTERVAL_FN(timestamp, step);
|
|
26828
26838
|
if (timestamp !== aligned) {
|
|
26829
|
-
throw new Error(`checkCandles: ${
|
|
26839
|
+
throw new Error(`checkCandles: ${files[i]} timestamp not aligned to ${interval} boundary (actual=${timestamp}, expected=${aligned})`);
|
|
26830
26840
|
}
|
|
26831
26841
|
if (prevTimestamp !== null) {
|
|
26832
26842
|
const gap = timestamp - prevTimestamp;
|
|
26833
26843
|
if (gap !== stepMs) {
|
|
26834
|
-
throw new Error(`checkCandles: gap between ${prevName} and ${
|
|
26844
|
+
throw new Error(`checkCandles: gap between ${prevName} and ${files[i]} (actual=${gap}ms, expected=${stepMs}ms)`);
|
|
26835
26845
|
}
|
|
26836
26846
|
}
|
|
26837
26847
|
prevTimestamp = timestamp;
|
|
26838
|
-
prevName =
|
|
26839
|
-
|
|
26840
|
-
PRINT_PROGRESS_FN(checkd, totalFiles, symbol, interval);
|
|
26848
|
+
prevName = files[i];
|
|
26849
|
+
PRINT_PROGRESS_FN(i + 1, files.length, symbol, interval);
|
|
26841
26850
|
}
|
|
26842
|
-
console.log(`checkCandles: OK ${
|
|
26851
|
+
console.log(`checkCandles: OK ${files.length} candles ${symbol} ${interval}`);
|
|
26843
26852
|
}
|
|
26844
26853
|
/**
|
|
26845
26854
|
* Pre-caches candles for a date range into persist storage.
|
package/package.json
CHANGED
package/types.d.ts
CHANGED
|
@@ -337,6 +337,10 @@ interface ICheckCandlesParams {
|
|
|
337
337
|
exchangeName: ExchangeName;
|
|
338
338
|
/** Candle time interval (e.g., "1m", "4h") */
|
|
339
339
|
interval: CandleInterval;
|
|
340
|
+
/** Start date of the validation range (inclusive) */
|
|
341
|
+
from: Date;
|
|
342
|
+
/** End date of the validation range (inclusive) */
|
|
343
|
+
to: Date;
|
|
340
344
|
/** Base directory of candle persist storage (default: "./dump/data/candle") */
|
|
341
345
|
baseDir?: string;
|
|
342
346
|
}
|