@backtest-kit/cli 8.1.0 → 8.3.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
@@ -241,16 +241,16 @@ const TYPES = {
241
241
 
242
242
  const entrySubject = new BehaviorSubject();
243
243
 
244
- const __filename$1 = fileURLToPath(import.meta.url);
245
- const __dirname$1 = path.dirname(__filename$1);
244
+ const __filename$2 = fileURLToPath(import.meta.url);
245
+ const __dirname$2 = path.dirname(__filename$2);
246
246
  let _is_launched = false;
247
247
  class ResolveService {
248
248
  constructor() {
249
249
  this.loggerService = inject(TYPES.loggerService);
250
250
  this.loaderService = inject(TYPES.loaderService);
251
- this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$1, '..', 'template');
252
- this.DEFAULT_MODULES_DIR = path.resolve(__dirname$1, '..', 'modules');
253
- this.DEFAULT_CONFIG_DIR = path.resolve(__dirname$1, '..', 'config');
251
+ this.DEFAULT_TEMPLATE_DIR = path.resolve(__dirname$2, '..', 'template');
252
+ this.DEFAULT_MODULES_DIR = path.resolve(__dirname$2, '..', 'modules');
253
+ this.DEFAULT_CONFIG_DIR = path.resolve(__dirname$2, '..', 'config');
254
254
  this.OVERRIDE_TEMPLATE_DIR = path.resolve(process.cwd(), 'template');
255
255
  this.OVERRIDE_MODULES_DIR = path.resolve(process.cwd(), 'modules');
256
256
  this.OVERRIDE_CONFIG_DIR = path.resolve(process.cwd(), 'config');
@@ -561,10 +561,22 @@ const getArgs = singleshot(() => {
561
561
  type: "string",
562
562
  default: "",
563
563
  },
564
+ brokerdebug: {
565
+ type: "boolean",
566
+ default: false,
567
+ },
568
+ commit: {
569
+ type: "string",
570
+ default: "",
571
+ },
564
572
  init: {
565
573
  type: "boolean",
566
574
  default: false,
567
575
  },
576
+ docker: {
577
+ type: "boolean",
578
+ default: false,
579
+ },
568
580
  help: {
569
581
  type: "boolean",
570
582
  default: false,
@@ -2970,14 +2982,14 @@ const cli = {
2970
2982
  };
2971
2983
  init();
2972
2984
 
2973
- const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "flush", "init", "help", "version"];
2985
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "brokerdebug", "flush", "init", "docker", "help", "version"];
2974
2986
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
2975
2987
  const HELP_TEXT$1 = `
2976
2988
  Example:
2977
2989
 
2978
2990
  node ${ENTRY_PATH$1} --help
2979
2991
  `.trimStart();
2980
- const main$e = async () => {
2992
+ const main$f = async () => {
2981
2993
  if (!getEntry(import.meta.url)) {
2982
2994
  return;
2983
2995
  }
@@ -2985,14 +2997,14 @@ const main$e = async () => {
2985
2997
  if (MODES.some((mode) => values[mode])) {
2986
2998
  return;
2987
2999
  }
2988
- process.stdout.write(`@backtest-kit/cli ${"8.1.0"}\n`);
3000
+ process.stdout.write(`@backtest-kit/cli ${"8.3.0"}\n`);
2989
3001
  process.stdout.write("\n");
2990
3002
  process.stdout.write(`Run with --help to see available commands.\n`);
2991
3003
  process.stdout.write("\n");
2992
3004
  process.stdout.write(HELP_TEXT$1);
2993
3005
  process.exit(0);
2994
3006
  };
2995
- main$e();
3007
+ main$f();
2996
3008
 
2997
3009
  const notifyShutdown = singleshot(async () => {
2998
3010
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
@@ -3008,7 +3020,7 @@ const flush = async (entryPoint) => {
3008
3020
  console.log(`Removed: ${target}`);
3009
3021
  }
3010
3022
  };
3011
- const main$d = async () => {
3023
+ const main$e = async () => {
3012
3024
  if (!getEntry(import.meta.url)) {
3013
3025
  return;
3014
3026
  }
@@ -3025,7 +3037,7 @@ const main$d = async () => {
3025
3037
  }
3026
3038
  process.exit(0);
3027
3039
  };
3028
- main$d();
3040
+ main$e();
3029
3041
 
3030
3042
  const BEFORE_EXIT_FN$5 = singleshot(async () => {
3031
3043
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3047,7 +3059,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
3047
3059
  const listenGracefulShutdown$5 = singleshot(() => {
3048
3060
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3049
3061
  });
3050
- const main$c = async () => {
3062
+ const main$d = async () => {
3051
3063
  if (!getEntry(import.meta.url)) {
3052
3064
  return;
3053
3065
  }
@@ -3062,7 +3074,7 @@ const main$c = async () => {
3062
3074
  await cli.backtestMainService.connect();
3063
3075
  listenGracefulShutdown$5();
3064
3076
  };
3065
- main$c();
3077
+ main$d();
3066
3078
 
3067
3079
  const BEFORE_EXIT_FN$4 = singleshot(async () => {
3068
3080
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3080,7 +3092,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
3080
3092
  const listenGracefulShutdown$4 = singleshot(() => {
3081
3093
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3082
3094
  });
3083
- const main$b = async () => {
3095
+ const main$c = async () => {
3084
3096
  if (!getEntry(import.meta.url)) {
3085
3097
  return;
3086
3098
  }
@@ -3096,7 +3108,7 @@ const main$b = async () => {
3096
3108
  listenGracefulShutdown$4();
3097
3109
  await cli.walkerMainService.connect();
3098
3110
  };
3099
- main$b();
3111
+ main$c();
3100
3112
 
3101
3113
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
3102
3114
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3117,7 +3129,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
3117
3129
  const listenGracefulShutdown$3 = singleshot(() => {
3118
3130
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3119
3131
  });
3120
- const main$a = async () => {
3132
+ const main$b = async () => {
3121
3133
  if (!getEntry(import.meta.url)) {
3122
3134
  return;
3123
3135
  }
@@ -3128,7 +3140,7 @@ const main$a = async () => {
3128
3140
  cli.paperMainService.connect();
3129
3141
  listenGracefulShutdown$3();
3130
3142
  };
3131
- main$a();
3143
+ main$b();
3132
3144
 
3133
3145
  const BEFORE_EXIT_FN$2 = singleshot(async () => {
3134
3146
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -3149,7 +3161,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
3149
3161
  const listenGracefulShutdown$2 = singleshot(() => {
3150
3162
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3151
3163
  });
3152
- const main$9 = async () => {
3164
+ const main$a = async () => {
3153
3165
  if (!getEntry(import.meta.url)) {
3154
3166
  return;
3155
3167
  }
@@ -3160,7 +3172,7 @@ const main$9 = async () => {
3160
3172
  await cli.liveMainService.connect();
3161
3173
  listenGracefulShutdown$2();
3162
3174
  };
3163
- main$9();
3175
+ main$a();
3164
3176
 
3165
3177
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
3166
3178
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -3170,7 +3182,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
3170
3182
  const listenGracefulShutdown$1 = singleshot(() => {
3171
3183
  process.on("SIGINT", BEFORE_EXIT_FN$1);
3172
3184
  });
3173
- const main$8 = async () => {
3185
+ const main$9 = async () => {
3174
3186
  if (!getEntry(import.meta.url)) {
3175
3187
  return;
3176
3188
  }
@@ -3180,7 +3192,7 @@ const main$8 = async () => {
3180
3192
  }
3181
3193
  listenGracefulShutdown$1();
3182
3194
  };
3183
- main$8();
3195
+ main$9();
3184
3196
 
3185
3197
  const BEFORE_EXIT_FN = singleshot(async () => {
3186
3198
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -3190,7 +3202,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
3190
3202
  const listenGracefulShutdown = singleshot(() => {
3191
3203
  process.on("SIGINT", BEFORE_EXIT_FN);
3192
3204
  });
3193
- const main$7 = async () => {
3205
+ const main$8 = async () => {
3194
3206
  if (!getEntry(import.meta.url)) {
3195
3207
  return;
3196
3208
  }
@@ -3200,7 +3212,7 @@ const main$7 = async () => {
3200
3212
  }
3201
3213
  listenGracefulShutdown();
3202
3214
  };
3203
- main$7();
3215
+ main$8();
3204
3216
 
3205
3217
  const EXTRACT_ROWS_FN = (plots, schema) => {
3206
3218
  const keys = Object.keys(schema);
@@ -3222,7 +3234,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
3222
3234
  }
3223
3235
  return rows;
3224
3236
  };
3225
- const main$6 = async () => {
3237
+ const main$7 = async () => {
3226
3238
  if (!getEntry(import.meta.url)) {
3227
3239
  return;
3228
3240
  }
@@ -3302,9 +3314,9 @@ const main$6 = async () => {
3302
3314
  console.log(await toMarkdown(signalId, plots, signalSchema));
3303
3315
  process.exit(0);
3304
3316
  };
3305
- main$6();
3317
+ main$7();
3306
3318
 
3307
- const main$5 = async () => {
3319
+ const main$6 = async () => {
3308
3320
  if (!getEntry(import.meta.url)) {
3309
3321
  return;
3310
3322
  }
@@ -3339,9 +3351,9 @@ const main$5 = async () => {
3339
3351
  };
3340
3352
  process.on("SIGINT", beforeExit);
3341
3353
  };
3342
- main$5();
3354
+ main$6();
3343
3355
 
3344
- const main$4 = async () => {
3356
+ const main$5 = async () => {
3345
3357
  if (!getEntry(import.meta.url)) {
3346
3358
  return;
3347
3359
  }
@@ -3402,9 +3414,9 @@ const main$4 = async () => {
3402
3414
  console.log(JSON.stringify(candles, null, 2));
3403
3415
  process.exit(0);
3404
3416
  };
3405
- main$4();
3417
+ main$5();
3406
3418
 
3407
- const main$3 = async () => {
3419
+ const main$4 = async () => {
3408
3420
  if (!getEntry(import.meta.url)) {
3409
3421
  return;
3410
3422
  }
@@ -3498,16 +3510,16 @@ const main$3 = async () => {
3498
3510
  }
3499
3511
  process.exit(0);
3500
3512
  };
3501
- main$3();
3513
+ main$4();
3502
3514
 
3503
- const __filename = fileURLToPath(import.meta.url);
3504
- const __dirname = dirname(__filename);
3515
+ const __filename$1 = fileURLToPath(import.meta.url);
3516
+ const __dirname$1 = dirname(__filename$1);
3505
3517
  const MUSTACHE_EXT = ".mustache";
3506
3518
  const MUSTACHE_RENAME = {
3507
3519
  "gitignore": ".gitignore",
3508
3520
  "package": "package.json",
3509
3521
  };
3510
- async function isDirEmpty(dirPath) {
3522
+ async function isDirEmpty$1(dirPath) {
3511
3523
  try {
3512
3524
  const files = await readdir(dirPath);
3513
3525
  return files.length === 0;
@@ -3519,13 +3531,13 @@ async function isDirEmpty(dirPath) {
3519
3531
  throw error;
3520
3532
  }
3521
3533
  }
3522
- async function copyDir(srcDir, destDir, data) {
3534
+ async function copyDir$1(srcDir, destDir, data) {
3523
3535
  await mkdir(destDir, { recursive: true });
3524
3536
  const entries = await readdir(srcDir, { withFileTypes: true });
3525
3537
  for (const entry of entries) {
3526
3538
  const srcPath = join(srcDir, entry.name);
3527
3539
  if (entry.isDirectory()) {
3528
- await copyDir(srcPath, join(destDir, entry.name), data);
3540
+ await copyDir$1(srcPath, join(destDir, entry.name), data);
3529
3541
  continue;
3530
3542
  }
3531
3543
  if (entry.name === ".gitkeep") {
@@ -3561,7 +3573,7 @@ function runScript(scriptPath, cwd) {
3561
3573
  child.on("error", reject);
3562
3574
  });
3563
3575
  }
3564
- function runNpmInstall(cwd) {
3576
+ function runNpmInstall$1(cwd) {
3565
3577
  return new Promise((resolve, reject) => {
3566
3578
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
3567
3579
  const child = spawn(npm, ["install"], { cwd, stdio: "inherit", shell: true });
@@ -3575,7 +3587,7 @@ function runNpmInstall(cwd) {
3575
3587
  child.on("error", reject);
3576
3588
  });
3577
3589
  }
3578
- const main$2 = async () => {
3590
+ const main$3 = async () => {
3579
3591
  if (!getEntry(import.meta.url)) {
3580
3592
  return;
3581
3593
  }
@@ -3585,21 +3597,95 @@ const main$2 = async () => {
3585
3597
  }
3586
3598
  const projectName = values.output || "backtest-kit-project";
3587
3599
  const projectPath = join(process.cwd(), projectName);
3588
- const templatePath = join(__dirname, "../template/project");
3589
- const isEmpty = await isDirEmpty(projectPath);
3600
+ const templatePath = join(__dirname$1, "../template/project");
3601
+ const isEmpty = await isDirEmpty$1(projectPath);
3590
3602
  if (!isEmpty) {
3591
3603
  console.error(`Directory "${projectName}" already exists and is not empty.`);
3592
3604
  process.exit(1);
3593
3605
  }
3594
3606
  console.log(`Creating project in ${projectPath}`);
3595
- await copyDir(templatePath, projectPath, { PROJECT_NAME: projectName });
3607
+ await copyDir$1(templatePath, projectPath, { PROJECT_NAME: projectName });
3596
3608
  console.log(`Fetching docs...`);
3597
3609
  await runScript(join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
3598
3610
  console.log(`Installing dependencies...`);
3599
- await runNpmInstall(projectPath);
3611
+ await runNpmInstall$1(projectPath);
3600
3612
  console.log(`Done! Project created at ${projectPath}`);
3601
3613
  process.exit(0);
3602
3614
  };
3615
+ main$3();
3616
+
3617
+ const __filename = fileURLToPath(import.meta.url);
3618
+ const __dirname = dirname(__filename);
3619
+ async function isDirEmpty(dirPath) {
3620
+ try {
3621
+ const files = await readdir(dirPath);
3622
+ return files.length === 0;
3623
+ }
3624
+ catch (error) {
3625
+ if (error.code === "ENOENT") {
3626
+ return true;
3627
+ }
3628
+ throw error;
3629
+ }
3630
+ }
3631
+ async function copyDir(srcDir, destDir) {
3632
+ await mkdir(destDir, { recursive: true });
3633
+ const entries = await readdir(srcDir, { withFileTypes: true });
3634
+ for (const entry of entries) {
3635
+ const srcPath = join(srcDir, entry.name);
3636
+ if (entry.isDirectory()) {
3637
+ await copyDir(srcPath, join(destDir, entry.name));
3638
+ continue;
3639
+ }
3640
+ if (entry.name === ".gitkeep") {
3641
+ continue;
3642
+ }
3643
+ const destPath = join(destDir, entry.name);
3644
+ await copyFile(srcPath, destPath);
3645
+ console.log(` -> ${destPath}`);
3646
+ }
3647
+ }
3648
+ function runNpmInstall(cwd) {
3649
+ return new Promise((resolve, reject) => {
3650
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
3651
+ const child = spawn(npm, ["install"], { cwd, stdio: "inherit", shell: true });
3652
+ child.on("close", (code) => {
3653
+ if (code !== 0) {
3654
+ reject(new Error(`npm install exited with code ${code}`));
3655
+ return;
3656
+ }
3657
+ resolve();
3658
+ });
3659
+ child.on("error", reject);
3660
+ });
3661
+ }
3662
+ const main$2 = async () => {
3663
+ if (!getEntry(import.meta.url)) {
3664
+ return;
3665
+ }
3666
+ const { values } = getArgs();
3667
+ if (!values.docker) {
3668
+ return;
3669
+ }
3670
+ const projectName = values.output || "backtest-kit-docker";
3671
+ const projectPath = join(process.cwd(), projectName);
3672
+ const templatePath = join(__dirname, "../docker");
3673
+ const isEmpty = await isDirEmpty(projectPath);
3674
+ if (!isEmpty) {
3675
+ console.error(`Directory "${projectName}" already exists and is not empty.`);
3676
+ process.exit(1);
3677
+ }
3678
+ console.log(`Creating Docker workspace in ${projectPath}`);
3679
+ await copyDir(templatePath, projectPath);
3680
+ console.log(`Installing dependencies...`);
3681
+ await runNpmInstall(projectPath);
3682
+ console.log(`Done! Docker workspace created at ${projectPath}`);
3683
+ console.log(`Next steps:`);
3684
+ console.log(` cd ${projectName}`);
3685
+ console.log(` docker compose up -d`);
3686
+ console.log(` docker compose logs -f`);
3687
+ process.exit(0);
3688
+ };
3603
3689
  main$2();
3604
3690
 
3605
3691
  const ENTRY_PATH = "./node_modules/@backtest-kit/cli/build/index.mjs";
@@ -3617,8 +3703,10 @@ Modes:
3617
3703
  --editor Open the Pine Script visual editor in the browser
3618
3704
  --dump Fetch and save raw OHLCV candles
3619
3705
  --pnldebug Simulate PnL per minute for a given entry price and direction
3706
+ --brokerdebug Fire a single broker commit against the live broker adapter
3620
3707
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
3621
3708
  --init Scaffold a new project in the current directory
3709
+ --docker Scaffold a Docker workspace for running strategies in a container
3622
3710
  --help Print this help message
3623
3711
 
3624
3712
  Backtest flags:
@@ -3704,6 +3792,17 @@ PnL debug flags (--pnldebug):
3704
3792
 
3705
3793
  Module file ./modules/pnldebug.module is loaded automatically if it exists.
3706
3794
 
3795
+ Broker debug flags (--brokerdebug):
3796
+
3797
+ --symbol <string> Trading pair (default: BTCUSDT)
3798
+ --exchange <string> Exchange name (default: first registered)
3799
+ --commit <string> Commit type to fire: signal-open, signal-close, partial-profit,
3800
+ partial-loss, average-buy, trailing-stop, trailing-take, breakeven
3801
+ (default: signal-open)
3802
+
3803
+ Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
3804
+ the selected broker commit with synthetic payload values derived from current price.
3805
+
3707
3806
  Flush flags (--flush):
3708
3807
 
3709
3808
  One or more positional entry points. For each entry point the following
@@ -3717,6 +3816,14 @@ Init flags (--init):
3717
3816
 
3718
3817
  Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
3719
3818
 
3819
+ Docker flags (--docker):
3820
+
3821
+ --output <string> Target directory name (default: backtest-kit-docker)
3822
+
3823
+ Scaffolds a Docker workspace: docker-compose.yaml, .env.example, package.json,
3824
+ tsconfig.json, and a sample strategy under content/. Run npm install then
3825
+ docker compose up to start the container.
3826
+
3720
3827
  Module hooks (loaded automatically by each mode):
3721
3828
 
3722
3829
  modules/backtest.module --backtest Broker adapter for backtest
@@ -3726,7 +3833,8 @@ Module hooks (loaded automatically by each mode):
3726
3833
  modules/pine.module --pine Exchange schema for PineScript runs
3727
3834
  modules/editor.module --editor Exchange schema for the visual Pine editor
3728
3835
  modules/dump.module --dump Exchange schema for candle dumps
3729
- modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
3836
+ modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
3837
+ modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
3730
3838
 
3731
3839
  --flush has no associated module. It only removes dump subdirectories.
3732
3840
 
@@ -3752,9 +3860,12 @@ Examples:
3752
3860
  node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
3753
3861
  node ${ENTRY_PATH} --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
3754
3862
  node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
3863
+ node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
3864
+ node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
3755
3865
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
3756
3866
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
3757
3867
  node ${ENTRY_PATH} --init --output my-trading-bot
3868
+ node ${ENTRY_PATH} --docker --output my-docker-workspace
3758
3869
  `.trimStart();
3759
3870
  const main$1 = async () => {
3760
3871
  if (!getEntry(import.meta.url)) {
@@ -3764,7 +3875,7 @@ const main$1 = async () => {
3764
3875
  if (!values.help) {
3765
3876
  return;
3766
3877
  }
3767
- process.stdout.write(`@backtest-kit/cli ${"8.1.0"}\n\n`);
3878
+ process.stdout.write(`@backtest-kit/cli ${"8.3.0"}\n\n`);
3768
3879
  process.stdout.write(HELP_TEXT);
3769
3880
  process.exit(0);
3770
3881
  };
@@ -3778,7 +3889,7 @@ const main = async () => {
3778
3889
  if (!values.version) {
3779
3890
  return;
3780
3891
  }
3781
- process.stdout.write(`@backtest-kit/cli ${"8.1.0"}\n`);
3892
+ process.stdout.write(`@backtest-kit/cli ${"8.3.0"}\n`);
3782
3893
  process.exit(0);
3783
3894
  };
3784
3895
  main();
@@ -0,0 +1,2 @@
1
+ CC_REDIS_HOST=host.docker.internal
2
+ CC_MONGO_CONNECTION_STRING=mongodb://host.docker.internal:27017/backtest-kit?wtimeoutMS=15000
@@ -0,0 +1,11 @@
1
+ import {
2
+ addStrategySchema,
3
+ } from "backtest-kit";
4
+
5
+ addStrategySchema({
6
+ strategyName: "feb_2026_strategy",
7
+ getSignal: async (_symbol, when) => {
8
+ console.log(when);
9
+ return null;
10
+ },
11
+ });
@@ -0,0 +1,83 @@
1
+ import { addExchangeSchema, addFrameSchema, roundTicks } from "backtest-kit";
2
+ import { singleshot } from "functools-kit";
3
+ import ccxt from "ccxt";
4
+
5
+ const getExchange = singleshot(async () => {
6
+ const exchange = new ccxt.binance({
7
+ options: {
8
+ defaultType: "spot",
9
+ adjustForTimeDifference: true,
10
+ recvWindow: 60000,
11
+ },
12
+ enableRateLimit: true,
13
+ });
14
+ await exchange.loadMarkets();
15
+ return exchange;
16
+ });
17
+
18
+ addExchangeSchema({
19
+ exchangeName: "ccxt-exchange",
20
+ getCandles: async (symbol, interval, since, limit) => {
21
+ const exchange = await getExchange();
22
+ const candles = await exchange.fetchOHLCV(
23
+ symbol,
24
+ interval,
25
+ since.getTime(),
26
+ limit,
27
+ );
28
+ return candles.map(([timestamp, open, high, low, close, volume]) => ({
29
+ timestamp,
30
+ open,
31
+ high,
32
+ low,
33
+ close,
34
+ volume,
35
+ }));
36
+ },
37
+ getOrderBook: async (symbol, depth, _from, _to, backtest) => {
38
+ if (backtest) {
39
+ throw new Error(
40
+ "Order book fetching is not supported in backtest mode for the default exchange schema. Please implement it according to your needs.",
41
+ );
42
+ }
43
+ const exchange = await getExchange();
44
+ const bookData = await exchange.fetchOrderBook(symbol, depth);
45
+ return {
46
+ symbol,
47
+ asks: bookData.asks.map(([price, quantity]) => ({
48
+ price: String(price),
49
+ quantity: String(quantity),
50
+ })),
51
+ bids: bookData.bids.map(([price, quantity]) => ({
52
+ price: String(price),
53
+ quantity: String(quantity),
54
+ })),
55
+ };
56
+ },
57
+ formatPrice: async (symbol, price) => {
58
+ const exchange = await getExchange();
59
+ const market = exchange.market(symbol);
60
+ const tickSize = market.limits?.price?.min || market.precision?.price;
61
+ if (tickSize !== undefined) {
62
+ return roundTicks(price, tickSize);
63
+ }
64
+ return exchange.priceToPrecision(symbol, price);
65
+ },
66
+ formatQuantity: async (symbol, quantity) => {
67
+ const exchange = await getExchange();
68
+ const market = exchange.market(symbol);
69
+ const stepSize = market.limits?.amount?.min || market.precision?.amount;
70
+ if (stepSize !== undefined) {
71
+ return roundTicks(quantity, stepSize);
72
+ }
73
+ return exchange.amountToPrecision(symbol, quantity);
74
+ },
75
+ });
76
+
77
+ addFrameSchema({
78
+ frameName: "feb_2026_frame",
79
+ interval: "1m",
80
+ startDate: new Date("2026-02-01T00:00:00Z"),
81
+ endDate: new Date("2026-02-28T23:59:59Z"),
82
+ note: "February 2026",
83
+ });
@@ -0,0 +1,24 @@
1
+ version: '3.8'
2
+
3
+ services:
4
+ backtest:
5
+ image: tripolskypetr/backtest-kit
6
+ network_mode: host
7
+ extra_hosts:
8
+ - "host.docker.internal:host-gateway"
9
+ container_name: backtest
10
+ ports:
11
+ - "60050:60050"
12
+ restart: unless-stopped
13
+ volumes:
14
+ - ./:/workspace
15
+ working_dir: /workspace
16
+ command:
17
+ - --backtest
18
+ - --symbol
19
+ - BTCUSDT
20
+ - --strategy
21
+ - feb_2026_strategy
22
+ - --exchange
23
+ - ccxt-exchange
24
+ - ./content/feb_2026/feb_2026.strategy.ts
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "example",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "node ./node_modules/@backtest-kit/cli/build/index.mjs",
8
+ "start:debug": "node --inspect-brk ./node_modules/@backtest-kit/cli/build/index.mjs"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "ISC",
13
+ "type": "commonjs",
14
+ "devDependencies": {
15
+ "@types/node": "25.6.0"
16
+ },
17
+ "dependencies": {
18
+ "@backtest-kit/cli": "8.3.0",
19
+ "@backtest-kit/graph": "8.3.0",
20
+ "@backtest-kit/pinets": "8.3.0",
21
+ "@backtest-kit/signals": "8.3.0",
22
+ "@backtest-kit/ui": "8.3.0",
23
+ "@tavily/core": "0.7.2",
24
+ "@tensorflow/tfjs": "4.22.0",
25
+ "@tensorflow/tfjs-backend-wasm": "4.22.0",
26
+ "@tensorflow/tfjs-core": "4.22.0",
27
+ "agent-swarm-kit": "2.6.0",
28
+ "backtest-kit": "8.3.0",
29
+ "dayjs": "1.11.20",
30
+ "functools-kit": "2.3.0",
31
+ "garch": "1.2.3",
32
+ "get-moment-stamp": "1.1.2",
33
+ "jsonrepair": "3.12.0",
34
+ "ollama": "0.6.3",
35
+ "slugify": "1.6.9",
36
+ "volume-anomaly": "1.2.3"
37
+ }
38
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "compilerOptions": {
3
+ "ignoreDeprecations": "6.0",
4
+ "lib": [
5
+ "esnext",
6
+ "dom"
7
+ ],
8
+ "types": [
9
+ "node"
10
+ ],
11
+ "moduleDetection": "force",
12
+ "target": "ES2020",
13
+ "module": "ESNext",
14
+ "moduleResolution": "bundler",
15
+ "noEmit": true,
16
+ "strict": false,
17
+ "downlevelIteration": true,
18
+ "noImplicitAny": false,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUnusedLocals": false,
22
+ "noUnusedParameters": false,
23
+ "noPropertyAccessFromIndexSignature": false,
24
+ "paths": {
25
+ "logic": ["./logic/index.ts"],
26
+ "logic/*": ["./logic/*"],
27
+ "utils": ["./utils/index.ts"],
28
+ "utils/*": ["./utils/*"]
29
+ }
30
+ },
31
+ "include": [
32
+ "./logic",
33
+ "./content",
34
+ "./modules",
35
+ ],
36
+ }