@backtest-kit/sidekick 8.4.0 → 8.5.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.
Files changed (49) hide show
  1. package/README.md +173 -173
  2. package/content/config/source/timeframe_15m.pine +113 -113
  3. package/content/config/source/timeframe_4h.pine +56 -56
  4. package/content/config/symbol.config.cjs +460 -460
  5. package/content/docker/ollama/docker-compose.yaml +34 -34
  6. package/content/docker/ollama/watch.sh +2 -2
  7. package/content/scripts/cache/cache_candles.mjs +47 -47
  8. package/content/scripts/cache/cache_model.mjs +42 -42
  9. package/content/scripts/cache/validate_candles.mjs +46 -46
  10. package/content/scripts/run_timeframe_15m.mjs +77 -77
  11. package/content/scripts/run_timeframe_4h.mjs +68 -68
  12. package/package.json +68 -68
  13. package/scripts/init.mjs +304 -304
  14. package/src/classes/BacktestLowerStopOnBreakevenAction.mjs +17 -17
  15. package/src/classes/BacktestPartialProfitTakingAction.mjs +24 -24
  16. package/src/classes/BacktestPositionMonitorAction.mjs +57 -57
  17. package/src/config/ccxt.mjs +15 -15
  18. package/src/config/params.mjs +1 -1
  19. package/src/config/setup.mjs +45 -45
  20. package/src/config/validate.mjs +14 -14
  21. package/src/enum/ActionName.mjs +5 -5
  22. package/src/enum/ExchangeName.mjs +3 -3
  23. package/src/enum/FrameName.mjs +6 -6
  24. package/src/enum/RiskName.mjs +4 -4
  25. package/src/enum/StrategyName.mjs +3 -3
  26. package/src/index.mjs +6 -6
  27. package/src/logic/action/backtest_lower_stop_on_breakeven.action.mjs +9 -9
  28. package/src/logic/action/backtest_partial_profit_taking.action.mjs +9 -9
  29. package/src/logic/action/backtest_position_monitor.action.mjs +9 -9
  30. package/src/logic/exchange/binance.exchange.mjs +69 -69
  31. package/src/logic/frame/dec_2025.frame.mjs +10 -10
  32. package/src/logic/frame/feb_2024.frame.mjs +10 -10
  33. package/src/logic/frame/nov_2025.frame.mjs +10 -10
  34. package/src/logic/frame/oct_2025.frame.mjs +10 -10
  35. package/src/logic/index.mjs +15 -15
  36. package/src/logic/risk/sl_distance.risk.mjs +32 -32
  37. package/src/logic/risk/tp_distance.risk.mjs +32 -32
  38. package/src/logic/strategy/main.strategy.mjs +48 -48
  39. package/src/main/bootstrap.mjs +52 -52
  40. package/src/math/timeframe_15m.math.mjs +68 -68
  41. package/src/math/timeframe_4h.math.mjs +53 -53
  42. package/src/utils/getArgs.mjs +17 -17
  43. package/template/CLAUDE.mustache +421 -421
  44. package/template/README.mustache +257 -257
  45. package/template/env.mustache +2 -2
  46. package/template/gitignore.mustache +29 -29
  47. package/template/jsconfig.json.mustache +26 -26
  48. package/template/package.mustache +37 -37
  49. package/template/types.mustache +11 -11
