@backtest-kit/cli 5.11.0 → 6.0.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/README.md +90 -0
- package/build/index.cjs +254 -18
- package/build/index.mjs +256 -20
- package/package.json +13 -13
- package/template/project/CLAUDE.md +158 -0
- package/template/project/README.md +207 -0
- package/template/project/content/feb_2026.strategy.ts +114 -0
- package/template/project/docs/backtest_actions.md +158 -0
- package/template/project/docs/backtest_graph_multiple_outputs.md +137 -0
- package/template/project/docs/backtest_graph_pattern.md +235 -0
- package/template/project/docs/backtest_logging_jsonl.md +246 -0
- package/template/project/docs/backtest_pinets_usage.md +190 -0
- package/template/project/docs/backtest_risk_async.md +153 -0
- package/template/project/docs/backtest_strategy_structure.md +250 -0
- package/template/project/docs/pine_debug.md +174 -0
- package/template/project/docs/pine_indicator_warmup.md +88 -0
- package/template/project/gitignore.mustache +30 -0
- package/template/project/math/feb_2026.pine +120 -0
- package/template/project/modules/dump.module.ts +37 -0
- package/template/project/modules/pine.module.ts +37 -0
- package/template/project/package.mustache +27 -0
- package/template/project/report/feb_2026.md +78 -0
- package/template/project/scripts/fetch_docs.mjs +58 -0
- /package/template/project/{.gitkeep → docs/lib/.gitkeep} +0 -0
package/build/index.mjs
CHANGED
|
@@ -4,8 +4,8 @@ import { Storage, Notification, Markdown, Report, Dump, Memory, StorageLive, Sto
|
|
|
4
4
|
import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, execpool, queued, sleep, randomString, createAwaiter, TIMEOUT_SYMBOL, typo, retry, trycatch, memoize } from 'functools-kit';
|
|
5
5
|
import fs, { constants } from 'fs';
|
|
6
6
|
import * as stackTrace from 'stack-trace';
|
|
7
|
-
import path, { basename, extname, join, resolve } from 'path';
|
|
8
|
-
import fs$1, { access, readFile, mkdir, writeFile } from 'fs/promises';
|
|
7
|
+
import path, { basename, extname, join, resolve, dirname } from 'path';
|
|
8
|
+
import fs$1, { access, readFile, mkdir, writeFile, readdir, copyFile } from 'fs/promises';
|
|
9
9
|
import dotenv from 'dotenv';
|
|
10
10
|
import { createActivator } from 'di-kit';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
@@ -30,6 +30,7 @@ import * as BacktestKitOllama from '@backtest-kit/ollama';
|
|
|
30
30
|
import * as BacktestKitPinets from '@backtest-kit/pinets';
|
|
31
31
|
import { run as run$1, Code, toMarkdown } from '@backtest-kit/pinets';
|
|
32
32
|
import * as BacktestKitSignals from '@backtest-kit/signals';
|
|
33
|
+
import { spawn } from 'child_process';
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Fix for `Attempted to assign to readonly property (at redactToken)`
|
|
@@ -259,15 +260,15 @@ const TYPES = {
|
|
|
259
260
|
|
|
260
261
|
const entrySubject = new BehaviorSubject();
|
|
261
262
|
|
|
262
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
263
|
-
const __dirname = path.dirname(__filename);
|
|
263
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
264
|
+
const __dirname$1 = path.dirname(__filename$1);
|
|
264
265
|
let _is_launched = false;
|
|
265
266
|
class ResolveService {
|
|
266
267
|
constructor() {
|
|
267
268
|
this.loggerService = inject(TYPES.loggerService);
|
|
268
269
|
this.loaderService = inject(TYPES.loaderService);
|
|
269
|
-
this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname, '..', 'template');
|
|
270
|
-
this.DEFAULT_MODULES_DIR = path.resolve(__dirname, '..', 'modules');
|
|
270
|
+
this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$1, '..', 'template');
|
|
271
|
+
this.DEFAULT_MODULES_DIR = path.resolve(__dirname$1, '..', 'modules');
|
|
271
272
|
this.OVERRIDE_TEMPLATE_DIR = path.resolve(process.cwd(), 'template');
|
|
272
273
|
this.OVERRIDE_MODULES_DIR = path.resolve(process.cwd(), 'modules');
|
|
273
274
|
this.getIsLaunched = () => {
|
|
@@ -429,6 +430,10 @@ const ALLOWED_EXTENSIONS = [
|
|
|
429
430
|
`.js`,
|
|
430
431
|
`.pine`,
|
|
431
432
|
];
|
|
433
|
+
const DISALLOWED_PATHS = [
|
|
434
|
+
"node_modules",
|
|
435
|
+
"@backtest-kit",
|
|
436
|
+
];
|
|
432
437
|
const getArgs = singleshot(() => {
|
|
433
438
|
const { values, positionals } = parseArgs({
|
|
434
439
|
args: process.argv,
|
|
@@ -523,6 +528,18 @@ const getArgs = singleshot(() => {
|
|
|
523
528
|
type: "boolean",
|
|
524
529
|
default: false,
|
|
525
530
|
},
|
|
531
|
+
init: {
|
|
532
|
+
type: "boolean",
|
|
533
|
+
default: false,
|
|
534
|
+
},
|
|
535
|
+
help: {
|
|
536
|
+
type: "boolean",
|
|
537
|
+
default: false,
|
|
538
|
+
},
|
|
539
|
+
version: {
|
|
540
|
+
type: "boolean",
|
|
541
|
+
default: false,
|
|
542
|
+
},
|
|
526
543
|
},
|
|
527
544
|
strict: false,
|
|
528
545
|
allowPositionals: true,
|
|
@@ -534,7 +551,9 @@ const getArgs = singleshot(() => {
|
|
|
534
551
|
});
|
|
535
552
|
const getPositional = singleshot(() => {
|
|
536
553
|
const { positionals = [] } = getArgs();
|
|
537
|
-
const result = positionals
|
|
554
|
+
const result = positionals
|
|
555
|
+
.filter((value) => !DISALLOWED_PATHS.some((path) => value.includes(path)))
|
|
556
|
+
.find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
|
|
538
557
|
return result || null;
|
|
539
558
|
});
|
|
540
559
|
|
|
@@ -2191,7 +2210,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
|
2191
2210
|
const listenGracefulShutdown$4 = singleshot(() => {
|
|
2192
2211
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
2193
2212
|
});
|
|
2194
|
-
const main$
|
|
2213
|
+
const main$9 = async () => {
|
|
2195
2214
|
if (!getEntry(import.meta.url)) {
|
|
2196
2215
|
return;
|
|
2197
2216
|
}
|
|
@@ -2202,7 +2221,7 @@ const main$6 = async () => {
|
|
|
2202
2221
|
await cli.backtestMainService.connect();
|
|
2203
2222
|
listenGracefulShutdown$4();
|
|
2204
2223
|
};
|
|
2205
|
-
main$
|
|
2224
|
+
main$9();
|
|
2206
2225
|
|
|
2207
2226
|
const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
2208
2227
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -2223,7 +2242,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
|
2223
2242
|
const listenGracefulShutdown$3 = singleshot(() => {
|
|
2224
2243
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
2225
2244
|
});
|
|
2226
|
-
const main$
|
|
2245
|
+
const main$8 = async () => {
|
|
2227
2246
|
if (!getEntry(import.meta.url)) {
|
|
2228
2247
|
return;
|
|
2229
2248
|
}
|
|
@@ -2234,7 +2253,7 @@ const main$5 = async () => {
|
|
|
2234
2253
|
cli.paperMainService.connect();
|
|
2235
2254
|
listenGracefulShutdown$3();
|
|
2236
2255
|
};
|
|
2237
|
-
main$
|
|
2256
|
+
main$8();
|
|
2238
2257
|
|
|
2239
2258
|
const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
2240
2259
|
process.off("SIGINT", BEFORE_EXIT_FN$2);
|
|
@@ -2255,7 +2274,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
|
2255
2274
|
const listenGracefulShutdown$2 = singleshot(() => {
|
|
2256
2275
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
2257
2276
|
});
|
|
2258
|
-
const main$
|
|
2277
|
+
const main$7 = async () => {
|
|
2259
2278
|
if (!getEntry(import.meta.url)) {
|
|
2260
2279
|
return;
|
|
2261
2280
|
}
|
|
@@ -2266,7 +2285,7 @@ const main$4 = async () => {
|
|
|
2266
2285
|
await cli.liveMainService.connect();
|
|
2267
2286
|
listenGracefulShutdown$2();
|
|
2268
2287
|
};
|
|
2269
|
-
main$
|
|
2288
|
+
main$7();
|
|
2270
2289
|
|
|
2271
2290
|
const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
2272
2291
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -2276,7 +2295,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
|
2276
2295
|
const listenGracefulShutdown$1 = singleshot(() => {
|
|
2277
2296
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
2278
2297
|
});
|
|
2279
|
-
const main$
|
|
2298
|
+
const main$6 = async () => {
|
|
2280
2299
|
if (!getEntry(import.meta.url)) {
|
|
2281
2300
|
return;
|
|
2282
2301
|
}
|
|
@@ -2286,7 +2305,7 @@ const main$3 = async () => {
|
|
|
2286
2305
|
}
|
|
2287
2306
|
listenGracefulShutdown$1();
|
|
2288
2307
|
};
|
|
2289
|
-
main$
|
|
2308
|
+
main$6();
|
|
2290
2309
|
|
|
2291
2310
|
const BEFORE_EXIT_FN = singleshot(async () => {
|
|
2292
2311
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -2296,7 +2315,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
|
|
|
2296
2315
|
const listenGracefulShutdown = singleshot(() => {
|
|
2297
2316
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
2298
2317
|
});
|
|
2299
|
-
const main$
|
|
2318
|
+
const main$5 = async () => {
|
|
2300
2319
|
if (!getEntry(import.meta.url)) {
|
|
2301
2320
|
return;
|
|
2302
2321
|
}
|
|
@@ -2306,7 +2325,7 @@ const main$2 = async () => {
|
|
|
2306
2325
|
}
|
|
2307
2326
|
listenGracefulShutdown();
|
|
2308
2327
|
};
|
|
2309
|
-
main$
|
|
2328
|
+
main$5();
|
|
2310
2329
|
|
|
2311
2330
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
2312
2331
|
const keys = Object.keys(schema);
|
|
@@ -2328,7 +2347,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
2328
2347
|
}
|
|
2329
2348
|
return rows;
|
|
2330
2349
|
};
|
|
2331
|
-
const main$
|
|
2350
|
+
const main$4 = async () => {
|
|
2332
2351
|
if (!getEntry(import.meta.url)) {
|
|
2333
2352
|
return;
|
|
2334
2353
|
}
|
|
@@ -2381,6 +2400,7 @@ const main$1 = async () => {
|
|
|
2381
2400
|
await mkdir(dumpDir, { recursive: true });
|
|
2382
2401
|
await writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
2383
2402
|
console.log(`Saved: ${filePath}`);
|
|
2403
|
+
process.exit(0);
|
|
2384
2404
|
return;
|
|
2385
2405
|
}
|
|
2386
2406
|
if (values.jsonl) {
|
|
@@ -2389,6 +2409,7 @@ const main$1 = async () => {
|
|
|
2389
2409
|
await mkdir(dumpDir, { recursive: true });
|
|
2390
2410
|
await writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
2391
2411
|
console.log(`Saved: ${filePath}`);
|
|
2412
|
+
process.exit(0);
|
|
2392
2413
|
return;
|
|
2393
2414
|
}
|
|
2394
2415
|
if (values.markdown) {
|
|
@@ -2396,13 +2417,15 @@ const main$1 = async () => {
|
|
|
2396
2417
|
await mkdir(dumpDir, { recursive: true });
|
|
2397
2418
|
await writeFile(filePath, await toMarkdown(signalId, plots, signalSchema), "utf-8");
|
|
2398
2419
|
console.log(`Saved: ${filePath}`);
|
|
2420
|
+
process.exit(0);
|
|
2399
2421
|
return;
|
|
2400
2422
|
}
|
|
2401
2423
|
console.log(await toMarkdown(signalId, plots, signalSchema));
|
|
2424
|
+
process.exit(0);
|
|
2402
2425
|
};
|
|
2403
|
-
main$
|
|
2426
|
+
main$4();
|
|
2404
2427
|
|
|
2405
|
-
const main = async () => {
|
|
2428
|
+
const main$3 = async () => {
|
|
2406
2429
|
if (!getEntry(import.meta.url)) {
|
|
2407
2430
|
return;
|
|
2408
2431
|
}
|
|
@@ -2436,6 +2459,7 @@ const main = async () => {
|
|
|
2436
2459
|
await mkdir(dumpDir, { recursive: true });
|
|
2437
2460
|
await writeFile(filePath, JSON.stringify(candles, null, 2), "utf-8");
|
|
2438
2461
|
console.log(`Saved: ${filePath}`);
|
|
2462
|
+
process.exit(0);
|
|
2439
2463
|
return;
|
|
2440
2464
|
}
|
|
2441
2465
|
if (values.jsonl) {
|
|
@@ -2443,9 +2467,221 @@ const main = async () => {
|
|
|
2443
2467
|
await mkdir(dumpDir, { recursive: true });
|
|
2444
2468
|
await writeFile(filePath, candles.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
2445
2469
|
console.log(`Saved: ${filePath}`);
|
|
2470
|
+
process.exit(0);
|
|
2446
2471
|
return;
|
|
2447
2472
|
}
|
|
2448
2473
|
console.log(JSON.stringify(candles, null, 2));
|
|
2474
|
+
process.exit(0);
|
|
2475
|
+
};
|
|
2476
|
+
main$3();
|
|
2477
|
+
|
|
2478
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2479
|
+
const __dirname = dirname(__filename);
|
|
2480
|
+
const MUSTACHE_EXT = ".mustache";
|
|
2481
|
+
const MUSTACHE_RENAME = {
|
|
2482
|
+
"gitignore": ".gitignore",
|
|
2483
|
+
"package": "package.json",
|
|
2484
|
+
};
|
|
2485
|
+
async function isDirEmpty(dirPath) {
|
|
2486
|
+
try {
|
|
2487
|
+
const files = await readdir(dirPath);
|
|
2488
|
+
return files.length === 0;
|
|
2489
|
+
}
|
|
2490
|
+
catch (error) {
|
|
2491
|
+
if (error.code === "ENOENT") {
|
|
2492
|
+
return true;
|
|
2493
|
+
}
|
|
2494
|
+
throw error;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
async function copyDir(srcDir, destDir, data) {
|
|
2498
|
+
await mkdir(destDir, { recursive: true });
|
|
2499
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
2500
|
+
for (const entry of entries) {
|
|
2501
|
+
const srcPath = join(srcDir, entry.name);
|
|
2502
|
+
if (entry.isDirectory()) {
|
|
2503
|
+
await copyDir(srcPath, join(destDir, entry.name), data);
|
|
2504
|
+
continue;
|
|
2505
|
+
}
|
|
2506
|
+
if (entry.name === ".gitkeep") {
|
|
2507
|
+
continue;
|
|
2508
|
+
}
|
|
2509
|
+
if (entry.name.endsWith(MUSTACHE_EXT)) {
|
|
2510
|
+
const baseName = entry.name.slice(0, -MUSTACHE_EXT.length);
|
|
2511
|
+
const destName = MUSTACHE_RENAME[baseName] ?? baseName;
|
|
2512
|
+
const destPath = join(destDir, destName);
|
|
2513
|
+
const template = await readFile(srcPath, "utf-8");
|
|
2514
|
+
const rendered = Mustache.render(template, data);
|
|
2515
|
+
await writeFile(destPath, rendered, "utf-8");
|
|
2516
|
+
console.log(` -> ${destPath}`);
|
|
2517
|
+
}
|
|
2518
|
+
else {
|
|
2519
|
+
const destPath = join(destDir, entry.name);
|
|
2520
|
+
await copyFile(srcPath, destPath);
|
|
2521
|
+
console.log(` -> ${destPath}`);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
function runScript(scriptPath, cwd) {
|
|
2526
|
+
return new Promise((resolve, reject) => {
|
|
2527
|
+
const node = process.execPath;
|
|
2528
|
+
const child = spawn(node, [scriptPath], { cwd, stdio: "inherit" });
|
|
2529
|
+
child.on("close", (code) => {
|
|
2530
|
+
if (code !== 0) {
|
|
2531
|
+
reject(new Error(`Script exited with code ${code}`));
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
resolve();
|
|
2535
|
+
});
|
|
2536
|
+
child.on("error", reject);
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
const main$2 = async () => {
|
|
2540
|
+
if (!getEntry(import.meta.url)) {
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const { values } = getArgs();
|
|
2544
|
+
if (!values.init) {
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
const projectName = values.output || "backtest-kit-project";
|
|
2548
|
+
const projectPath = join(process.cwd(), projectName);
|
|
2549
|
+
const templatePath = join(__dirname, "../template/project");
|
|
2550
|
+
const isEmpty = await isDirEmpty(projectPath);
|
|
2551
|
+
if (!isEmpty) {
|
|
2552
|
+
console.error(`Directory "${projectName}" already exists and is not empty.`);
|
|
2553
|
+
process.exit(1);
|
|
2554
|
+
}
|
|
2555
|
+
console.log(`Creating project in ${projectPath}`);
|
|
2556
|
+
await copyDir(templatePath, projectPath, { PROJECT_NAME: projectName });
|
|
2557
|
+
console.log(`Fetching docs...`);
|
|
2558
|
+
await runScript(join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
|
|
2559
|
+
console.log(`Done! Project created at ${projectPath}`);
|
|
2560
|
+
process.exit(0);
|
|
2561
|
+
};
|
|
2562
|
+
main$2();
|
|
2563
|
+
|
|
2564
|
+
const HELP_TEXT = `
|
|
2565
|
+
@backtest-kit/cli
|
|
2566
|
+
|
|
2567
|
+
Usage:
|
|
2568
|
+
node index.mjs --<mode> [flags] [entry-point]
|
|
2569
|
+
|
|
2570
|
+
Modes:
|
|
2571
|
+
|
|
2572
|
+
--backtest <entry> Run strategy against historical candle data
|
|
2573
|
+
--paper <entry> Paper trading (live prices, no real orders)
|
|
2574
|
+
--live <entry> Live trading with real orders
|
|
2575
|
+
--pine <entry> Execute a local .pine indicator file
|
|
2576
|
+
--dump Fetch and save raw OHLCV candles
|
|
2577
|
+
--init Scaffold a new project in the current directory
|
|
2578
|
+
--help Print this help message
|
|
2579
|
+
|
|
2580
|
+
Backtest flags:
|
|
2581
|
+
|
|
2582
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2583
|
+
--strategy <string> Strategy name from addStrategySchema (default: first registered)
|
|
2584
|
+
--exchange <string> Exchange name from addExchangeSchema (default: first registered)
|
|
2585
|
+
--frame <string> Frame name from addFrameSchema (default: first registered)
|
|
2586
|
+
--cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
|
|
2587
|
+
--noCache Skip candle cache warming before the run
|
|
2588
|
+
--verbose Log every candle fetch to stdout
|
|
2589
|
+
--ui Start web dashboard at http://localhost:60050
|
|
2590
|
+
--telegram Send trade notifications to Telegram
|
|
2591
|
+
|
|
2592
|
+
Paper / Live flags:
|
|
2593
|
+
|
|
2594
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2595
|
+
--strategy <string> Strategy name (default: first registered)
|
|
2596
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2597
|
+
--verbose Log every candle fetch to stdout
|
|
2598
|
+
--ui Start web dashboard
|
|
2599
|
+
--telegram Send Telegram notifications
|
|
2600
|
+
|
|
2601
|
+
PineScript flags (--pine):
|
|
2602
|
+
|
|
2603
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2604
|
+
--timeframe <string> Candle interval (default: 15m)
|
|
2605
|
+
--limit <string> Number of candles to fetch (default: 250)
|
|
2606
|
+
--when <string> End date — ISO 8601 or Unix ms (default: now)
|
|
2607
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2608
|
+
--output <string> Output file base name without extension
|
|
2609
|
+
--json Save output as JSON array to <pine-dir>/dump/<output>.json
|
|
2610
|
+
--jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
|
|
2611
|
+
--markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
|
|
2612
|
+
|
|
2613
|
+
Only plot() calls with display=display.data_window produce output columns.
|
|
2614
|
+
Module file ./modules/pine.module is loaded automatically if it exists.
|
|
2615
|
+
|
|
2616
|
+
Candle dump flags (--dump):
|
|
2617
|
+
|
|
2618
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2619
|
+
--timeframe <string> Candle interval (default: 15m)
|
|
2620
|
+
--limit <string> Number of candles (default: 250)
|
|
2621
|
+
--when <string> End date — ISO 8601 or Unix ms (default: now)
|
|
2622
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2623
|
+
--output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
|
|
2624
|
+
--json Save as JSON array to ./dump/<output>.json
|
|
2625
|
+
--jsonl Save as JSONL to ./dump/<output>.jsonl
|
|
2626
|
+
|
|
2627
|
+
Module file ./modules/dump.module is loaded automatically if it exists.
|
|
2628
|
+
|
|
2629
|
+
Init flags (--init):
|
|
2630
|
+
|
|
2631
|
+
--output <string> Target directory name (default: backtest-kit-project)
|
|
2632
|
+
|
|
2633
|
+
Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
|
|
2634
|
+
|
|
2635
|
+
Module hooks (loaded automatically by each mode):
|
|
2636
|
+
|
|
2637
|
+
modules/backtest.module --backtest Broker adapter for backtest
|
|
2638
|
+
modules/paper.module --paper Broker adapter for paper trading
|
|
2639
|
+
modules/live.module --live Broker adapter for live trading
|
|
2640
|
+
modules/pine.module --pine Exchange schema for PineScript runs
|
|
2641
|
+
modules/dump.module --dump Exchange schema for candle dumps
|
|
2642
|
+
|
|
2643
|
+
Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
|
|
2644
|
+
|
|
2645
|
+
Environment variables:
|
|
2646
|
+
|
|
2647
|
+
CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
|
|
2648
|
+
CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
|
|
2649
|
+
CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
|
|
2650
|
+
CC_WWWROOT_PORT UI server port (default: 60050)
|
|
2651
|
+
|
|
2652
|
+
Examples:
|
|
2653
|
+
|
|
2654
|
+
node index.mjs --backtest ./content/feb_2026.strategy.ts
|
|
2655
|
+
node index.mjs --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
|
|
2656
|
+
node index.mjs --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
|
|
2657
|
+
node index.mjs --live --ui --telegram ./content/feb_2026.strategy.ts
|
|
2658
|
+
node index.mjs --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
|
|
2659
|
+
node index.mjs --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
|
|
2660
|
+
node index.mjs --init --output my-trading-bot
|
|
2661
|
+
`.trimStart();
|
|
2662
|
+
const main$1 = async () => {
|
|
2663
|
+
if (!getEntry(import.meta.url)) {
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
const { values } = getArgs();
|
|
2667
|
+
if (!values.help) {
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
process.stdout.write(HELP_TEXT);
|
|
2671
|
+
process.exit(0);
|
|
2672
|
+
};
|
|
2673
|
+
main$1();
|
|
2674
|
+
|
|
2675
|
+
const main = async () => {
|
|
2676
|
+
if (!getEntry(import.meta.url)) {
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
const { values } = getArgs();
|
|
2680
|
+
if (!values.version) {
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
process.stdout.write(`@backtest-kit/cli ${"6.0.0"}\n`);
|
|
2684
|
+
process.exit(0);
|
|
2449
2685
|
};
|
|
2450
2686
|
main();
|
|
2451
2687
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backtest-kit/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Zero-boilerplate CLI runner for backtest-kit strategies. Run backtests, paper trading, and live bots with candle cache warming, web dashboard, and Telegram notifications — no setup code required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Petr Tripolsky",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@babel/plugin-transform-modules-umd": "7.27.1",
|
|
63
63
|
"@babel/standalone": "7.29.1",
|
|
64
|
-
"@backtest-kit/ui": "
|
|
65
|
-
"@backtest-kit/graph": "
|
|
66
|
-
"@backtest-kit/ollama": "
|
|
67
|
-
"@backtest-kit/pinets": "
|
|
68
|
-
"@backtest-kit/signals": "
|
|
64
|
+
"@backtest-kit/ui": "6.0.0",
|
|
65
|
+
"@backtest-kit/graph": "6.0.0",
|
|
66
|
+
"@backtest-kit/ollama": "6.0.0",
|
|
67
|
+
"@backtest-kit/pinets": "6.0.0",
|
|
68
|
+
"@backtest-kit/signals": "6.0.0",
|
|
69
69
|
"@rollup/plugin-replace": "6.0.3",
|
|
70
70
|
"@rollup/plugin-typescript": "11.1.6",
|
|
71
71
|
"@types/image-size": "0.7.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@types/mustache": "4.2.6",
|
|
74
74
|
"@types/node": "22.9.0",
|
|
75
75
|
"@types/stack-trace": "0.0.33",
|
|
76
|
-
"backtest-kit": "
|
|
76
|
+
"backtest-kit": "6.0.0",
|
|
77
77
|
"glob": "11.0.1",
|
|
78
78
|
"markdown-it": "14.1.1",
|
|
79
79
|
"rimraf": "6.0.1",
|
|
@@ -88,12 +88,12 @@
|
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"@babel/plugin-transform-modules-umd": "^7.27.1",
|
|
90
90
|
"@babel/standalone": "^7.29.1",
|
|
91
|
-
"@backtest-kit/ui": "^
|
|
92
|
-
"@backtest-kit/graph": "^
|
|
93
|
-
"@backtest-kit/ollama": "^
|
|
94
|
-
"@backtest-kit/pinets": "^
|
|
95
|
-
"@backtest-kit/signals": "^
|
|
96
|
-
"backtest-kit": "^
|
|
91
|
+
"@backtest-kit/ui": "^6.0.0",
|
|
92
|
+
"@backtest-kit/graph": "^6.0.0",
|
|
93
|
+
"@backtest-kit/ollama": "^6.0.0",
|
|
94
|
+
"@backtest-kit/pinets": "^6.0.0",
|
|
95
|
+
"@backtest-kit/signals": "^6.0.0",
|
|
96
|
+
"backtest-kit": "^6.0.0",
|
|
97
97
|
"markdown-it": "^14.1.1",
|
|
98
98
|
"typescript": "^5.0.0"
|
|
99
99
|
},
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
## Guide
|
|
2
|
+
|
|
3
|
+
### How to Write a Strategy
|
|
4
|
+
|
|
5
|
+
**What NOT to do**
|
|
6
|
+
|
|
7
|
+
- Don't read all project files and bloat the context.
|
|
8
|
+
|
|
9
|
+
Strategies are written as simple `.pine` files; the command to run them is below.
|
|
10
|
+
|
|
11
|
+
- Don't brute-force iterate.
|
|
12
|
+
|
|
13
|
+
The worst thing you can do is start incrementally writing into an existing project file. That's not how this works — you need market analysis, not work for the sake of work.
|
|
14
|
+
|
|
15
|
+
- Don't sacrifice efficiency for universality.
|
|
16
|
+
|
|
17
|
+
Markets change. By building a universal solution you lose the optimization that is the competitive edge actually generating profit at any given moment.
|
|
18
|
+
|
|
19
|
+
- Don't write `.pine` files with side effects.
|
|
20
|
+
|
|
21
|
+
You don't need `var` and `na` in PineScript — compute all values on every iteration. This makes errors and unpredictable behavior more likely to surface before going to production. Keep the code easy to understand; avoid premature optimization.
|
|
22
|
+
|
|
23
|
+
- Don't use hacks in trading strategy code.
|
|
24
|
+
|
|
25
|
+
You cannot disguise the absence of an SL by using ATR when the exit keeps shifting relative to the close price on every iteration. Trailing criteria must be finite — you cannot keep shifting the stop loss forever hoping for a bounce or a drop. Avoid HOLD in any form.
|
|
26
|
+
|
|
27
|
+
- Don't build strategies that produce one signal every few days.
|
|
28
|
+
|
|
29
|
+
Three profitable signals is not a successful trading strategy — it's luck. To evaluate a strategy statistically you need at least one signal per day.
|
|
30
|
+
|
|
31
|
+
**What TO do**
|
|
32
|
+
|
|
33
|
+
- Every strategy is written for a single calendar month.
|
|
34
|
+
|
|
35
|
+
Follow the naming pattern or refuse to work. The money is in optimizing for current market conditions; a backtest spanning two or more months is mathematically meaningless because the final balance will wipe out profit through commission whipsaw.
|
|
36
|
+
|
|
37
|
+
* `./math/jan_2026.pine`, `./content/jan_2026.strategy.ts`
|
|
38
|
+
* `./math/feb_2026.pine`, `./content/feb_2026.strategy.ts`
|
|
39
|
+
* `./math/march_2026.pine`, `./content/march_2026.strategy.ts`
|
|
40
|
+
* `./math/apr_2026.pine`, `./content/apr_2026.strategy.ts`
|
|
41
|
+
* `./math/may_2026.pine`, `./content/may_2026.strategy.ts`
|
|
42
|
+
|
|
43
|
+
- Read the news background for the chosen time period.
|
|
44
|
+
|
|
45
|
+
The focus should ALWAYS be on negative news. Searching for the Bitcoin price gives you marketing trash. Searching for analytics gives you SEO garbage. Use queries like:
|
|
46
|
+
|
|
47
|
+
* Bitcoin negative news March 2026 price drop regulatory problems…
|
|
48
|
+
* bitcoin price February 5 2024 current level forecast analytics BTC
|
|
49
|
+
* bitcoin negative news February 2024 problems regulator crackdown bitcoin
|
|
50
|
+
* bitcoin negative news March 2026 regulatory problems bans
|
|
51
|
+
* bitcoin security hackers fraud regulation negative news problems
|
|
52
|
+
|
|
53
|
+
- Create a `--dump` to output candles.
|
|
54
|
+
|
|
55
|
+
You need to see where the money actually is in the market. Identify the general trend: if it's bearish, protect against LONGs; if it's bullish, protect against SHORTs. There may be a short-term bounce or panic driven by geopolitical news.
|
|
56
|
+
|
|
57
|
+
- The market may be ranging (sideways).
|
|
58
|
+
|
|
59
|
+
There are cases when no position should be opened at all — your analysis must account for this.
|
|
60
|
+
|
|
61
|
+
- TP/SL should be dynamic, but not scalping.
|
|
62
|
+
|
|
63
|
+
The exchange charges 0.2% to enter and 0.2% to exit. You may think the strategy is profitable, but it's whipsaw. Minimum TP: 1%.
|
|
64
|
+
|
|
65
|
+
- Don't try to build an all-weather strategy.
|
|
66
|
+
|
|
67
|
+
I need to understand where the money is in the market only within the specified time period. If the strategy stops being profitable I'll simply ask you to run the analysis again.
|
|
68
|
+
|
|
69
|
+
- Don't build HOLD strategies.
|
|
70
|
+
|
|
71
|
+
I need to find where the money actually is in the market, not sit in a position hoping for luck. The criterion for "where the money is" must be expressed as a formula that finds effective entry points that lead to profit directly.
|
|
72
|
+
|
|
73
|
+
- Don't brut force strategies.
|
|
74
|
+
|
|
75
|
+
Use fresh strategies with different concepts. Do not edit existing strategy one cause this will give you a loop even if you coded it. I need concept engineering
|
|
76
|
+
|
|
77
|
+
### Market Candle Dump
|
|
78
|
+
|
|
79
|
+
File `BTCUSDT_500_15m_1772236800000.jsonl` will be created at `./dump/BTCUSDT_500_15m_1772236800000.jsonl`
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
npm start -- --dump --timeframe 15m --limit 500 --when "2026-02-28T00:00:00.000Z" --jsonl
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Running `.pine` Files
|
|
86
|
+
|
|
87
|
+
File `impulse_trend_15m.jsonl` will be created at `./math/dump/impulse_trend_15m.jsonl`
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
npm start -- --pine ./math/impulse_trend_15m.pine --timeframe 15m --limit 500 --when "2026-02-28T00:00:00.000Z" --jsonl
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Algorithm
|
|
94
|
+
|
|
95
|
+
**Planning the Work**
|
|
96
|
+
|
|
97
|
+
1. Read the `.pine` file from the previous month if one exists.
|
|
98
|
+
|
|
99
|
+
2. Read news from the internet for the current month with a focus on negative news.
|
|
100
|
+
|
|
101
|
+
3. Correlate the news background with the candle dump. News sources must visibly influence the candle data for the chosen time period: price bounce, sideways range, neutral trend, decline, or rally.
|
|
102
|
+
|
|
103
|
+
4. Understand why the previous month's file stopped working by interpreting its logic in the context of the new news background.
|
|
104
|
+
|
|
105
|
+
5. In addition to news, review the candle dump independently: assess volatility, market gaps, trading volumes, and risks.
|
|
106
|
+
|
|
107
|
+
**Writing the Strategy**
|
|
108
|
+
|
|
109
|
+
1. Create NEW files for the current month and write them from scratch. Do not copy-paste and do not attempt to brute-force parameters. New month — new strategy.
|
|
110
|
+
|
|
111
|
+
2. Run the `.pine` file and review the output. The acceptance criterion is a profitable trading strategy, not code for the sake of code. Do not stop until profit is achieved.
|
|
112
|
+
|
|
113
|
+
3. After obtaining a profitable strategy, ALWAYS save the knowledge base used to build it into a markdown file with fundamental market analysis, following the naming pattern:
|
|
114
|
+
|
|
115
|
+
* `./report/jan_2026.md`
|
|
116
|
+
* `./report/feb_2026.md`
|
|
117
|
+
* `./report/march_2026.md`
|
|
118
|
+
|
|
119
|
+
4. Run a code review as a separate agent.
|
|
120
|
+
|
|
121
|
+
The code review must check the strategy for perpetual hold without strict exit conditions — for example, a trailing SL that shifts forever relative to the close of the last candle. I need not just to make money, but to mathematically identify where the money is in the market in order to avoid large portfolio liquidity drawdowns.
|
|
122
|
+
|
|
123
|
+
5. If the code review fails, incorporate the findings and rebuild the strategy from scratch following this guide.
|
|
124
|
+
|
|
125
|
+
### Recommendations
|
|
126
|
+
|
|
127
|
+
- Search the internet for ideas.
|
|
128
|
+
|
|
129
|
+
I welcome borrowing trading ideas from other people via internet search rather than brute-forcing options. It's important not only to find someone else's concept but also to verify in practice that it is actually profitable.
|
|
130
|
+
|
|
131
|
+
- Analyze market structure.
|
|
132
|
+
|
|
133
|
+
Looking at the candles reveals the structures present in the current month: sideways range, neutral trend, bullish trend, bearish trend, high volatility. Think through how to identify and how to act in each case.
|
|
134
|
+
|
|
135
|
+
- Use a TODO list.
|
|
136
|
+
|
|
137
|
+
This guide has many steps. Form a TODO list and work through it step by step, marking each item complete. Include news research, candle `--dump` analysis, pine strategy analysis — as granularly as possible.
|
|
138
|
+
|
|
139
|
+
- Use deep research.
|
|
140
|
+
|
|
141
|
+
The guide calls for intelligent market analysis, not code for the sake of code. Think carefully; don't cut corners on tokens.
|
|
142
|
+
|
|
143
|
+
- Think logically.
|
|
144
|
+
|
|
145
|
+
A 1% target cannot be reached in less than 4 hours. A target below 1% is uninteresting because slippage will knock the position out before achieving an effective Risk/Reward. I don't need a formal reply — I need market analysis.
|
|
146
|
+
|
|
147
|
+
### Deliverable
|
|
148
|
+
|
|
149
|
+
A `.pine` file free of marketing fluff:
|
|
150
|
+
|
|
151
|
+
- Forbidden: TP=0.5% SL=-10% and any similar asymmetric nonsense. Risk management must be sound and must rule out holding on luck.
|
|
152
|
+
- Clearly described and commented operating modes with references to the time period on which they were tested.
|
|
153
|
+
- An honest profitability summary in the file header as a comment.
|
|
154
|
+
- An honest average daily signal count in the file header.
|
|
155
|
+
- An honest `sharpeRatio`, `avgPnl`, `stdDev` in the file header.
|
|
156
|
+
- One or more signals per day — more is better.
|
|
157
|
+
|
|
158
|
+
If it is impossible to make money, do not try to fudge the results. Write it as it is, without embellishment.
|