@backtest-kit/pinets 14.1.0 → 15.1.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 +245 -245
- package/build/index.cjs +106 -85
- package/build/index.mjs +106 -85
- package/package.json +90 -90
- package/types.d.ts +6 -0
package/build/index.cjs
CHANGED
|
@@ -9,7 +9,9 @@ var fs = require('fs/promises');
|
|
|
9
9
|
var module$1 = require('module');
|
|
10
10
|
|
|
11
11
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
12
|
-
|
|
12
|
+
// Symbol.for keeps the brand check working when CJS and ESM copies of the
|
|
13
|
+
// package end up in the same process (dual-package hazard).
|
|
14
|
+
const CODE_TYPE_SYMBOL = Symbol.for("backtest-kit.pinets.code-type");
|
|
13
15
|
class Code {
|
|
14
16
|
constructor(source) {
|
|
15
17
|
this.source = source;
|
|
@@ -28,7 +30,9 @@ Code.isCode = (value) => {
|
|
|
28
30
|
value.__type__ === CODE_TYPE_SYMBOL);
|
|
29
31
|
};
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
// Symbol.for keeps the brand check working when CJS and ESM copies of the
|
|
34
|
+
// package end up in the same process (dual-package hazard).
|
|
35
|
+
const FILE_TYPE_SYMBOL = Symbol.for("backtest-kit.pinets.file-type");
|
|
32
36
|
class File {
|
|
33
37
|
constructor(path, baseDir) {
|
|
34
38
|
this.path = path;
|
|
@@ -243,6 +247,22 @@ const ExchangeContextService = diScoped.scoped(class {
|
|
|
243
247
|
}
|
|
244
248
|
});
|
|
245
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Known quote assets for base/quote splitting in getSymbolInfo, longest first.
|
|
252
|
+
* Symbols with an unrecognized quote keep the whole ticker as base.
|
|
253
|
+
*/
|
|
254
|
+
const QUOTE_ASSETS = [
|
|
255
|
+
"USDT",
|
|
256
|
+
"USDC",
|
|
257
|
+
"BUSD",
|
|
258
|
+
"TUSD",
|
|
259
|
+
"FDUSD",
|
|
260
|
+
"USD",
|
|
261
|
+
"BTC",
|
|
262
|
+
"ETH",
|
|
263
|
+
"BNB",
|
|
264
|
+
"EUR",
|
|
265
|
+
];
|
|
246
266
|
const PINE_TF_MAP = {
|
|
247
267
|
"1": "1m",
|
|
248
268
|
"3": "3m",
|
|
@@ -286,6 +306,11 @@ class CandleProviderService {
|
|
|
286
306
|
.toUpperCase()
|
|
287
307
|
.replace(/^BINANCE:|^BYBIT:|^OKX:/, "");
|
|
288
308
|
const normalizedTimeframe = PINE_TF_MAP[timeframe] ?? timeframe;
|
|
309
|
+
if (!INTERVAL_MINUTES[normalizedTimeframe]) {
|
|
310
|
+
throw new Error(`CandleProvider getMarketData: unknown timeframe=${timeframe}. ` +
|
|
311
|
+
`Allowed Pine values: ${Object.keys(PINE_TF_MAP).join(", ")}; ` +
|
|
312
|
+
`allowed intervals: ${Object.keys(INTERVAL_MINUTES).join(", ")}`);
|
|
313
|
+
}
|
|
289
314
|
let clampedEDate = eDate;
|
|
290
315
|
if (backtestKit.ExecutionContextService.hasContext()) {
|
|
291
316
|
const whenMs = backtestKit.lib.executionContextService.context.when.getTime();
|
|
@@ -311,12 +336,12 @@ class CandleProviderService {
|
|
|
311
336
|
const symbol = tickerId
|
|
312
337
|
.toUpperCase()
|
|
313
338
|
.replace(/^BINANCE:|^BYBIT:|^OKX:/, "");
|
|
314
|
-
const
|
|
315
|
-
const
|
|
339
|
+
const quote = QUOTE_ASSETS.find((asset) => symbol.endsWith(asset) && symbol.length > asset.length) ?? "";
|
|
340
|
+
const base = quote ? symbol.slice(0, symbol.length - quote.length) : symbol;
|
|
316
341
|
const result = {
|
|
317
342
|
ticker: symbol,
|
|
318
343
|
tickerid: symbol,
|
|
319
|
-
description: `${base}/${quote}
|
|
344
|
+
description: quote ? `${base}/${quote}` : symbol,
|
|
320
345
|
type: "crypto",
|
|
321
346
|
basecurrency: base,
|
|
322
347
|
currency: quote || "USDT",
|
|
@@ -361,8 +386,13 @@ class PineJobService {
|
|
|
361
386
|
limit,
|
|
362
387
|
});
|
|
363
388
|
const runner = await CREATE_RUNNER_FN(this, tickerId, timeframe, limit);
|
|
364
|
-
|
|
365
|
-
|
|
389
|
+
// The Indicator class is only required to pass custom inputs. Do not load
|
|
390
|
+
// it otherwise: usePine-only setups (no pinets package, no useIndicator)
|
|
391
|
+
// must keep working for plain script runs.
|
|
392
|
+
const payload = Object.keys(inputs).length
|
|
393
|
+
? await this.indicatorConnectionService.getInstance(code.source, inputs)
|
|
394
|
+
: code.source;
|
|
395
|
+
const result = await runner.run(payload);
|
|
366
396
|
const plots = Object.entries(result.plots).reduce((acm, [key, value]) => {
|
|
367
397
|
if (key === "undefined") {
|
|
368
398
|
return acm;
|
|
@@ -377,12 +407,30 @@ class PineJobService {
|
|
|
377
407
|
}
|
|
378
408
|
}
|
|
379
409
|
|
|
380
|
-
const GET_VALUE_FN = (plots, name, barsBack = 0) => {
|
|
410
|
+
const GET_VALUE_FN = (plots, name, barsBack = 0, defaultValue) => {
|
|
381
411
|
const data = plots[name]?.data;
|
|
382
|
-
if (!data || data.length === 0)
|
|
383
|
-
|
|
412
|
+
if (!data || data.length === 0) {
|
|
413
|
+
if (defaultValue !== undefined) {
|
|
414
|
+
return defaultValue;
|
|
415
|
+
}
|
|
416
|
+
throw new Error(`pinets extract: plot "${name}" is missing or empty. ` +
|
|
417
|
+
`Available plots: ${Object.keys(plots).join(", ") || "<none>"}`);
|
|
418
|
+
}
|
|
384
419
|
const idx = data.length - 1 - barsBack;
|
|
385
|
-
|
|
420
|
+
if (idx < 0) {
|
|
421
|
+
if (defaultValue !== undefined) {
|
|
422
|
+
return defaultValue;
|
|
423
|
+
}
|
|
424
|
+
throw new Error(`pinets extract: plot "${name}" has only ${data.length} points, cannot read barsBack=${barsBack}`);
|
|
425
|
+
}
|
|
426
|
+
const point = data[idx];
|
|
427
|
+
if (point == null || point.value == null) {
|
|
428
|
+
if (defaultValue !== undefined) {
|
|
429
|
+
return defaultValue;
|
|
430
|
+
}
|
|
431
|
+
throw new Error(`pinets extract: plot "${name}" has no value at barsBack=${barsBack}`);
|
|
432
|
+
}
|
|
433
|
+
return point.value;
|
|
386
434
|
};
|
|
387
435
|
const GET_VALUE_AT_FN = (plots, name, i, barsBack = 0) => {
|
|
388
436
|
const data = plots[name]?.data;
|
|
@@ -414,13 +462,19 @@ class PineDataService {
|
|
|
414
462
|
}
|
|
415
463
|
else {
|
|
416
464
|
const raw = GET_VALUE_AT_FN(plots, config.plot, i, config.barsBack ?? 0);
|
|
417
|
-
|
|
465
|
+
const value = raw ?? config.defaultValue ?? null;
|
|
466
|
+
row[key] = (value !== null && config.transform ? config.transform(value) : value);
|
|
418
467
|
}
|
|
419
468
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
469
|
+
let timestamp = "";
|
|
470
|
+
for (const name of plotNames) {
|
|
471
|
+
const point = plots[name]?.data?.[i];
|
|
472
|
+
if (point?.time) {
|
|
473
|
+
timestamp = new Date(point.time).toISOString();
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
row.timestamp = timestamp;
|
|
424
478
|
rows.push(row);
|
|
425
479
|
}
|
|
426
480
|
return rows;
|
|
@@ -437,7 +491,7 @@ class PineDataService {
|
|
|
437
491
|
Object.assign(result, { [key]: GET_VALUE_FN(plots, config) });
|
|
438
492
|
}
|
|
439
493
|
else {
|
|
440
|
-
const value = GET_VALUE_FN(plots, config.plot, config.barsBack ?? 0);
|
|
494
|
+
const value = GET_VALUE_FN(plots, config.plot, config.barsBack ?? 0, config.defaultValue);
|
|
441
495
|
Object.assign(result, {
|
|
442
496
|
[key]: config.transform ? config.transform(value) : value,
|
|
443
497
|
});
|
|
@@ -595,6 +649,9 @@ class PineMarkdownService {
|
|
|
595
649
|
if (entries.length === 0) {
|
|
596
650
|
return [];
|
|
597
651
|
}
|
|
652
|
+
if ("time" in mapping) {
|
|
653
|
+
throw new Error('pineMarkdownService getData: mapping key "time" is reserved for the row timestamp, rename the mapping key');
|
|
654
|
+
}
|
|
598
655
|
const plotNames = entries.map(([key, config]) => ({ key, plotName: getPlotName(config) }));
|
|
599
656
|
const dataLength = Math.max(...plotNames.map(({ plotName }) => plots[plotName]?.data?.length ?? 0));
|
|
600
657
|
if (dataLength === 0) {
|
|
@@ -791,8 +848,11 @@ function useIndicator(ctor) {
|
|
|
791
848
|
pine.indicatorConnectionService.useIndicator(ctor);
|
|
792
849
|
}
|
|
793
850
|
|
|
794
|
-
|
|
795
|
-
|
|
851
|
+
/**
|
|
852
|
+
* Resolves a script source (File reference or inline Code) into Code.
|
|
853
|
+
* Shared by run(), markdown() and getSignal().
|
|
854
|
+
*/
|
|
855
|
+
const getSourceCode = async (source) => {
|
|
796
856
|
if (File.isFile(source)) {
|
|
797
857
|
const code = await pine.pineCacheService.readFile(source.path, source.baseDir);
|
|
798
858
|
return Code.fromString(code);
|
|
@@ -802,14 +862,21 @@ const GET_SOURCE_FN$2 = async (source) => {
|
|
|
802
862
|
}
|
|
803
863
|
throw new Error("Source must be a File or Code instance");
|
|
804
864
|
};
|
|
805
|
-
const
|
|
806
|
-
const VALIDATE_NO_TRADING_FN$1 = () => {
|
|
865
|
+
const VALIDATE_NO_TRADING_FN = () => {
|
|
807
866
|
if (backtestKit.ExecutionContextService.hasContext()) {
|
|
808
867
|
throw new Error(functoolsKit.str.newline("Time overrides are not allowed when running scripts in a trading context.", "Please remove the 'when' parameter from the run function call."));
|
|
809
868
|
}
|
|
810
869
|
};
|
|
811
|
-
|
|
812
|
-
|
|
870
|
+
/**
|
|
871
|
+
* Runs a Pine script through PineJobService, optionally scoped to an
|
|
872
|
+
* exchange context and/or a fixed point in time (backtest-style override).
|
|
873
|
+
* Shared by run() and markdown().
|
|
874
|
+
*/
|
|
875
|
+
const runInference = async (script, symbol, timeframe, limit, inputs, exchangeName, when) => {
|
|
876
|
+
if (when) {
|
|
877
|
+
VALIDATE_NO_TRADING_FN();
|
|
878
|
+
}
|
|
879
|
+
let fn = () => pine.pineJobService.run(script, symbol, timeframe, limit, inputs);
|
|
813
880
|
if (exchangeName) {
|
|
814
881
|
fn = ExchangeContextService.runWithContext(fn, { exchangeName });
|
|
815
882
|
}
|
|
@@ -820,15 +887,10 @@ const CREATE_INFERENCE_FN$1 = (script, symbol, timeframe, limit, inputs, exchang
|
|
|
820
887
|
backtest: true,
|
|
821
888
|
});
|
|
822
889
|
}
|
|
823
|
-
return fn;
|
|
824
|
-
};
|
|
825
|
-
const RUN_INFERENCE_FN$1 = async (script, symbol, timeframe, limit, inputs, exchangeName, when) => {
|
|
826
|
-
if (when) {
|
|
827
|
-
VALIDATE_NO_TRADING_FN$1();
|
|
828
|
-
}
|
|
829
|
-
const inference = CREATE_INFERENCE_FN$1(script, symbol, timeframe, limit, inputs, exchangeName, when);
|
|
830
|
-
return await inference();
|
|
890
|
+
return await fn();
|
|
831
891
|
};
|
|
892
|
+
|
|
893
|
+
const METHOD_NAME_RUN$1 = "run.run";
|
|
832
894
|
async function run(source, { symbol, timeframe, limit, inputs = {} }, exchangeName, when) {
|
|
833
895
|
pine.loggerService.info(METHOD_NAME_RUN$1, {
|
|
834
896
|
source,
|
|
@@ -836,8 +898,8 @@ async function run(source, { symbol, timeframe, limit, inputs = {} }, exchangeNa
|
|
|
836
898
|
timeframe,
|
|
837
899
|
limit,
|
|
838
900
|
});
|
|
839
|
-
const script = await
|
|
840
|
-
const { plots } = await
|
|
901
|
+
const script = await getSourceCode(source);
|
|
902
|
+
const { plots } = await runInference(script, symbol, timeframe, limit, inputs, exchangeName, when);
|
|
841
903
|
return plots;
|
|
842
904
|
}
|
|
843
905
|
|
|
@@ -860,6 +922,7 @@ function setLogger(logger) {
|
|
|
860
922
|
pine.loggerService.setLogger(logger);
|
|
861
923
|
}
|
|
862
924
|
|
|
925
|
+
const IS_VALID_PRICE_OPEN_FN = (value) => typeof value === "number" && isFinite(value) && value > 0;
|
|
863
926
|
function toSignalDto(id, data, priceOpen = data.priceOpen) {
|
|
864
927
|
if (data.position === 1) {
|
|
865
928
|
const result = {
|
|
@@ -869,7 +932,7 @@ function toSignalDto(id, data, priceOpen = data.priceOpen) {
|
|
|
869
932
|
priceStopLoss: data.priceStopLoss,
|
|
870
933
|
minuteEstimatedTime: data.minuteEstimatedTime,
|
|
871
934
|
};
|
|
872
|
-
if (priceOpen) {
|
|
935
|
+
if (IS_VALID_PRICE_OPEN_FN(priceOpen)) {
|
|
873
936
|
Object.assign(result, { priceOpen });
|
|
874
937
|
}
|
|
875
938
|
return result;
|
|
@@ -882,7 +945,7 @@ function toSignalDto(id, data, priceOpen = data.priceOpen) {
|
|
|
882
945
|
priceStopLoss: data.priceStopLoss,
|
|
883
946
|
minuteEstimatedTime: data.minuteEstimatedTime,
|
|
884
947
|
};
|
|
885
|
-
if (priceOpen) {
|
|
948
|
+
if (IS_VALID_PRICE_OPEN_FN(priceOpen)) {
|
|
886
949
|
Object.assign(result, { priceOpen });
|
|
887
950
|
}
|
|
888
951
|
return result;
|
|
@@ -892,23 +955,18 @@ function toSignalDto(id, data, priceOpen = data.priceOpen) {
|
|
|
892
955
|
|
|
893
956
|
const METHOD_NAME_RUN = "strategy.getSignal";
|
|
894
957
|
const DEFAULT_ESTIMATED_TIME = 240;
|
|
895
|
-
const GET_SOURCE_FN$1 = async (source) => {
|
|
896
|
-
if (File.isFile(source)) {
|
|
897
|
-
const code = await pine.pineCacheService.readFile(source.path, source.baseDir);
|
|
898
|
-
return Code.fromString(code);
|
|
899
|
-
}
|
|
900
|
-
if (Code.isCode(source)) {
|
|
901
|
-
return source;
|
|
902
|
-
}
|
|
903
|
-
throw new Error("Source must be a File or Code instance");
|
|
904
|
-
};
|
|
905
958
|
const SIGNAL_SCHEMA = {
|
|
906
959
|
position: "Signal",
|
|
907
|
-
priceOpen:
|
|
960
|
+
priceOpen: {
|
|
961
|
+
plot: "Close",
|
|
962
|
+
defaultValue: 0,
|
|
963
|
+
transform: (v) => v,
|
|
964
|
+
},
|
|
908
965
|
priceTakeProfit: "TakeProfit",
|
|
909
966
|
priceStopLoss: "StopLoss",
|
|
910
967
|
minuteEstimatedTime: {
|
|
911
968
|
plot: "EstimatedTime",
|
|
969
|
+
defaultValue: 0,
|
|
912
970
|
transform: (v) => v || DEFAULT_ESTIMATED_TIME,
|
|
913
971
|
},
|
|
914
972
|
};
|
|
@@ -919,7 +977,7 @@ async function getSignal(source, { symbol, timeframe, limit, inputs = {} }) {
|
|
|
919
977
|
timeframe,
|
|
920
978
|
limit,
|
|
921
979
|
});
|
|
922
|
-
const { plots } = await pine.pineJobService.run(await
|
|
980
|
+
const { plots } = await pine.pineJobService.run(await getSourceCode(source), symbol, timeframe, limit, inputs);
|
|
923
981
|
const resultId = functoolsKit.randomString();
|
|
924
982
|
const data = pine.pineDataService.extract(plots, SIGNAL_SCHEMA);
|
|
925
983
|
return toSignalDto(resultId, data);
|
|
@@ -938,43 +996,6 @@ async function dumpPlotData(signalId, plots, mapping, taName, outputDir = `./dum
|
|
|
938
996
|
|
|
939
997
|
const TO_MARKDOWN_METHOD_NAME = "markdown.toMarkdown";
|
|
940
998
|
const MARKDOWN_METHOD_NAME = "markdown.markdown";
|
|
941
|
-
const GET_SOURCE_FN = async (source) => {
|
|
942
|
-
if (File.isFile(source)) {
|
|
943
|
-
const code = await pine.pineCacheService.readFile(source.path, source.baseDir);
|
|
944
|
-
return Code.fromString(code);
|
|
945
|
-
}
|
|
946
|
-
if (Code.isCode(source)) {
|
|
947
|
-
return source;
|
|
948
|
-
}
|
|
949
|
-
throw new Error("Source must be a File or Code instance");
|
|
950
|
-
};
|
|
951
|
-
const BASE_RUNNER_FN = async (script, symbol, timeframe, limit, inputs) => await pine.pineJobService.run(script, symbol, timeframe, limit, inputs);
|
|
952
|
-
const VALIDATE_NO_TRADING_FN = () => {
|
|
953
|
-
if (backtestKit.ExecutionContextService.hasContext()) {
|
|
954
|
-
throw new Error(functoolsKit.str.newline("Time overrides are not allowed when running scripts in a trading context.", "Please remove the 'when' parameter from the run function call."));
|
|
955
|
-
}
|
|
956
|
-
};
|
|
957
|
-
const CREATE_INFERENCE_FN = (script, symbol, timeframe, limit, inputs, exchangeName, when) => {
|
|
958
|
-
let fn = () => BASE_RUNNER_FN(script, symbol, timeframe, limit, inputs);
|
|
959
|
-
if (exchangeName) {
|
|
960
|
-
fn = ExchangeContextService.runWithContext(fn, { exchangeName });
|
|
961
|
-
}
|
|
962
|
-
if (when) {
|
|
963
|
-
fn = backtestKit.ExecutionContextService.runWithContext(fn, {
|
|
964
|
-
when,
|
|
965
|
-
symbol,
|
|
966
|
-
backtest: true,
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
return fn;
|
|
970
|
-
};
|
|
971
|
-
const RUN_INFERENCE_FN = async (script, symbol, timeframe, limit, inputs, exchangeName, when) => {
|
|
972
|
-
if (when) {
|
|
973
|
-
VALIDATE_NO_TRADING_FN();
|
|
974
|
-
}
|
|
975
|
-
const inference = CREATE_INFERENCE_FN(script, symbol, timeframe, limit, inputs, exchangeName, when);
|
|
976
|
-
return await inference();
|
|
977
|
-
};
|
|
978
999
|
async function toMarkdown(signalId, plots, mapping, limit = Number.POSITIVE_INFINITY) {
|
|
979
1000
|
pine.loggerService.log(TO_MARKDOWN_METHOD_NAME, {
|
|
980
1001
|
signalId,
|
|
@@ -992,7 +1013,7 @@ async function markdown(signalId, source, { symbol, timeframe, limit, inputs = {
|
|
|
992
1013
|
exchangeName,
|
|
993
1014
|
when,
|
|
994
1015
|
});
|
|
995
|
-
const { plots } = await
|
|
1016
|
+
const { plots } = await runInference(await getSourceCode(source), symbol, timeframe, limit, inputs, exchangeName, when);
|
|
996
1017
|
return await pine.pineMarkdownService.getReport(signalId, plots, mapping, Number.POSITIVE_INFINITY);
|
|
997
1018
|
}
|
|
998
1019
|
|