@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/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
- const CODE_TYPE_SYMBOL = Symbol("code-type");
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
- const FILE_TYPE_SYMBOL = Symbol("file-type");
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 base = symbol.replace(/USDT$|BUSD$|USD$/, "");
315
- const quote = symbol.replace(base, "");
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
- const indicator = await this.indicatorConnectionService.getInstance(code.source, inputs);
365
- const result = await runner.run(Object.keys(inputs).length ? indicator : code.source);
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
- return 0;
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
- return idx >= 0 ? (data[idx]?.value ?? 0) : 0;
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
- row[key] = (raw !== null && config.transform ? config.transform(raw) : raw);
465
+ const value = raw ?? config.defaultValue ?? null;
466
+ row[key] = (value !== null && config.transform ? config.transform(value) : value);
418
467
  }
419
468
  }
420
- const firstPlot = plots[plotNames[0]]?.data?.[i];
421
- row.timestamp = firstPlot?.time
422
- ? new Date(firstPlot.time).toISOString()
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
- const METHOD_NAME_RUN$1 = "run.run";
795
- const GET_SOURCE_FN$2 = async (source) => {
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 BASE_RUNNER_FN$1 = async (script, symbol, timeframe, limit, inputs) => await pine.pineJobService.run(script, symbol, timeframe, limit, inputs);
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
- const CREATE_INFERENCE_FN$1 = (script, symbol, timeframe, limit, inputs, exchangeName, when) => {
812
- let fn = () => BASE_RUNNER_FN$1(script, symbol, timeframe, limit, inputs);
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 GET_SOURCE_FN$2(source);
840
- const { plots } = await RUN_INFERENCE_FN$1(script, symbol, timeframe, limit, inputs, exchangeName, when);
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: "Close",
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 GET_SOURCE_FN$1(source), symbol, timeframe, limit, inputs);
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 RUN_INFERENCE_FN(await GET_SOURCE_FN(source), symbol, timeframe, limit, inputs, exchangeName, when);
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