@backtest-kit/cli 5.10.2 → 5.11.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.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)`
@@ -93,6 +94,7 @@ setConfig({
93
94
  setConfig({
94
95
  CC_ENABLE_DCA_EVERYWHERE: true,
95
96
  CC_ENABLE_PPPL_EVERYWHERE: true,
97
+ CC_ENABLE_TRAILING_EVERYWHERE: true,
96
98
  });
97
99
  setConfig({
98
100
  CC_MAX_SIGNAL_GENERATION_SECONDS: 15 * 60,
@@ -258,15 +260,15 @@ const TYPES = {
258
260
 
259
261
  const entrySubject = new BehaviorSubject();
260
262
 
261
- const __filename = fileURLToPath(import.meta.url);
262
- const __dirname = path.dirname(__filename);
263
+ const __filename$1 = fileURLToPath(import.meta.url);
264
+ const __dirname$1 = path.dirname(__filename$1);
263
265
  let _is_launched = false;
264
266
  class ResolveService {
265
267
  constructor() {
266
268
  this.loggerService = inject(TYPES.loggerService);
267
269
  this.loaderService = inject(TYPES.loaderService);
268
- this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname, '..', 'template');
269
- 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');
270
272
  this.OVERRIDE_TEMPLATE_DIR = path.resolve(process.cwd(), 'template');
271
273
  this.OVERRIDE_MODULES_DIR = path.resolve(process.cwd(), 'modules');
272
274
  this.getIsLaunched = () => {
@@ -420,6 +422,18 @@ var FrameName;
420
422
  })(FrameName || (FrameName = {}));
421
423
  var FrameName$1 = FrameName;
422
424
 
425
+ const ALLOWED_EXTENSIONS = [
426
+ `.cjs`,
427
+ `.mjs`,
428
+ `.ts`,
429
+ `.tsx`,
430
+ `.js`,
431
+ `.pine`,
432
+ ];
433
+ const DISALLOWED_PATHS = [
434
+ "node_modules",
435
+ "@backtest-kit",
436
+ ];
423
437
  const getArgs = singleshot(() => {
424
438
  const { values, positionals } = parseArgs({
425
439
  args: process.argv,
@@ -514,6 +528,10 @@ const getArgs = singleshot(() => {
514
528
  type: "boolean",
515
529
  default: false,
516
530
  },
531
+ init: {
532
+ type: "boolean",
533
+ default: false,
534
+ },
517
535
  },
518
536
  strict: false,
519
537
  allowPositionals: true,
@@ -523,6 +541,13 @@ const getArgs = singleshot(() => {
523
541
  positionals,
524
542
  };
525
543
  });
544
+ const getPositional = singleshot(() => {
545
+ const { positionals = [] } = getArgs();
546
+ const result = positionals
547
+ .filter((value) => !DISALLOWED_PATHS.some((path) => value.includes(path)))
548
+ .find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
549
+ return result || null;
550
+ });
526
551
 
527
552
  const ADD_FRAME_FN = (self) => {
528
553
  self.loggerService.log("Adding February 2024 as a default frame schema");
@@ -708,11 +733,11 @@ class BacktestMainService {
708
733
  if (!getEntry(import.meta.url)) {
709
734
  return;
710
735
  }
711
- const { values, positionals } = getArgs();
736
+ const { values } = getArgs();
712
737
  if (!values.backtest) {
713
738
  return;
714
739
  }
715
- const [entryPoint = null] = positionals.slice(-1);
740
+ const entryPoint = getPositional();
716
741
  if (!entryPoint) {
717
742
  throw new Error("Entry point is required");
718
743
  }
@@ -787,11 +812,11 @@ class LiveMainService {
787
812
  if (!getEntry(import.meta.url)) {
788
813
  return;
789
814
  }
790
- const { values, positionals } = getArgs();
815
+ const { values } = getArgs();
791
816
  if (!values.live) {
792
817
  return;
793
818
  }
794
- const [entryPoint = null] = positionals.slice(-1);
819
+ const entryPoint = getPositional();
795
820
  if (!entryPoint) {
796
821
  throw new Error("Entry point is required");
797
822
  }
@@ -860,11 +885,11 @@ class PaperMainService {
860
885
  if (!getEntry(import.meta.url)) {
861
886
  return;
862
887
  }
863
- const { values, positionals } = getArgs();
888
+ const { values } = getArgs();
864
889
  if (!values.paper) {
865
890
  return;
866
891
  }
867
- const [entryPoint = null] = positionals.slice(-1);
892
+ const entryPoint = getPositional();
868
893
  if (!entryPoint) {
869
894
  throw new Error("Entry point is required");
870
895
  }
@@ -2177,7 +2202,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
2177
2202
  const listenGracefulShutdown$4 = singleshot(() => {
2178
2203
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2179
2204
  });
2180
- const main$6 = async () => {
2205
+ const main$7 = async () => {
2181
2206
  if (!getEntry(import.meta.url)) {
2182
2207
  return;
2183
2208
  }
@@ -2188,7 +2213,7 @@ const main$6 = async () => {
2188
2213
  await cli.backtestMainService.connect();
2189
2214
  listenGracefulShutdown$4();
2190
2215
  };
2191
- main$6();
2216
+ main$7();
2192
2217
 
2193
2218
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
2194
2219
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -2209,7 +2234,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
2209
2234
  const listenGracefulShutdown$3 = singleshot(() => {
2210
2235
  process.on("SIGINT", BEFORE_EXIT_FN$3);
2211
2236
  });
2212
- const main$5 = async () => {
2237
+ const main$6 = async () => {
2213
2238
  if (!getEntry(import.meta.url)) {
2214
2239
  return;
2215
2240
  }
@@ -2220,7 +2245,7 @@ const main$5 = async () => {
2220
2245
  cli.paperMainService.connect();
2221
2246
  listenGracefulShutdown$3();
2222
2247
  };
2223
- main$5();
2248
+ main$6();
2224
2249
 
2225
2250
  const BEFORE_EXIT_FN$2 = singleshot(async () => {
2226
2251
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -2241,7 +2266,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
2241
2266
  const listenGracefulShutdown$2 = singleshot(() => {
2242
2267
  process.on("SIGINT", BEFORE_EXIT_FN$2);
2243
2268
  });
2244
- const main$4 = async () => {
2269
+ const main$5 = async () => {
2245
2270
  if (!getEntry(import.meta.url)) {
2246
2271
  return;
2247
2272
  }
@@ -2252,7 +2277,7 @@ const main$4 = async () => {
2252
2277
  await cli.liveMainService.connect();
2253
2278
  listenGracefulShutdown$2();
2254
2279
  };
2255
- main$4();
2280
+ main$5();
2256
2281
 
2257
2282
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
2258
2283
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -2262,7 +2287,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
2262
2287
  const listenGracefulShutdown$1 = singleshot(() => {
2263
2288
  process.on("SIGINT", BEFORE_EXIT_FN$1);
2264
2289
  });
2265
- const main$3 = async () => {
2290
+ const main$4 = async () => {
2266
2291
  if (!getEntry(import.meta.url)) {
2267
2292
  return;
2268
2293
  }
@@ -2272,7 +2297,7 @@ const main$3 = async () => {
2272
2297
  }
2273
2298
  listenGracefulShutdown$1();
2274
2299
  };
2275
- main$3();
2300
+ main$4();
2276
2301
 
2277
2302
  const BEFORE_EXIT_FN = singleshot(async () => {
2278
2303
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -2282,7 +2307,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
2282
2307
  const listenGracefulShutdown = singleshot(() => {
2283
2308
  process.on("SIGINT", BEFORE_EXIT_FN);
2284
2309
  });
2285
- const main$2 = async () => {
2310
+ const main$3 = async () => {
2286
2311
  if (!getEntry(import.meta.url)) {
2287
2312
  return;
2288
2313
  }
@@ -2292,7 +2317,7 @@ const main$2 = async () => {
2292
2317
  }
2293
2318
  listenGracefulShutdown();
2294
2319
  };
2295
- main$2();
2320
+ main$3();
2296
2321
 
2297
2322
  const EXTRACT_ROWS_FN = (plots, schema) => {
2298
2323
  const keys = Object.keys(schema);
@@ -2314,15 +2339,15 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
2314
2339
  }
2315
2340
  return rows;
2316
2341
  };
2317
- const main$1 = async () => {
2342
+ const main$2 = async () => {
2318
2343
  if (!getEntry(import.meta.url)) {
2319
2344
  return;
2320
2345
  }
2321
- const { values, positionals } = getArgs();
2346
+ const { values } = getArgs();
2322
2347
  if (!values.pine) {
2323
2348
  return;
2324
2349
  }
2325
- const [entryPoint = null] = positionals.slice(-1);
2350
+ const entryPoint = getPositional();
2326
2351
  if (!entryPoint) {
2327
2352
  return;
2328
2353
  }
@@ -2386,9 +2411,9 @@ const main$1 = async () => {
2386
2411
  }
2387
2412
  console.log(await toMarkdown(signalId, plots, signalSchema));
2388
2413
  };
2389
- main$1();
2414
+ main$2();
2390
2415
 
2391
- const main = async () => {
2416
+ const main$1 = async () => {
2392
2417
  if (!getEntry(import.meta.url)) {
2393
2418
  return;
2394
2419
  }
@@ -2433,6 +2458,83 @@ const main = async () => {
2433
2458
  }
2434
2459
  console.log(JSON.stringify(candles, null, 2));
2435
2460
  };
2461
+ main$1();
2462
+
2463
+ const __filename = fileURLToPath(import.meta.url);
2464
+ const __dirname = dirname(__filename);
2465
+ const MUSTACHE_EXT = ".mustache";
2466
+ async function isDirEmpty(dirPath) {
2467
+ try {
2468
+ const files = await readdir(dirPath);
2469
+ return files.length === 0;
2470
+ }
2471
+ catch (error) {
2472
+ if (error.code === "ENOENT") {
2473
+ return true;
2474
+ }
2475
+ throw error;
2476
+ }
2477
+ }
2478
+ async function copyDir(srcDir, destDir, data) {
2479
+ await mkdir(destDir, { recursive: true });
2480
+ const entries = await readdir(srcDir, { withFileTypes: true });
2481
+ for (const entry of entries) {
2482
+ const srcPath = join(srcDir, entry.name);
2483
+ if (entry.isDirectory()) {
2484
+ await copyDir(srcPath, join(destDir, entry.name), data);
2485
+ continue;
2486
+ }
2487
+ if (entry.name.endsWith(MUSTACHE_EXT)) {
2488
+ const destName = entry.name.slice(0, -MUSTACHE_EXT.length);
2489
+ const destPath = join(destDir, destName);
2490
+ const template = await readFile(srcPath, "utf-8");
2491
+ const rendered = Mustache.render(template, data);
2492
+ await writeFile(destPath, rendered, "utf-8");
2493
+ console.log(` -> ${destPath}`);
2494
+ }
2495
+ else {
2496
+ const destPath = join(destDir, entry.name);
2497
+ await copyFile(srcPath, destPath);
2498
+ console.log(` -> ${destPath}`);
2499
+ }
2500
+ }
2501
+ }
2502
+ function runScript(scriptPath, cwd) {
2503
+ return new Promise((resolve, reject) => {
2504
+ const node = process.execPath;
2505
+ const child = spawn(node, [scriptPath], { cwd, stdio: "inherit" });
2506
+ child.on("close", (code) => {
2507
+ if (code !== 0) {
2508
+ reject(new Error(`Script exited with code ${code}`));
2509
+ return;
2510
+ }
2511
+ resolve();
2512
+ });
2513
+ child.on("error", reject);
2514
+ });
2515
+ }
2516
+ const main = async () => {
2517
+ if (!getEntry(import.meta.url)) {
2518
+ return;
2519
+ }
2520
+ const { values } = getArgs();
2521
+ if (!values.init) {
2522
+ return;
2523
+ }
2524
+ const projectName = values.output || "backtest-kit-project";
2525
+ const projectPath = join(process.cwd(), projectName);
2526
+ const templatePath = join(__dirname, "../../template/project");
2527
+ const isEmpty = await isDirEmpty(projectPath);
2528
+ if (!isEmpty) {
2529
+ console.error(`Directory "${projectName}" already exists and is not empty.`);
2530
+ process.exit(1);
2531
+ }
2532
+ console.log(`Creating project in ${projectPath}`);
2533
+ await copyDir(templatePath, projectPath, { PROJECT_NAME: projectName });
2534
+ console.log(`Fetching docs...`);
2535
+ await runScript(join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
2536
+ console.log(`Done! Project created at ${projectPath}`);
2537
+ };
2436
2538
  main();
2437
2539
 
2438
2540
  function setLogger(logger) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/cli",
3
- "version": "5.10.2",
3
+ "version": "5.11.1",
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.10.0",
65
- "@backtest-kit/graph": "5.10.0",
66
- "@backtest-kit/ollama": "5.10.0",
67
- "@backtest-kit/pinets": "5.10.0",
68
- "@backtest-kit/signals": "5.10.0",
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",
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.10.0",
76
+ "backtest-kit": "5.11.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.10.0",
92
- "@backtest-kit/graph": "^5.10.0",
93
- "@backtest-kit/ollama": "^5.10.0",
94
- "@backtest-kit/pinets": "^5.10.0",
95
- "@backtest-kit/signals": "^5.10.0",
96
- "backtest-kit": "^5.10.0",
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",
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.
@@ -0,0 +1,114 @@
1
+ import {
2
+ addExchangeSchema,
3
+ addFrameSchema,
4
+ addStrategySchema,
5
+ listenError,
6
+ Cache,
7
+ Log,
8
+ } from "backtest-kit";
9
+ import {
10
+ errorData,
11
+ getErrorMessage,
12
+ randomString,
13
+ singleshot,
14
+ } from "functools-kit";
15
+ import ccxt from "ccxt";
16
+ import { run, File, extract } from "@backtest-kit/pinets";
17
+ import { outputNode, resolve, sourceNode } from "@backtest-kit/graph";
18
+
19
+ const getExchange = singleshot(async () => {
20
+ const exchange = new ccxt.binance({
21
+ options: {
22
+ defaultType: "spot",
23
+ adjustForTimeDifference: true,
24
+ recvWindow: 60000,
25
+ },
26
+ enableRateLimit: true,
27
+ });
28
+ await exchange.loadMarkets();
29
+ return exchange;
30
+ });
31
+
32
+ const pineSource = sourceNode(
33
+ Cache.fn(
34
+ async (symbol) => {
35
+ const plots = await run(File.fromPath("feb_2026.pine", "../math"), {
36
+ symbol,
37
+ timeframe: "15m",
38
+ limit: 2688,
39
+ });
40
+
41
+ return await extract(plots, {
42
+ position: "Position",
43
+ entryPrice: "EntryPrice",
44
+ tp: "TP",
45
+ sl: "SL",
46
+ });
47
+ },
48
+ { interval: "15m", key: ([symbol]) => symbol },
49
+ ),
50
+ );
51
+
52
+ const signalOutput = outputNode(async ([pineSource]) => {
53
+ const position =
54
+ pineSource.position === -1
55
+ ? "short"
56
+ : pineSource.position === 1
57
+ ? "long"
58
+ : "wait";
59
+
60
+ if (position === "wait") {
61
+ return null;
62
+ }
63
+
64
+ return {
65
+ id: randomString(),
66
+ position,
67
+ priceOpen: pineSource.entryPrice,
68
+ priceTakeProfit: pineSource.tp,
69
+ priceStopLoss: pineSource.sl,
70
+ minuteEstimatedTime: Infinity,
71
+ } as const;
72
+ }, pineSource);
73
+
74
+ addExchangeSchema({
75
+ exchangeName: "ccxt-exchange",
76
+ getCandles: async (symbol, interval, since, limit) => {
77
+ const exchange = await getExchange();
78
+ const candles = await exchange.fetchOHLCV(
79
+ symbol,
80
+ interval,
81
+ since.getTime(),
82
+ limit,
83
+ );
84
+ return candles.map(([timestamp, open, high, low, close, volume]) => ({
85
+ timestamp,
86
+ open,
87
+ high,
88
+ low,
89
+ close,
90
+ volume,
91
+ }));
92
+ },
93
+ });
94
+
95
+ addFrameSchema({
96
+ frameName: "feb_2026_frame",
97
+ interval: "1m",
98
+ startDate: new Date("2026-02-01T00:00:00Z"),
99
+ endDate: new Date("2026-02-28T23:59:59Z"),
100
+ note: "February 2026",
101
+ });
102
+
103
+ addStrategySchema({
104
+ strategyName: "feb_2026_strategy",
105
+ interval: "1m",
106
+ getSignal: async () => await resolve(signalOutput),
107
+ });
108
+
109
+ listenError((error) => {
110
+ Log.debug("error", {
111
+ error: errorData(error),
112
+ message: getErrorMessage(error),
113
+ });
114
+ });