@backtest-kit/cli 6.1.4 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -1
- package/build/index.cjs +399 -145
- package/build/index.mjs +400 -146
- package/package.json +13 -13
- package/template/project/README.md +29 -0
- package/template/project/package.mustache +5 -5
- package/types.d.ts +22 -0
package/build/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as BacktestKit from 'backtest-kit';
|
|
3
|
-
import { Storage, Notification, Markdown, Report, Dump, Memory, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, setConfig, Log, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync,
|
|
4
|
-
import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, execpool, queued, sleep, randomString,
|
|
3
|
+
import { Storage, Notification, Markdown, Report, Dump, Memory, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, setConfig, Log, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, Exchange } from 'backtest-kit';
|
|
4
|
+
import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, createAwaiter, execpool, queued, sleep, randomString, 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, {
|
|
7
|
+
import path, { join, resolve, basename, extname, dirname } from 'path';
|
|
8
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';
|
|
@@ -224,6 +224,7 @@ const connectionServices$1 = {
|
|
|
224
224
|
};
|
|
225
225
|
const mainServices$1 = {
|
|
226
226
|
backtestMainService: Symbol('backtestMainService'),
|
|
227
|
+
walkerMainService: Symbol('walkerMainService'),
|
|
227
228
|
paperMainService: Symbol('paperMainService'),
|
|
228
229
|
liveMainService: Symbol('liveMainService'),
|
|
229
230
|
};
|
|
@@ -292,6 +293,19 @@ class ResolveService {
|
|
|
292
293
|
_is_launched = true;
|
|
293
294
|
return await readFile(absolutePath, "utf-8");
|
|
294
295
|
};
|
|
296
|
+
this.attachStrategy = async (jsPath) => {
|
|
297
|
+
this.loggerService.log("resolveService attachStrategy", {
|
|
298
|
+
jsPath
|
|
299
|
+
});
|
|
300
|
+
const absolutePath = path.resolve(jsPath);
|
|
301
|
+
await access(absolutePath, constants.F_OK | constants.R_OK);
|
|
302
|
+
const moduleRoot = path.dirname(absolutePath);
|
|
303
|
+
{
|
|
304
|
+
const cwd = process.cwd();
|
|
305
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
306
|
+
}
|
|
307
|
+
this.loaderService.import(absolutePath, moduleRoot);
|
|
308
|
+
};
|
|
295
309
|
this.attachJavascript = async (jsPath) => {
|
|
296
310
|
this.loggerService.log("resolveService attachJavascript", {
|
|
297
311
|
jsPath
|
|
@@ -459,6 +473,10 @@ const getArgs = singleshot(() => {
|
|
|
459
473
|
type: "boolean",
|
|
460
474
|
default: false,
|
|
461
475
|
},
|
|
476
|
+
walker: {
|
|
477
|
+
type: "boolean",
|
|
478
|
+
default: false,
|
|
479
|
+
},
|
|
462
480
|
live: {
|
|
463
481
|
type: "boolean",
|
|
464
482
|
default: false,
|
|
@@ -549,12 +567,11 @@ const getArgs = singleshot(() => {
|
|
|
549
567
|
positionals,
|
|
550
568
|
};
|
|
551
569
|
});
|
|
552
|
-
const
|
|
570
|
+
const getPositionals = singleshot(() => {
|
|
553
571
|
const { positionals = [] } = getArgs();
|
|
554
|
-
|
|
572
|
+
return positionals
|
|
555
573
|
.filter((value) => !DISALLOWED_PATHS.some((path) => value.includes(path)))
|
|
556
|
-
.
|
|
557
|
-
return result || null;
|
|
574
|
+
.filter((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
|
|
558
575
|
});
|
|
559
576
|
|
|
560
577
|
const ADD_FRAME_FN = (self) => {
|
|
@@ -659,11 +676,11 @@ const notifyVerbose = singleshot(() => {
|
|
|
659
676
|
});
|
|
660
677
|
});
|
|
661
678
|
|
|
662
|
-
const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
|
|
663
|
-
const GET_CACHE_INTERVAL_LIST_FN = () => {
|
|
679
|
+
const DEFAULT_CACHE_LIST$1 = ["1m", "15m", "30m", "1h", "4h"];
|
|
680
|
+
const GET_CACHE_INTERVAL_LIST_FN$1 = () => {
|
|
664
681
|
const { values } = getArgs();
|
|
665
682
|
if (!values.cacheInterval) {
|
|
666
|
-
return DEFAULT_CACHE_LIST;
|
|
683
|
+
return DEFAULT_CACHE_LIST$1;
|
|
667
684
|
}
|
|
668
685
|
return String(values.cacheInterval)
|
|
669
686
|
.split(",")
|
|
@@ -688,7 +705,10 @@ class BacktestMainService {
|
|
|
688
705
|
this.frontendProviderService.connect();
|
|
689
706
|
this.telegramProviderService.connect();
|
|
690
707
|
}
|
|
691
|
-
|
|
708
|
+
{
|
|
709
|
+
await this.resolveService.attachJavascript(payload.entryPoint);
|
|
710
|
+
await this.moduleConnectionService.loadModule("./backtest.module");
|
|
711
|
+
}
|
|
692
712
|
{
|
|
693
713
|
this.exchangeSchemaService.addSchema();
|
|
694
714
|
this.symbolSchemaService.addSchema();
|
|
@@ -728,7 +748,6 @@ class BacktestMainService {
|
|
|
728
748
|
});
|
|
729
749
|
notifyVerbose();
|
|
730
750
|
}
|
|
731
|
-
await this.moduleConnectionService.loadModule("./backtest.module");
|
|
732
751
|
Backtest.background(symbol, {
|
|
733
752
|
strategyName,
|
|
734
753
|
frameName,
|
|
@@ -745,11 +764,11 @@ class BacktestMainService {
|
|
|
745
764
|
if (!values.backtest) {
|
|
746
765
|
return;
|
|
747
766
|
}
|
|
748
|
-
const entryPoint =
|
|
767
|
+
const [entryPoint = null] = getPositionals();
|
|
749
768
|
if (!entryPoint) {
|
|
750
769
|
throw new Error("Entry point is required");
|
|
751
770
|
}
|
|
752
|
-
const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
|
|
771
|
+
const cacheInterval = GET_CACHE_INTERVAL_LIST_FN$1();
|
|
753
772
|
return await this.run({
|
|
754
773
|
symbol: values.symbol,
|
|
755
774
|
entryPoint,
|
|
@@ -764,6 +783,170 @@ class BacktestMainService {
|
|
|
764
783
|
}
|
|
765
784
|
}
|
|
766
785
|
|
|
786
|
+
const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
|
|
787
|
+
const WALKER_NAME = "cli-walker";
|
|
788
|
+
const GET_CACHE_INTERVAL_LIST_FN = () => {
|
|
789
|
+
const { values } = getArgs();
|
|
790
|
+
if (!values.cacheInterval) {
|
|
791
|
+
return DEFAULT_CACHE_LIST;
|
|
792
|
+
}
|
|
793
|
+
return String(values.cacheInterval)
|
|
794
|
+
.split(",")
|
|
795
|
+
.map((timeframe) => timeframe.trim());
|
|
796
|
+
};
|
|
797
|
+
class WalkerMainService {
|
|
798
|
+
constructor() {
|
|
799
|
+
this.loggerService = inject(TYPES.loggerService);
|
|
800
|
+
this.resolveService = inject(TYPES.resolveService);
|
|
801
|
+
this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
|
|
802
|
+
this.symbolSchemaService = inject(TYPES.symbolSchemaService);
|
|
803
|
+
this.cacheLogicService = inject(TYPES.cacheLogicService);
|
|
804
|
+
this.moduleConnectionService = inject(TYPES.moduleConnectionService);
|
|
805
|
+
this.run = singleshot(async (payload) => {
|
|
806
|
+
this.loggerService.log("walkerMainService run", { payload });
|
|
807
|
+
for (const entryPoint of payload.entryPoints) {
|
|
808
|
+
await this.resolveService.attachStrategy(entryPoint);
|
|
809
|
+
}
|
|
810
|
+
await this.moduleConnectionService.loadModule("./walker.module");
|
|
811
|
+
{
|
|
812
|
+
this.exchangeSchemaService.addSchema();
|
|
813
|
+
this.symbolSchemaService.addSchema();
|
|
814
|
+
}
|
|
815
|
+
{
|
|
816
|
+
const { length } = await listFrameSchema();
|
|
817
|
+
if (!length) {
|
|
818
|
+
const endDate = alignToInterval(new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), "1m");
|
|
819
|
+
const startDate = alignToInterval(new Date(Date.now() - 31 * 24 * 60 * 60 * 1000), "1m");
|
|
820
|
+
console.warn(`Warning: The default frame schema is set to the interval ${startDate.toISOString()} — ${endDate.toISOString()}. Please make sure to update it according to your needs using addFrameSchema in your strategy files.`);
|
|
821
|
+
addFrameSchema({
|
|
822
|
+
frameName: FrameName$1.DefaultFrame,
|
|
823
|
+
interval: "1m",
|
|
824
|
+
startDate,
|
|
825
|
+
endDate,
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const symbol = payload.symbol || "BTCUSDT";
|
|
830
|
+
const strategyList = await listStrategySchema();
|
|
831
|
+
const strategyNames = strategyList.map((s) => s.strategyName);
|
|
832
|
+
if (!strategyNames.length) {
|
|
833
|
+
throw new Error("No strategies found in provided entry points");
|
|
834
|
+
}
|
|
835
|
+
const [defaultExchangeName = null] = await listExchangeSchema();
|
|
836
|
+
const [defaultFrameName = null] = await listFrameSchema();
|
|
837
|
+
const exchangeName = defaultExchangeName?.exchangeName;
|
|
838
|
+
const frameName = defaultFrameName?.frameName;
|
|
839
|
+
if (!exchangeName) {
|
|
840
|
+
throw new Error("Exchange name is required");
|
|
841
|
+
}
|
|
842
|
+
if (!frameName) {
|
|
843
|
+
throw new Error("Frame name is required");
|
|
844
|
+
}
|
|
845
|
+
addWalkerSchema({
|
|
846
|
+
walkerName: WALKER_NAME,
|
|
847
|
+
exchangeName,
|
|
848
|
+
frameName,
|
|
849
|
+
strategies: strategyNames,
|
|
850
|
+
});
|
|
851
|
+
if (!payload.noCache) {
|
|
852
|
+
await this.cacheLogicService.execute(payload.cacheInterval, {
|
|
853
|
+
exchangeName,
|
|
854
|
+
frameName,
|
|
855
|
+
symbol,
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
if (payload.verbose) {
|
|
859
|
+
overrideExchangeSchema({
|
|
860
|
+
exchangeName,
|
|
861
|
+
callbacks: {
|
|
862
|
+
onCandleData(symbol, interval, since) {
|
|
863
|
+
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
864
|
+
},
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
notifyVerbose();
|
|
868
|
+
}
|
|
869
|
+
if (payload.verbose) {
|
|
870
|
+
overrideWalkerSchema({
|
|
871
|
+
walkerName: WALKER_NAME,
|
|
872
|
+
callbacks: {
|
|
873
|
+
onStrategyStart(strategyName, symbol) {
|
|
874
|
+
console.log(`Strategy started: ${strategyName} for symbol: ${symbol}`);
|
|
875
|
+
},
|
|
876
|
+
onStrategyError(strategyName, symbol, error) {
|
|
877
|
+
console.error(`Strategy error: ${strategyName} for symbol: ${symbol}`, error);
|
|
878
|
+
},
|
|
879
|
+
onStrategyComplete(strategyName, symbol) {
|
|
880
|
+
console.log(`Strategy completed: ${strategyName} for symbol: ${symbol}`);
|
|
881
|
+
},
|
|
882
|
+
onComplete(results) {
|
|
883
|
+
console.log(`Walker completed for symbol: ${results.symbol}`, results);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
Walker.background(symbol, { walkerName: WALKER_NAME });
|
|
889
|
+
const [awaiter, { resolve: res }] = createAwaiter();
|
|
890
|
+
const unWalker = listenDoneWalker(() => {
|
|
891
|
+
console.log("Walker comparison finished");
|
|
892
|
+
unWalker();
|
|
893
|
+
res();
|
|
894
|
+
});
|
|
895
|
+
payload.verbose && console.time("Walker");
|
|
896
|
+
await awaiter;
|
|
897
|
+
payload.verbose && console.timeEnd("Walker");
|
|
898
|
+
const dumpName = payload.output || `walker_${symbol}_${Date.now()}`;
|
|
899
|
+
const dumpDir = join(process.cwd(), "dump");
|
|
900
|
+
if (payload.json) {
|
|
901
|
+
const filePath = resolve(dumpDir, `${dumpName}.json`);
|
|
902
|
+
const data = await Walker.getData(symbol, { walkerName: WALKER_NAME });
|
|
903
|
+
await mkdir(dumpDir, { recursive: true });
|
|
904
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
905
|
+
console.log(`Saved: ${filePath}`);
|
|
906
|
+
process.exit(0);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (payload.markdown) {
|
|
910
|
+
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
911
|
+
const report = await Walker.getReport(symbol, { walkerName: WALKER_NAME });
|
|
912
|
+
await mkdir(dumpDir, { recursive: true });
|
|
913
|
+
await writeFile(filePath, report, "utf-8");
|
|
914
|
+
console.log(`Saved: ${filePath}`);
|
|
915
|
+
process.exit(0);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const report = await Walker.getReport(symbol, { walkerName: WALKER_NAME });
|
|
919
|
+
console.log(report);
|
|
920
|
+
process.exit(0);
|
|
921
|
+
});
|
|
922
|
+
this.connect = singleshot(async () => {
|
|
923
|
+
this.loggerService.log("walkerMainService connect");
|
|
924
|
+
if (!getEntry(import.meta.url)) {
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
const { values } = getArgs();
|
|
928
|
+
if (!values.walker) {
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const entryPoints = getPositionals();
|
|
932
|
+
if (!entryPoints.length) {
|
|
933
|
+
throw new Error("At least one entry point is required");
|
|
934
|
+
}
|
|
935
|
+
const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
|
|
936
|
+
return await this.run({
|
|
937
|
+
entryPoints,
|
|
938
|
+
json: values.json,
|
|
939
|
+
markdown: values.markdown,
|
|
940
|
+
symbol: values.symbol,
|
|
941
|
+
output: values.output,
|
|
942
|
+
cacheInterval,
|
|
943
|
+
verbose: values.verbose,
|
|
944
|
+
noCache: values.noCache,
|
|
945
|
+
});
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
767
950
|
class LiveMainService {
|
|
768
951
|
constructor() {
|
|
769
952
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -781,7 +964,10 @@ class LiveMainService {
|
|
|
781
964
|
this.frontendProviderService.connect();
|
|
782
965
|
this.telegramProviderService.connect();
|
|
783
966
|
}
|
|
784
|
-
|
|
967
|
+
{
|
|
968
|
+
await this.resolveService.attachJavascript(payload.entryPoint);
|
|
969
|
+
await this.moduleConnectionService.loadModule("./live.module");
|
|
970
|
+
}
|
|
785
971
|
{
|
|
786
972
|
this.exchangeSchemaService.addSchema();
|
|
787
973
|
this.symbolSchemaService.addSchema();
|
|
@@ -808,7 +994,6 @@ class LiveMainService {
|
|
|
808
994
|
});
|
|
809
995
|
notifyVerbose();
|
|
810
996
|
}
|
|
811
|
-
await this.moduleConnectionService.loadModule("./live.module");
|
|
812
997
|
Live.background(symbol, {
|
|
813
998
|
strategyName,
|
|
814
999
|
exchangeName,
|
|
@@ -824,7 +1009,7 @@ class LiveMainService {
|
|
|
824
1009
|
if (!values.live) {
|
|
825
1010
|
return;
|
|
826
1011
|
}
|
|
827
|
-
const entryPoint =
|
|
1012
|
+
const [entryPoint = null] = getPositionals();
|
|
828
1013
|
if (!entryPoint) {
|
|
829
1014
|
throw new Error("Entry point is required");
|
|
830
1015
|
}
|
|
@@ -854,7 +1039,10 @@ class PaperMainService {
|
|
|
854
1039
|
this.frontendProviderService.connect();
|
|
855
1040
|
this.telegramProviderService.connect();
|
|
856
1041
|
}
|
|
857
|
-
|
|
1042
|
+
{
|
|
1043
|
+
await this.resolveService.attachJavascript(payload.entryPoint);
|
|
1044
|
+
await this.moduleConnectionService.loadModule("./paper.module");
|
|
1045
|
+
}
|
|
858
1046
|
{
|
|
859
1047
|
this.exchangeSchemaService.addSchema();
|
|
860
1048
|
this.symbolSchemaService.addSchema();
|
|
@@ -881,7 +1069,6 @@ class PaperMainService {
|
|
|
881
1069
|
});
|
|
882
1070
|
notifyVerbose();
|
|
883
1071
|
}
|
|
884
|
-
await this.moduleConnectionService.loadModule("./paper.module");
|
|
885
1072
|
Live.background(symbol, {
|
|
886
1073
|
strategyName,
|
|
887
1074
|
exchangeName,
|
|
@@ -897,7 +1084,7 @@ class PaperMainService {
|
|
|
897
1084
|
if (!values.paper) {
|
|
898
1085
|
return;
|
|
899
1086
|
}
|
|
900
|
-
const entryPoint =
|
|
1087
|
+
const [entryPoint = null] = getPositionals();
|
|
901
1088
|
if (!entryPoint) {
|
|
902
1089
|
throw new Error("Entry point is required");
|
|
903
1090
|
}
|
|
@@ -1898,8 +2085,7 @@ class BabelService {
|
|
|
1898
2085
|
}
|
|
1899
2086
|
}
|
|
1900
2087
|
|
|
1901
|
-
const TRANSPILE_FN = (code, self) => {
|
|
1902
|
-
const require = self.getBaseRequire();
|
|
2088
|
+
const TRANSPILE_FN = memoize(([path]) => `${path}`, (path, code, self, require) => {
|
|
1903
2089
|
const __filename = self.__filename;
|
|
1904
2090
|
const __dirname = self.__dirname;
|
|
1905
2091
|
const module = { exports: {} };
|
|
@@ -1908,7 +2094,7 @@ const TRANSPILE_FN = (code, self) => {
|
|
|
1908
2094
|
eval(self.params.babel.transpile(code));
|
|
1909
2095
|
}
|
|
1910
2096
|
catch (error) {
|
|
1911
|
-
console.log(`Error during transpilation error=\`${getErrorMessage(error)}\` __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
|
|
2097
|
+
console.log(`Error during transpilation error=\`${getErrorMessage(error)}\` path=${path} __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
|
|
1912
2098
|
process.exit(-1);
|
|
1913
2099
|
}
|
|
1914
2100
|
return {
|
|
@@ -1918,18 +2104,18 @@ const TRANSPILE_FN = (code, self) => {
|
|
|
1918
2104
|
exports,
|
|
1919
2105
|
module,
|
|
1920
2106
|
};
|
|
1921
|
-
};
|
|
1922
|
-
const REQUIRE_ENTRY_FACTORY = (filePath, self) => {
|
|
2107
|
+
});
|
|
2108
|
+
const REQUIRE_ENTRY_FACTORY = (filePath, self, seen) => {
|
|
1923
2109
|
{
|
|
1924
2110
|
return null;
|
|
1925
2111
|
}
|
|
1926
2112
|
};
|
|
1927
|
-
const BABEL_ENTRY_FACTORY = (filePath, self) => {
|
|
2113
|
+
const BABEL_ENTRY_FACTORY = (filePath, self, seen) => {
|
|
1928
2114
|
try {
|
|
1929
2115
|
const resolvedPath = path.resolve(self.__dirname, filePath);
|
|
1930
2116
|
const code = fs.readFileSync(resolvedPath, "utf-8");
|
|
1931
2117
|
const child = self.fork(path.dirname(resolvedPath));
|
|
1932
|
-
const { module } = TRANSPILE_FN(code, child);
|
|
2118
|
+
const { module } = TRANSPILE_FN(resolvedPath, code, child, CREATE_BASE_REQUIRE_FN(child, seen));
|
|
1933
2119
|
return "default" in module.exports
|
|
1934
2120
|
? module.exports.default
|
|
1935
2121
|
: module.exports;
|
|
@@ -1970,21 +2156,20 @@ const GET_RESOLVED_EXT_FN = (filePath) => {
|
|
|
1970
2156
|
}
|
|
1971
2157
|
return filePath;
|
|
1972
2158
|
};
|
|
1973
|
-
const ENTRY_FACTORY = (filePath, self) => {
|
|
1974
|
-
filePath = GET_RESOLVED_EXT_FN(filePath);
|
|
2159
|
+
const ENTRY_FACTORY = (filePath, self, seen) => {
|
|
1975
2160
|
{
|
|
1976
2161
|
let result = null;
|
|
1977
2162
|
if ((result = REQUIRE_ENTRY_FACTORY())) {
|
|
1978
2163
|
return result;
|
|
1979
2164
|
}
|
|
1980
|
-
if ((result = BABEL_ENTRY_FACTORY(filePath, self))) {
|
|
2165
|
+
if ((result = BABEL_ENTRY_FACTORY(filePath, self, seen))) {
|
|
1981
2166
|
return result;
|
|
1982
2167
|
}
|
|
1983
2168
|
}
|
|
1984
2169
|
throw new Error(`Failed to load module at ${filePath} (basepath: ${self.params.path})`);
|
|
1985
2170
|
};
|
|
1986
|
-
const CREATE_BASE_REQUIRE_FN = (self) => {
|
|
1987
|
-
const baseRequire =
|
|
2171
|
+
const CREATE_BASE_REQUIRE_FN = (self, seen) => {
|
|
2172
|
+
const baseRequire = self.baseRequire();
|
|
1988
2173
|
return new Proxy(baseRequire, {
|
|
1989
2174
|
apply(_target, _this, args) {
|
|
1990
2175
|
const id = args[0];
|
|
@@ -2005,7 +2190,7 @@ const CREATE_BASE_REQUIRE_FN = (self) => {
|
|
|
2005
2190
|
if (id.startsWith("./") || id.startsWith("../")) {
|
|
2006
2191
|
const resolved = path.resolve(self.__dirname, id);
|
|
2007
2192
|
const child = self.fork(path.dirname(resolved));
|
|
2008
|
-
return child.import(resolved);
|
|
2193
|
+
return child.import(resolved, seen);
|
|
2009
2194
|
}
|
|
2010
2195
|
return baseRequire(id);
|
|
2011
2196
|
},
|
|
@@ -2019,11 +2204,11 @@ const BacktestKitCli = new Proxy({}, {
|
|
|
2019
2204
|
class ClientLoader {
|
|
2020
2205
|
constructor(params) {
|
|
2021
2206
|
this.params = params;
|
|
2022
|
-
this.
|
|
2023
|
-
this.params.logger.log("ClientLoader
|
|
2207
|
+
this.baseRequire = singleshot(() => {
|
|
2208
|
+
this.params.logger.log("ClientLoader baseRequire", {
|
|
2024
2209
|
basePath: this.params.path,
|
|
2025
2210
|
});
|
|
2026
|
-
return
|
|
2211
|
+
return createRequire(this.__filename);
|
|
2027
2212
|
});
|
|
2028
2213
|
this.__filename = path.join(params.path, "index.cjs");
|
|
2029
2214
|
this.__dirname = path.dirname(this.__filename);
|
|
@@ -2039,12 +2224,21 @@ class ClientLoader {
|
|
|
2039
2224
|
logger: this.params.logger,
|
|
2040
2225
|
});
|
|
2041
2226
|
}
|
|
2042
|
-
import(filePath) {
|
|
2227
|
+
import(filePath, seen = new Set()) {
|
|
2043
2228
|
this.params.logger.log("ClientLoader import", {
|
|
2044
2229
|
filePath,
|
|
2045
2230
|
basePath: this.params.path,
|
|
2046
2231
|
});
|
|
2047
|
-
|
|
2232
|
+
const resolved = GET_RESOLVED_EXT_FN(filePath);
|
|
2233
|
+
if (seen.has(resolved)) {
|
|
2234
|
+
throw new Error(`Circular dependency detected: ${resolved} (seen: ${[...seen].join("->")}->${resolved})`);
|
|
2235
|
+
}
|
|
2236
|
+
const currentSeen = new Set(seen);
|
|
2237
|
+
if (!seen.size) {
|
|
2238
|
+
currentSeen.add(path.resolve(this.__dirname, filePath));
|
|
2239
|
+
}
|
|
2240
|
+
currentSeen.add(resolved);
|
|
2241
|
+
return ENTRY_FACTORY(resolved, this, currentSeen);
|
|
2048
2242
|
}
|
|
2049
2243
|
check(filePath) {
|
|
2050
2244
|
this.params.logger.log("ClientLoader check", {
|
|
@@ -2112,6 +2306,7 @@ class LoaderService {
|
|
|
2112
2306
|
}
|
|
2113
2307
|
{
|
|
2114
2308
|
provide(TYPES.backtestMainService, () => new BacktestMainService());
|
|
2309
|
+
provide(TYPES.walkerMainService, () => new WalkerMainService());
|
|
2115
2310
|
provide(TYPES.paperMainService, () => new PaperMainService());
|
|
2116
2311
|
provide(TYPES.liveMainService, () => new LiveMainService());
|
|
2117
2312
|
}
|
|
@@ -2151,6 +2346,7 @@ const connectionServices = {
|
|
|
2151
2346
|
};
|
|
2152
2347
|
const mainServices = {
|
|
2153
2348
|
backtestMainService: inject(TYPES.backtestMainService),
|
|
2349
|
+
walkerMainService: inject(TYPES.walkerMainService),
|
|
2154
2350
|
paperMainService: inject(TYPES.paperMainService),
|
|
2155
2351
|
liveMainService: inject(TYPES.liveMainService),
|
|
2156
2352
|
};
|
|
@@ -2186,8 +2382,14 @@ const cli = {
|
|
|
2186
2382
|
};
|
|
2187
2383
|
init();
|
|
2188
2384
|
|
|
2189
|
-
const MODES = ["backtest", "paper", "live", "pine", "dump", "init", "help", "version"];
|
|
2190
|
-
const
|
|
2385
|
+
const MODES = ["backtest", "walker", "paper", "live", "pine", "dump", "init", "help", "version"];
|
|
2386
|
+
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
2387
|
+
const HELP_TEXT$1 = `
|
|
2388
|
+
Example:
|
|
2389
|
+
|
|
2390
|
+
node ${ENTRY_PATH$1} --help
|
|
2391
|
+
`.trimStart();
|
|
2392
|
+
const main$b = async () => {
|
|
2191
2393
|
if (!getEntry(import.meta.url)) {
|
|
2192
2394
|
return;
|
|
2193
2395
|
}
|
|
@@ -2195,18 +2397,21 @@ const main$a = async () => {
|
|
|
2195
2397
|
if (MODES.some((mode) => values[mode])) {
|
|
2196
2398
|
return;
|
|
2197
2399
|
}
|
|
2198
|
-
process.stdout.write(`@backtest-kit/cli ${"6.
|
|
2400
|
+
process.stdout.write(`@backtest-kit/cli ${"6.2.0"}\n`);
|
|
2401
|
+
process.stdout.write("\n");
|
|
2199
2402
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
2403
|
+
process.stdout.write("\n");
|
|
2404
|
+
process.stdout.write(HELP_TEXT$1);
|
|
2200
2405
|
process.exit(0);
|
|
2201
2406
|
};
|
|
2202
|
-
main$
|
|
2407
|
+
main$b();
|
|
2203
2408
|
|
|
2204
2409
|
const notifyShutdown = singleshot(async () => {
|
|
2205
2410
|
console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
|
|
2206
2411
|
});
|
|
2207
2412
|
|
|
2208
|
-
const BEFORE_EXIT_FN$
|
|
2209
|
-
process.off("SIGINT", BEFORE_EXIT_FN$
|
|
2413
|
+
const BEFORE_EXIT_FN$5 = singleshot(async () => {
|
|
2414
|
+
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
2210
2415
|
const [running = null] = await Backtest.list();
|
|
2211
2416
|
if (!running) {
|
|
2212
2417
|
return;
|
|
@@ -2222,6 +2427,35 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
|
2222
2427
|
frameName,
|
|
2223
2428
|
});
|
|
2224
2429
|
});
|
|
2430
|
+
const listenGracefulShutdown$5 = singleshot(() => {
|
|
2431
|
+
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
2432
|
+
});
|
|
2433
|
+
const main$a = async () => {
|
|
2434
|
+
if (!getEntry(import.meta.url)) {
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
const { values } = getArgs();
|
|
2438
|
+
if (!values.backtest) {
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
await cli.backtestMainService.connect();
|
|
2442
|
+
listenGracefulShutdown$5();
|
|
2443
|
+
};
|
|
2444
|
+
main$a();
|
|
2445
|
+
|
|
2446
|
+
const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
2447
|
+
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
2448
|
+
const [running = null] = await Walker.list();
|
|
2449
|
+
if (!running) {
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
notifyShutdown();
|
|
2453
|
+
const { walkerName, symbol, status } = running;
|
|
2454
|
+
if (status === "fulfilled") {
|
|
2455
|
+
return;
|
|
2456
|
+
}
|
|
2457
|
+
Walker.stop(symbol, { walkerName });
|
|
2458
|
+
});
|
|
2225
2459
|
const listenGracefulShutdown$4 = singleshot(() => {
|
|
2226
2460
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
2227
2461
|
});
|
|
@@ -2230,11 +2464,11 @@ const main$9 = async () => {
|
|
|
2230
2464
|
return;
|
|
2231
2465
|
}
|
|
2232
2466
|
const { values } = getArgs();
|
|
2233
|
-
if (!values.
|
|
2467
|
+
if (!values.walker) {
|
|
2234
2468
|
return;
|
|
2235
2469
|
}
|
|
2236
|
-
await cli.backtestMainService.connect();
|
|
2237
2470
|
listenGracefulShutdown$4();
|
|
2471
|
+
await cli.walkerMainService.connect();
|
|
2238
2472
|
};
|
|
2239
2473
|
main$9();
|
|
2240
2474
|
|
|
@@ -2370,7 +2604,7 @@ const main$4 = async () => {
|
|
|
2370
2604
|
if (!values.pine) {
|
|
2371
2605
|
return;
|
|
2372
2606
|
}
|
|
2373
|
-
const entryPoint =
|
|
2607
|
+
const [entryPoint = null] = getPositionals();
|
|
2374
2608
|
if (!entryPoint) {
|
|
2375
2609
|
return;
|
|
2376
2610
|
}
|
|
@@ -2590,103 +2824,122 @@ const main$2 = async () => {
|
|
|
2590
2824
|
main$2();
|
|
2591
2825
|
|
|
2592
2826
|
const ENTRY_PATH = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
2593
|
-
const HELP_TEXT = `
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
--
|
|
2602
|
-
--
|
|
2603
|
-
--
|
|
2604
|
-
--
|
|
2605
|
-
--
|
|
2606
|
-
--
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
--
|
|
2612
|
-
--
|
|
2613
|
-
--
|
|
2614
|
-
--
|
|
2615
|
-
--
|
|
2616
|
-
--
|
|
2617
|
-
--
|
|
2618
|
-
--
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
--
|
|
2624
|
-
--
|
|
2625
|
-
--
|
|
2626
|
-
--
|
|
2627
|
-
--
|
|
2628
|
-
--
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
--
|
|
2640
|
-
--
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
--
|
|
2649
|
-
--
|
|
2650
|
-
--
|
|
2651
|
-
--
|
|
2652
|
-
--
|
|
2653
|
-
--
|
|
2654
|
-
--
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2827
|
+
const HELP_TEXT = `
|
|
2828
|
+
Usage:
|
|
2829
|
+
node index.mjs --<mode> [flags] [entry-point]
|
|
2830
|
+
|
|
2831
|
+
Modes:
|
|
2832
|
+
|
|
2833
|
+
--backtest <entry> Run strategy against historical candle data
|
|
2834
|
+
--walker <entry...> Run Walker A/B strategy comparison across multiple strategies
|
|
2835
|
+
--paper <entry> Paper trading (live prices, no real orders)
|
|
2836
|
+
--live <entry> Live trading with real orders
|
|
2837
|
+
--pine <entry> Execute a local .pine indicator file
|
|
2838
|
+
--dump Fetch and save raw OHLCV candles
|
|
2839
|
+
--init Scaffold a new project in the current directory
|
|
2840
|
+
--help Print this help message
|
|
2841
|
+
|
|
2842
|
+
Backtest flags:
|
|
2843
|
+
|
|
2844
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2845
|
+
--strategy <string> Strategy name from addStrategySchema (default: first registered)
|
|
2846
|
+
--exchange <string> Exchange name from addExchangeSchema (default: first registered)
|
|
2847
|
+
--frame <string> Frame name from addFrameSchema (default: first registered)
|
|
2848
|
+
--cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
|
|
2849
|
+
--noCache Skip candle cache warming before the run
|
|
2850
|
+
--verbose Log every candle fetch to stdout
|
|
2851
|
+
--ui Start web dashboard at http://localhost:60050
|
|
2852
|
+
--telegram Send trade notifications to Telegram
|
|
2853
|
+
|
|
2854
|
+
Walker flags (--walker):
|
|
2855
|
+
|
|
2856
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2857
|
+
--cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
|
|
2858
|
+
--noCache Skip candle cache warming before the run
|
|
2859
|
+
--verbose Log every candle fetch to stdout
|
|
2860
|
+
--output <string> Output file base name (default: walker_{SYMBOL}_{TIMESTAMP})
|
|
2861
|
+
--json Save results as JSON to ./dump/<output>.json
|
|
2862
|
+
--markdown Save report as Markdown to ./dump/<output>.md
|
|
2863
|
+
|
|
2864
|
+
Each positional argument is a strategy entry point. All strategy files are loaded without
|
|
2865
|
+
changing process.cwd() — .env is read from the working directory only.
|
|
2866
|
+
addWalkerSchema is called automatically using the registered exchange and frame.
|
|
2867
|
+
After comparison completes the report is printed to stdout (or saved if --json/--markdown).
|
|
2868
|
+
|
|
2869
|
+
Module file ./modules/walker.module is loaded automatically if it exists.
|
|
2870
|
+
|
|
2871
|
+
Paper / Live flags:
|
|
2872
|
+
|
|
2873
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2874
|
+
--strategy <string> Strategy name (default: first registered)
|
|
2875
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2876
|
+
--verbose Log every candle fetch to stdout
|
|
2877
|
+
--ui Start web dashboard
|
|
2878
|
+
--telegram Send Telegram notifications
|
|
2879
|
+
|
|
2880
|
+
PineScript flags (--pine):
|
|
2881
|
+
|
|
2882
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2883
|
+
--timeframe <string> Candle interval (default: 15m)
|
|
2884
|
+
--limit <string> Number of candles to fetch (default: 250)
|
|
2885
|
+
--when <string> End date — ISO 8601 or Unix ms (default: now)
|
|
2886
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2887
|
+
--output <string> Output file base name without extension
|
|
2888
|
+
--json Save output as JSON array to <pine-dir>/dump/<output>.json
|
|
2889
|
+
--jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
|
|
2890
|
+
--markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
|
|
2891
|
+
|
|
2892
|
+
Only plot() calls with display=display.data_window produce output columns.
|
|
2893
|
+
Module file ./modules/pine.module is loaded automatically if it exists.
|
|
2894
|
+
|
|
2895
|
+
Candle dump flags (--dump):
|
|
2896
|
+
|
|
2897
|
+
--symbol <string> Trading pair (default: BTCUSDT)
|
|
2898
|
+
--timeframe <string> Candle interval (default: 15m)
|
|
2899
|
+
--limit <string> Number of candles (default: 250)
|
|
2900
|
+
--when <string> End date — ISO 8601 or Unix ms (default: now)
|
|
2901
|
+
--exchange <string> Exchange name (default: first registered)
|
|
2902
|
+
--output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
|
|
2903
|
+
--json Save as JSON array to ./dump/<output>.json
|
|
2904
|
+
--jsonl Save as JSONL to ./dump/<output>.jsonl
|
|
2905
|
+
|
|
2906
|
+
Module file ./modules/dump.module is loaded automatically if it exists.
|
|
2907
|
+
|
|
2908
|
+
Init flags (--init):
|
|
2909
|
+
|
|
2910
|
+
--output <string> Target directory name (default: backtest-kit-project)
|
|
2911
|
+
|
|
2912
|
+
Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
|
|
2913
|
+
|
|
2914
|
+
Module hooks (loaded automatically by each mode):
|
|
2915
|
+
|
|
2916
|
+
modules/backtest.module --backtest Broker adapter for backtest
|
|
2917
|
+
modules/walker.module --walker Broker adapter for walker comparison
|
|
2918
|
+
modules/paper.module --paper Broker adapter for paper trading
|
|
2919
|
+
modules/live.module --live Broker adapter for live trading
|
|
2920
|
+
modules/pine.module --pine Exchange schema for PineScript runs
|
|
2921
|
+
modules/dump.module --dump Exchange schema for candle dumps
|
|
2922
|
+
|
|
2923
|
+
Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
|
|
2924
|
+
|
|
2925
|
+
Environment variables:
|
|
2926
|
+
|
|
2927
|
+
CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
|
|
2928
|
+
CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
|
|
2929
|
+
CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
|
|
2930
|
+
CC_WWWROOT_PORT UI server port (default: 60050)
|
|
2931
|
+
|
|
2932
|
+
Examples:
|
|
2933
|
+
|
|
2934
|
+
node ${ENTRY_PATH} --backtest ./content/feb_2026.strategy.ts
|
|
2935
|
+
node ${ENTRY_PATH} --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
|
|
2936
|
+
node ${ENTRY_PATH} --walker ./content/feb_2026_v1.strategy.ts ./content/feb_2026_v2.strategy.ts ./content/feb_2026_v3.strategy.ts
|
|
2937
|
+
node ${ENTRY_PATH} --walker --symbol BTCUSDT --noCache --markdown ./content/feb_2026_v1.ts ./content/feb_2026_v2.ts
|
|
2938
|
+
node ${ENTRY_PATH} --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
|
|
2939
|
+
node ${ENTRY_PATH} --live --ui --telegram ./content/feb_2026.strategy.ts
|
|
2940
|
+
node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
|
|
2941
|
+
node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
|
|
2942
|
+
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
2690
2943
|
`.trimStart();
|
|
2691
2944
|
const main$1 = async () => {
|
|
2692
2945
|
if (!getEntry(import.meta.url)) {
|
|
@@ -2696,6 +2949,7 @@ const main$1 = async () => {
|
|
|
2696
2949
|
if (!values.help) {
|
|
2697
2950
|
return;
|
|
2698
2951
|
}
|
|
2952
|
+
process.stdout.write(`@backtest-kit/cli ${"6.2.0"}\n\n`);
|
|
2699
2953
|
process.stdout.write(HELP_TEXT);
|
|
2700
2954
|
process.exit(0);
|
|
2701
2955
|
};
|
|
@@ -2709,7 +2963,7 @@ const main = async () => {
|
|
|
2709
2963
|
if (!values.version) {
|
|
2710
2964
|
return;
|
|
2711
2965
|
}
|
|
2712
|
-
process.stdout.write(`@backtest-kit/cli ${"6.
|
|
2966
|
+
process.stdout.write(`@backtest-kit/cli ${"6.2.0"}\n`);
|
|
2713
2967
|
process.exit(0);
|
|
2714
2968
|
};
|
|
2715
2969
|
main();
|
|
@@ -2731,7 +2985,7 @@ async function run(mode, args) {
|
|
|
2731
2985
|
}
|
|
2732
2986
|
if (mode === "backtest") {
|
|
2733
2987
|
await cli.backtestMainService.run(args);
|
|
2734
|
-
listenGracefulShutdown$
|
|
2988
|
+
listenGracefulShutdown$5();
|
|
2735
2989
|
return;
|
|
2736
2990
|
}
|
|
2737
2991
|
if (mode === "paper") {
|