package/scripts/init.mjs CHANGED
@@ -1,304 +1,304 @@
1
- #!/usr/bin/env node
2
-
3
- import { promises as fs } from 'fs';
4
- import path from 'path';
5
- import { fileURLToPath } from 'url';
6
- import { spawn } from 'child_process';
7
- import pc from 'picocolors';
8
- import { glob } from 'glob';
9
- import Mustache from 'mustache';
10
- import logSymbols from 'log-symbols';
11
-
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = path.dirname(__filename);
14
-
15
- const log = {
16
- info: (msg) => console.log(`${pc.cyan(logSymbols.info)} ${msg}`),
17
- success: (msg) => console.log(`${pc.green(logSymbols.success)} ${msg}`),
18
- error: (msg) => console.log(`${pc.red(logSymbols.error)} ${msg}`),
19
- warn: (msg) => console.log(`${pc.yellow(logSymbols.warning)} ${msg}`),
20
- };
21
-
22
- /**
23
- * Copy files using glob patterns
24
- */
25
- async function copyFiles(srcDir, destDir, pattern = '**/*') {
26
- await fs.mkdir(destDir, { recursive: true });
27
-
28
- const files = await glob(pattern, {
29
- cwd: srcDir,
30
- nodir: true,
31
- dot: true,
32
- absolute: false,
33
- });
34
-
35
- for (const file of files) {
36
- const srcPath = path.join(srcDir, file);
37
- const destPath = path.join(destDir, file);
38
-
39
- // Create destination directory if needed
40
- await fs.mkdir(path.dirname(destPath), { recursive: true });
41
-
42
- // Copy file
43
- await fs.copyFile(srcPath, destPath);
44
-
45
- // Log copied file
46
- console.log(` ${pc.dim('→')} ${file}`);
47
- }
48
- }
49
-
50
- /**
51
- * Check if directory exists and is empty
52
- */
53
- async function isDirEmpty(dirPath) {
54
- try {
55
- const files = await fs.readdir(dirPath);
56
- return files.length === 0;
57
- } catch (error) {
58
- if (error.code === 'ENOENT') {
59
- return true; // Directory doesn't exist, treat as empty
60
- }
61
- throw error;
62
- }
63
- }
64
-
65
- /**
66
- * Run npm install
67
- */
68
- function runNpmInstall(projectPath) {
69
- return new Promise((resolve, reject) => {
70
- log.info('Installing dependencies...');
71
-
72
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
73
- const install = spawn(npm, ['install'], {
74
- cwd: projectPath,
75
- stdio: 'inherit',
76
- shell: true,
77
- });
78
-
79
- install.on('close', (code) => {
80
- if (code !== 0) {
81
- reject(new Error(`npm install exited with code ${code}`));
82
- return;
83
- }
84
- resolve();
85
- });
86
-
87
- install.on('error', reject);
88
- });
89
- }
90
-
91
- /**
92
- * Load and render template file
93
- */
94
- async function renderTemplate(templatePath, data) {
95
- const template = await fs.readFile(templatePath, 'utf-8');
96
- return Mustache.render(template, data);
97
- }
98
-
99
- /**
100
- * Main function
101
- */
102
- async function main() {
103
- console.log();
104
- console.log(`${pc.bold(pc.blue('🧿 Backtest Kit'))}`);
105
- console.log();
106
-
107
- // Get project name from arguments
108
- const args = process.argv.slice(2);
109
- const projectName = args[0] || 'my-backtest-project';
110
-
111
- const projectPath = path.resolve(process.cwd(), projectName);
112
- const srcTemplatePath = path.resolve(__dirname, '..', 'src');
113
- const contentPath = path.resolve(__dirname, '..', 'content');
114
- const templateDir = path.resolve(__dirname, '..', 'template');
115
-
116
- // Template data for Mustache
117
- const templateData = {
118
- PROJECT_NAME: projectName,
119
- };
120
-
121
- try {
122
- // Check if directory exists and is not empty
123
- {
124
- const isEmpty = await isDirEmpty(projectPath);
125
- if (!isEmpty) {
126
- log.error(`Directory ${projectName} already exists and is not empty.`);
127
- process.exit(1);
128
- }
129
- }
130
-
131
- {
132
- log.info(`Creating a new Backtest Kit project in ${pc.bold(projectPath)}`);
133
- console.log();
134
- }
135
-
136
- // Create project directory
137
- {
138
- await fs.mkdir(projectPath, { recursive: true });
139
- log.success('Created project directory');
140
- }
141
-
142
- // Copy source template files using glob
143
- {
144
- log.info('Copying source files...');
145
- await copyFiles(srcTemplatePath, path.join(projectPath, 'src'));
146
- log.success('Copied source files');
147
- }
148
-
149
- // Copy content files (config/, scripts/, docker/)
150
- {
151
- log.info('Copying content files...');
152
- await copyFiles(contentPath, projectPath);
153
- log.success('Copied content files');
154
- }
155
-
156
- // Create types/backtest-kit.d.ts from template
157
- {
158
- log.info('Creating types/backtest-kit.d.ts...');
159
- await fs.mkdir(path.join(projectPath, 'types'), { recursive: true });
160
- const typesContent = await renderTemplate(
161
- path.join(templateDir, 'types.mustache'),
162
- templateData
163
- );
164
- await fs.writeFile(
165
- path.join(projectPath, 'types', 'backtest-kit.d.ts'),
166
- typesContent,
167
- 'utf-8'
168
- );
169
- log.success('Created types/backtest-kit.d.ts');
170
- }
171
-
172
- // Create package.json from template
173
- {
174
- log.info('Creating package.json...');
175
- const packageJsonContent = await renderTemplate(
176
- path.join(templateDir, 'package.mustache'),
177
- templateData
178
- );
179
- await fs.writeFile(
180
- path.join(projectPath, 'package.json'),
181
- packageJsonContent,
182
- 'utf-8'
183
- );
184
- log.success('Created package.json');
185
- }
186
-
187
- // Create .env files from template
188
- {
189
- log.info('Creating .env template...');
190
- const envContent = await renderTemplate(
191
- path.join(templateDir, 'env.mustache'),
192
- templateData
193
- );
194
- await fs.writeFile(
195
- path.join(projectPath, '.env.example'),
196
- envContent,
197
- 'utf-8'
198
- );
199
- await fs.writeFile(
200
- path.join(projectPath, '.env'),
201
- envContent,
202
- 'utf-8'
203
- );
204
- log.success('Created .env files');
205
- }
206
-
207
- // Create .gitignore from template
208
- {
209
- log.info('Creating .gitignore...');
210
- const gitignoreContent = await renderTemplate(
211
- path.join(templateDir, 'gitignore.mustache'),
212
- templateData
213
- );
214
- await fs.writeFile(
215
- path.join(projectPath, '.gitignore'),
216
- gitignoreContent,
217
- 'utf-8'
218
- );
219
- log.success('Created .gitignore');
220
- }
221
-
222
- // Create README.md from template
223
- {
224
- log.info('Creating README.md...');
225
- const readmeContent = await renderTemplate(
226
- path.join(templateDir, 'README.mustache'),
227
- templateData
228
- );
229
- await fs.writeFile(
230
- path.join(projectPath, 'README.md'),
231
- readmeContent,
232
- 'utf-8'
233
- );
234
- log.success('Created README.md');
235
- }
236
-
237
- // Create jsconfig.json from template
238
- {
239
- log.info('Creating jsconfig.json...');
240
- const jsconfigContent = await renderTemplate(
241
- path.join(templateDir, 'jsconfig.json.mustache'),
242
- templateData
243
- );
244
- await fs.writeFile(
245
- path.join(projectPath, 'jsconfig.json'),
246
- jsconfigContent,
247
- 'utf-8'
248
- );
249
- log.success('Created jsconfig.json');
250
- }
251
-
252
- // Create CLAUDE.md from template
253
- {
254
- log.info('Creating CLAUDE.md...');
255
- const claudeContent = await renderTemplate(
256
- path.join(templateDir, 'CLAUDE.mustache'),
257
- templateData
258
- );
259
- await fs.writeFile(
260
- path.join(projectPath, 'CLAUDE.md'),
261
- claudeContent,
262
- 'utf-8'
263
- );
264
- log.success('Created CLAUDE.md');
265
- console.log();
266
- }
267
-
268
- // Install dependencies
269
- {
270
- await runNpmInstall(projectPath);
271
- console.log();
272
- log.success('Installation complete!');
273
- console.log();
274
- }
275
-
276
- // Display success message with instructions
277
- {
278
- console.log(`${pc.bold('Success!')} Created ${pc.cyan(projectName)} at ${pc.bold(projectPath)}`);
279
- console.log();
280
- console.log('Inside that directory, you can run several commands:');
281
- console.log();
282
- console.log(` ${pc.cyan('npm start')}`);
283
- console.log(' Runs the default backtest.');
284
- console.log();
285
- console.log('We suggest that you begin by typing:');
286
- console.log();
287
- console.log(` ${pc.cyan(`cd ${projectName}`)}`);
288
- console.log(` ${pc.cyan('npm start')}`);
289
- console.log();
290
- console.log(`${pc.yellow('Don\'t forget to configure your API keys in .env file!')}`);
291
- console.log();
292
- console.log('Happy trading! 🚀');
293
- console.log();
294
- }
295
-
296
- } catch (error) {
297
- log.error('Failed to create project:');
298
- console.error(error);
299
- process.exit(1);
300
- }
301
- }
302
-
303
- // Run the script
304
- main().catch(console.error);
1
+ #!/usr/bin/env node
2
+
3
+ import { promises as fs } from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { spawn } from 'child_process';
7
+ import pc from 'picocolors';
8
+ import { glob } from 'glob';
9
+ import Mustache from 'mustache';
10
+ import logSymbols from 'log-symbols';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ const log = {
16
+ info: (msg) => console.log(`${pc.cyan(logSymbols.info)} ${msg}`),
17
+ success: (msg) => console.log(`${pc.green(logSymbols.success)} ${msg}`),
18
+ error: (msg) => console.log(`${pc.red(logSymbols.error)} ${msg}`),
19
+ warn: (msg) => console.log(`${pc.yellow(logSymbols.warning)} ${msg}`),
20
+ };
21
+
22
+ /**
23
+ * Copy files using glob patterns
24
+ */
25
+ async function copyFiles(srcDir, destDir, pattern = '**/*') {
26
+ await fs.mkdir(destDir, { recursive: true });
27
+
28
+ const files = await glob(pattern, {
29
+ cwd: srcDir,
30
+ nodir: true,
31
+ dot: true,
32
+ absolute: false,
33
+ });
34
+
35
+ for (const file of files) {
36
+ const srcPath = path.join(srcDir, file);
37
+ const destPath = path.join(destDir, file);
38
+
39
+ // Create destination directory if needed
40
+ await fs.mkdir(path.dirname(destPath), { recursive: true });
41
+
42
+ // Copy file
43
+ await fs.copyFile(srcPath, destPath);
44
+
45
+ // Log copied file
46
+ console.log(` ${pc.dim('→')} ${file}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Check if directory exists and is empty
52
+ */
53
+ async function isDirEmpty(dirPath) {
54
+ try {
55
+ const files = await fs.readdir(dirPath);
56
+ return files.length === 0;
57
+ } catch (error) {
58
+ if (error.code === 'ENOENT') {
59
+ return true; // Directory doesn't exist, treat as empty
60
+ }
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Run npm install
67
+ */
68
+ function runNpmInstall(projectPath) {
69
+ return new Promise((resolve, reject) => {
70
+ log.info('Installing dependencies...');
71
+
72
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
73
+ const install = spawn(npm, ['install'], {
74
+ cwd: projectPath,
75
+ stdio: 'inherit',
76
+ shell: true,
77
+ });
78
+
79
+ install.on('close', (code) => {
80
+ if (code !== 0) {
81
+ reject(new Error(`npm install exited with code ${code}`));
82
+ return;
83
+ }
84
+ resolve();
85
+ });
86
+
87
+ install.on('error', reject);
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Load and render template file
93
+ */
94
+ async function renderTemplate(templatePath, data) {
95
+ const template = await fs.readFile(templatePath, 'utf-8');
96
+ return Mustache.render(template, data);
97
+ }
98
+
99
+ /**
100
+ * Main function
101
+ */
102
+ async function main() {
103
+ console.log();
104
+ console.log(`${pc.bold(pc.blue('🧿 Backtest Kit'))}`);
105
+ console.log();
106
+
107
+ // Get project name from arguments
108
+ const args = process.argv.slice(2);
109
+ const projectName = args[0] || 'my-backtest-project';
110
+
111
+ const projectPath = path.resolve(process.cwd(), projectName);
112
+ const srcTemplatePath = path.resolve(__dirname, '..', 'src');
113
+ const contentPath = path.resolve(__dirname, '..', 'content');
114
+ const templateDir = path.resolve(__dirname, '..', 'template');
115
+
116
+ // Template data for Mustache
117
+ const templateData = {
118
+ PROJECT_NAME: projectName,
119
+ };
120
+
121
+ try {
122
+ // Check if directory exists and is not empty
123
+ {
124
+ const isEmpty = await isDirEmpty(projectPath);
125
+ if (!isEmpty) {
126
+ log.error(`Directory ${projectName} already exists and is not empty.`);
127
+ process.exit(1);
128
+ }
129
+ }
130
+
131
+ {
132
+ log.info(`Creating a new Backtest Kit project in ${pc.bold(projectPath)}`);
133
+ console.log();
134
+ }
135
+
136
+ // Create project directory
137
+ {
138
+ await fs.mkdir(projectPath, { recursive: true });
139
+ log.success('Created project directory');
140
+ }
141
+
142
+ // Copy source template files using glob
143
+ {
144
+ log.info('Copying source files...');
145
+ await copyFiles(srcTemplatePath, path.join(projectPath, 'src'));
146
+ log.success('Copied source files');
147
+ }
148
+
149
+ // Copy content files (config/, scripts/, docker/)
150
+ {
151
+ log.info('Copying content files...');
152
+ await copyFiles(contentPath, projectPath);
153
+ log.success('Copied content files');
154
+ }
155
+
156
+ // Create types/backtest-kit.d.ts from template
157
+ {
158
+ log.info('Creating types/backtest-kit.d.ts...');
159
+ await fs.mkdir(path.join(projectPath, 'types'), { recursive: true });
160
+ const typesContent = await renderTemplate(
161
+ path.join(templateDir, 'types.mustache'),
162
+ templateData
163
+ );
164
+ await fs.writeFile(
165
+ path.join(projectPath, 'types', 'backtest-kit.d.ts'),
166
+ typesContent,
167
+ 'utf-8'
168
+ );
169
+ log.success('Created types/backtest-kit.d.ts');
170
+ }
171
+
172
+ // Create package.json from template
173
+ {
174
+ log.info('Creating package.json...');
175
+ const packageJsonContent = await renderTemplate(
176
+ path.join(templateDir, 'package.mustache'),
177
+ templateData
178
+ );
179
+ await fs.writeFile(
180
+ path.join(projectPath, 'package.json'),
181
+ packageJsonContent,
182
+ 'utf-8'
183
+ );
184
+ log.success('Created package.json');
185
+ }
186
+
187
+ // Create .env files from template
188
+ {
189
+ log.info('Creating .env template...');
190
+ const envContent = await renderTemplate(
191
+ path.join(templateDir, 'env.mustache'),
192
+ templateData
193
+ );
194
+ await fs.writeFile(
195
+ path.join(projectPath, '.env.example'),
196
+ envContent,
197
+ 'utf-8'
198
+ );
199
+ await fs.writeFile(
200
+ path.join(projectPath, '.env'),
201
+ envContent,
202
+ 'utf-8'
203
+ );
204
+ log.success('Created .env files');
205
+ }
206
+
207
+ // Create .gitignore from template
208
+ {
209
+ log.info('Creating .gitignore...');
210
+ const gitignoreContent = await renderTemplate(
211
+ path.join(templateDir, 'gitignore.mustache'),
212
+ templateData
213
+ );
214
+ await fs.writeFile(
215
+ path.join(projectPath, '.gitignore'),
216
+ gitignoreContent,
217
+ 'utf-8'
218
+ );
219
+ log.success('Created .gitignore');
220
+ }
221
+
222
+ // Create README.md from template
223
+ {
224
+ log.info('Creating README.md...');
225
+ const readmeContent = await renderTemplate(
226
+ path.join(templateDir, 'README.mustache'),
227
+ templateData
228
+ );
229
+ await fs.writeFile(
230
+ path.join(projectPath, 'README.md'),
231
+ readmeContent,
232
+ 'utf-8'
233
+ );
234
+ log.success('Created README.md');
235
+ }
236
+
237
+ // Create jsconfig.json from template
238
+ {
239
+ log.info('Creating jsconfig.json...');
240
+ const jsconfigContent = await renderTemplate(
241
+ path.join(templateDir, 'jsconfig.json.mustache'),
242
+ templateData
243
+ );
244
+ await fs.writeFile(
245
+ path.join(projectPath, 'jsconfig.json'),
246
+ jsconfigContent,
247
+ 'utf-8'
248
+ );
249
+ log.success('Created jsconfig.json');
250
+ }
251
+
252
+ // Create CLAUDE.md from template
253
+ {
254
+ log.info('Creating CLAUDE.md...');
255
+ const claudeContent = await renderTemplate(
256
+ path.join(templateDir, 'CLAUDE.mustache'),
257
+ templateData
258
+ );
259
+ await fs.writeFile(
260
+ path.join(projectPath, 'CLAUDE.md'),
261
+ claudeContent,
262
+ 'utf-8'
263
+ );
264
+ log.success('Created CLAUDE.md');
265
+ console.log();
266
+ }
267
+
268
+ // Install dependencies
269
+ {
270
+ await runNpmInstall(projectPath);
271
+ console.log();
272
+ log.success('Installation complete!');
273
+ console.log();
274
+ }
275
+
276
+ // Display success message with instructions
277
+ {
278
+ console.log(`${pc.bold('Success!')} Created ${pc.cyan(projectName)} at ${pc.bold(projectPath)}`);
279
+ console.log();
280
+ console.log('Inside that directory, you can run several commands:');
281
+ console.log();
282
+ console.log(` ${pc.cyan('npm start')}`);
283
+ console.log(' Runs the default backtest.');
284
+ console.log();
285
+ console.log('We suggest that you begin by typing:');
286
+ console.log();
287
+ console.log(` ${pc.cyan(`cd ${projectName}`)}`);
288
+ console.log(` ${pc.cyan('npm start')}`);
289
+ console.log();
290
+ console.log(`${pc.yellow('Don\'t forget to configure your API keys in .env file!')}`);
291
+ console.log();
292
+ console.log('Happy trading! 🚀');
293
+ console.log();
294
+ }
295
+
296
+ } catch (error) {
297
+ log.error('Failed to create project:');
298
+ console.error(error);
299
+ process.exit(1);
300
+ }
301
+ }
302
+
303
+ // Run the script
304
+ main().catch(console.error);
@@ -1,17 +1,17 @@
1
- import { ActionBase, commitTrailingStop } from "backtest-kit";
2
-
3
- /**
4
- * Lowers trailing-stop by 3 points when breakeven is reached (ignores volatility)
5
- * @implements {bt.IPublicAction}
6
- */
7
- export class BacktestLowerStopOnBreakevenAction extends ActionBase {
8
- /**
9
- * @param {bt.BreakevenContract} param0
10
- */
11
- async breakevenAvailable({ symbol, currentPrice }) {
12
- // Lower trailing-stop by 3 points (negative value brings stop-loss closer to entry)
13
- await commitTrailingStop(symbol, -3, currentPrice);
14
- }
15
- }
16
-
17
- export default BacktestLowerStopOnBreakevenAction;
1
+ import { ActionBase, commitTrailingStop } from "backtest-kit";
2
+
3
+ /**
4
+ * Lowers trailing-stop by 3 points when breakeven is reached (ignores volatility)
5
+ * @implements {bt.IPublicAction}
6
+ */
7
+ export class BacktestLowerStopOnBreakevenAction extends ActionBase {
8
+ /**
9
+ * @param {bt.BreakevenContract} param0
10
+ */
11
+ async breakevenAvailable({ symbol, currentPrice }) {
12
+ // Lower trailing-stop by 3 points (negative value brings stop-loss closer to entry)
13
+ await commitTrailingStop(symbol, -3, currentPrice);
14
+ }
15
+ }
16
+
17
+ export default BacktestLowerStopOnBreakevenAction;