@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/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.find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
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$6 = async () => {
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$6();
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$5 = async () => {
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$5();
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$4 = async () => {
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$4();
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$3 = async () => {
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$3();
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$2 = async () => {
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$2();
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$1 = async () => {
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$1();
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": "5.11.0",
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": "5.11.0",
65
- "@backtest-kit/graph": "5.11.0",
66
- "@backtest-kit/ollama": "5.11.0",
67
- "@backtest-kit/pinets": "5.11.0",
68
- "@backtest-kit/signals": "5.11.0",
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": "5.11.0",
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": "^5.11.0",
92
- "@backtest-kit/graph": "^5.11.0",
93
- "@backtest-kit/ollama": "^5.11.0",
94
- "@backtest-kit/pinets": "^5.11.0",
95
- "@backtest-kit/signals": "^5.11.0",
96
- "backtest-kit": "^5.11.0",
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.