@backtest-kit/pinets 14.0.0 → 15.0.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
@@ -6,7 +6,9 @@ import { singleshot, memoize, str, randomString } from 'functools-kit';
6
6
  import fs from 'fs/promises';
7
7
  import { createRequire } from 'module';
8
8
 
9
- const CODE_TYPE_SYMBOL = Symbol("code-type");
9
+ // Symbol.for keeps the brand check working when CJS and ESM copies of the
10
+ // package end up in the same process (dual-package hazard).
11
+ const CODE_TYPE_SYMBOL = Symbol.for("backtest-kit.pinets.code-type");
10
12
  class Code {
11
13
  constructor(source) {
12
14
  this.source = source;
@@ -25,7 +27,9 @@ Code.isCode = (value) => {
25
27
  value.__type__ === CODE_TYPE_SYMBOL);
26
28
  };
27
29
 
28
- const FILE_TYPE_SYMBOL = Symbol("file-type");
30
+ // Symbol.for keeps the brand check working when CJS and ESM copies of the
31
+ // package end up in the same process (dual-package hazard).
32
+ const FILE_TYPE_SYMBOL = Symbol.for("backtest-kit.pinets.file-type");
29
33
  class File {
30
34
  constructor(path, baseDir) {
31
35
  this.path = path;
@@ -240,6 +244,22 @@ const ExchangeContextService = scoped(class {
240
244
  }
241
245
  });
242
246
 
247
+ /**
248
+ * Known quote assets for base/quote splitting in getSymbolInfo, longest first.
249
+ * Symbols with an unrecognized quote keep the whole ticker as base.
250
+ */
251
+ const QUOTE_ASSETS = [
252
+ "USDT",
253
+ "USDC",
254
+ "BUSD",
255
+ "TUSD",
256
+ "FDUSD",
257
+ "USD",
258
+ "BTC",
259
+ "ETH",
260
+ "BNB",
261
+ "EUR",
262
+ ];
243
263
  const PINE_TF_MAP = {
244
264
  "1": "1m",
245
265
  "3": "3m",
@@ -283,6 +303,11 @@ class CandleProviderService {
283
303
  .toUpperCase()
284
304
  .replace(/^BINANCE:|^BYBIT:|^OKX:/, "");
285
305
  const normalizedTimeframe = PINE_TF_MAP[timeframe] ?? timeframe;
306
+ if (!INTERVAL_MINUTES[normalizedTimeframe]) {
307
+ throw new Error(`CandleProvider getMarketData: unknown timeframe=${timeframe}. ` +
308
+ `Allowed Pine values: ${Object.keys(PINE_TF_MAP).join(", ")}; ` +
309
+ `allowed intervals: ${Object.keys(INTERVAL_MINUTES).join(", ")}`);
310
+ }
286
311
  let clampedEDate = eDate;
287
312
  if (ExecutionContextService.hasContext()) {
288
313
  const whenMs = lib.executionContextService.context.when.getTime();
@@ -308,12 +333,12 @@ class CandleProviderService {
308
333
  const symbol = tickerId
309
334
  .toUpperCase()
310
335
  .replace(/^BINANCE:|^BYBIT:|^OKX:/, "");
311
- const base = symbol.replace(/USDT$|BUSD$|USD$/, "");
312
- const quote = symbol.replace(base, "");
336
+ const quote = QUOTE_ASSETS.find((asset) => symbol.endsWith(asset) && symbol.length > asset.length) ?? "";
337
+ const base = quote ? symbol.slice(0, symbol.length - quote.length) : symbol;
313
338
  const result = {
314
339
  ticker: symbol,
315
340
  tickerid: symbol,
316
- description: `${base}/${quote}`,
341
+ description: quote ? `${base}/${quote}` : symbol,
317
342
  type: "crypto",
318
343
  basecurrency: base,
319
344
  currency: quote || "USDT",
@@ -358,8 +383,13 @@ class PineJobService {
358
383
  limit,
359
384
  });
360
385
  const runner = await CREATE_RUNNER_FN(this, tickerId, timeframe, limit);
361
- const indicator = await this.indicatorConnectionService.getInstance(code.source, inputs);
362
- const result = await runner.run(Object.keys(inputs).length ? indicator : code.source);
386
+ // The Indicator class is only required to pass custom inputs. Do not load
387
+ // it otherwise: usePine-only setups (no pinets package, no useIndicator)
388
+ // must keep working for plain script runs.
389
+ const payload = Object.keys(inputs).length
390
+ ? await this.indicatorConnectionService.getInstance(code.source, inputs)
391
+ : code.source;
392
+ const result = await runner.run(payload);
363
393
  const plots = Object.entries(result.plots).reduce((acm, [key, value]) => {
364
394
  if (key === "undefined") {
365
395
  return acm;
@@ -374,12 +404,30 @@ class PineJobService {
374
404
  }
375
405
  }
376
406
 
377
- const GET_VALUE_FN = (plots, name, barsBack = 0) => {
407
+ const GET_VALUE_FN = (plots, name, barsBack = 0, defaultValue) => {
378
408
  const data = plots[name]?.data;
379
- if (!data || data.length === 0)
380
- return 0;
409
+ if (!data || data.length === 0) {
410
+ if (defaultValue !== undefined) {
411
+ return defaultValue;
412
+ }
413
+ throw new Error(`pinets extract: plot "${name}" is missing or empty. ` +
414
+ `Available plots: ${Object.keys(plots).join(", ") || "<none>"}`);
415
+ }
381
416
  const idx = data.length - 1 - barsBack;
382
- return idx >= 0 ? (data[idx]?.value ?? 0) : 0;
417
+ if (idx < 0) {
418
+ if (defaultValue !== undefined) {
419
+ return defaultValue;
420
+ }
421
+ throw new Error(`pinets extract: plot "${name}" has only ${data.length} points, cannot read barsBack=${barsBack}`);
422
+ }
423
+ const point = data[idx];
424
+ if (point == null || point.value == null) {
425
+ if (defaultValue !== undefined) {
426
+ return defaultValue;
427
+ }
428
+ throw new Error(`pinets extract: plot "${name}" has no value at barsBack=${barsBack}`);
429
+ }
430
+ return point.value;
383
431
  };
384
432
  const GET_VALUE_AT_FN = (plots, name, i, barsBack = 0) => {
385
433
  const data = plots[name]?.data;
@@ -411,13 +459,19 @@ class PineDataService {
411
459
  }
412
460
  else {
413
461
  const raw = GET_VALUE_AT_FN(plots, config.plot, i, config.barsBack ?? 0);
414
- row[key] = (raw !== null && config.transform ? config.transform(raw) : raw);
462
+ const value = raw ?? config.defaultValue ?? null;
463
+ row[key] = (value !== null && config.transform ? config.transform(value) : value);
415
464
  }
416
465
  }
417
- const firstPlot = plots[plotNames[0]]?.data?.[i];
418
- row.timestamp = firstPlot?.time
419
- ? new Date(firstPlot.time).toISOString()
420
- : "";
466
+ let timestamp = "";
467
+ for (const name of plotNames) {
468
+ const point = plots[name]?.data?.[i];
469
+ if (point?.time) {
470
+ timestamp = new Date(point.time).toISOString();
471
+ break;
472
+ }
473
+ }
474
+ row.timestamp = timestamp;
421
475
  rows.push(row);
422
476
  }
423
477
  return rows;
@@ -434,7 +488,7 @@ class PineDataService {
434
488
  Object.assign(result, { [key]: GET_VALUE_FN(plots, config) });
435
489
  }
436
490
  else {
437
- const value = GET_VALUE_FN(plots, config.plot, config.barsBack ?? 0);
491
+ const value = GET_VALUE_FN(plots, config.plot, config.barsBack ?? 0, config.defaultValue);
438
492
  Object.assign(result, {
439
493
  [key]: config.transform ? config.transform(value) : value,
440
494
  });
@@ -592,6 +646,9 @@ class PineMarkdownService {
592
646
  if (entries.length === 0) {
593
647
  return [];
594
648
  }
649
+ if ("time" in mapping) {
650
+ throw new Error('pineMarkdownService getData: mapping key "time" is reserved for the row timestamp, rename the mapping key');
651
+ }
595
652
  const plotNames = entries.map(([key, config]) => ({ key, plotName: getPlotName(config) }));
596
653
  const dataLength = Math.max(...plotNames.map(({ plotName }) => plots[plotName]?.data?.length ?? 0));
597
654
  if (dataLength === 0) {
@@ -788,8 +845,11 @@ function useIndicator(ctor) {
788
845
  pine.indicatorConnectionService.useIndicator(ctor);
789
846
  }
790
847
 
791
- const METHOD_NAME_RUN$1 = "run.run";
792
- const GET_SOURCE_FN$2 = async (source) => {
848
+ /**
849
+ * Resolves a script source (File reference or inline Code) into Code.
850
+ * Shared by run(), markdown() and getSignal().
851
+ */
852
+ const getSourceCode = async (source) => {
793
853
  if (File.isFile(source)) {
794
854
  const code = await pine.pineCacheService.readFile(source.path, source.baseDir);
795
855
  return Code.fromString(code);
@@ -799,14 +859,21 @@ const GET_SOURCE_FN$2 = async (source) => {
799
859
  }
800
860
  throw new Error("Source must be a File or Code instance");
801
861
  };
802
- const BASE_RUNNER_FN$1 = async (script, symbol, timeframe, limit, inputs) => await pine.pineJobService.run(script, symbol, timeframe, limit, inputs);
803
- const VALIDATE_NO_TRADING_FN$1 = () => {
862
+ const VALIDATE_NO_TRADING_FN = () => {
804
863
  if (ExecutionContextService.hasContext()) {
805
864
  throw new Error(str.newline("Time overrides are not allowed when running scripts in a trading context.", "Please remove the 'when' parameter from the run function call."));
806
865
  }
807
866
  };
808
- const CREATE_INFERENCE_FN$1 = (script, symbol, timeframe, limit, inputs, exchangeName, when) => {
809
- let fn = () => BASE_RUNNER_FN$1(script, symbol, timeframe, limit, inputs);
867
+ /**
868
+ * Runs a Pine script through PineJobService, optionally scoped to an
869
+ * exchange context and/or a fixed point in time (backtest-style override).
870
+ * Shared by run() and markdown().
871
+ */
872
+ const runInference = async (script, symbol, timeframe, limit, inputs, exchangeName, when) => {
873
+ if (when) {
874
+ VALIDATE_NO_TRADING_FN();
875
+ }
876
+ let fn = () => pine.pineJobService.run(script, symbol, timeframe, limit, inputs);
810
877
  if (exchangeName) {
811
878
  fn = ExchangeContextService.runWithContext(fn, { exchangeName });
812
879
  }
@@ -817,15 +884,10 @@ const CREATE_INFERENCE_FN$1 = (script, symbol, timeframe, limit, inputs, exchang
817
884
  backtest: true,
818
885
  });
819
886
  }
820
- return fn;
821
- };
822
- const RUN_INFERENCE_FN$1 = async (script, symbol, timeframe, limit, inputs, exchangeName, when) => {
823
- if (when) {
824
- VALIDATE_NO_TRADING_FN$1();
825
- }
826
- const inference = CREATE_INFERENCE_FN$1(script, symbol, timeframe, limit, inputs, exchangeName, when);
827
- return await inference();
887
+ return await fn();
828
888
  };
889
+
890
+ const METHOD_NAME_RUN$1 = "run.run";
829
891
  async function run(source, { symbol, timeframe, limit, inputs = {} }, exchangeName, when) {
830
892
  pine.loggerService.info(METHOD_NAME_RUN$1, {
831
893
  source,
@@ -833,8 +895,8 @@ async function run(source, { symbol, timeframe, limit, inputs = {} }, exchangeNa
833
895
  timeframe,
834
896
  limit,
835
897
  });
836
- const script = await GET_SOURCE_FN$2(source);
837
- const { plots } = await RUN_INFERENCE_FN$1(script, symbol, timeframe, limit, inputs, exchangeName, when);
898
+ const script = await getSourceCode(source);
899
+ const { plots } = await runInference(script, symbol, timeframe, limit, inputs, exchangeName, when);
838
900
  return plots;
839
901
  }
840
902
 
@@ -857,6 +919,7 @@ function setLogger(logger) {
857
919
  pine.loggerService.setLogger(logger);
858
920
  }
859
921
 
922
+ const IS_VALID_PRICE_OPEN_FN = (value) => typeof value === "number" && isFinite(value) && value > 0;
860
923
  function toSignalDto(id, data, priceOpen = data.priceOpen) {
861
924
  if (data.position === 1) {
862
925
  const result = {
@@ -866,7 +929,7 @@ function toSignalDto(id, data, priceOpen = data.priceOpen) {
866
929
  priceStopLoss: data.priceStopLoss,
867
930
  minuteEstimatedTime: data.minuteEstimatedTime,
868
931
  };
869
- if (priceOpen) {
932
+ if (IS_VALID_PRICE_OPEN_FN(priceOpen)) {
870
933
  Object.assign(result, { priceOpen });
871
934
  }
872
935
  return result;
@@ -879,7 +942,7 @@ function toSignalDto(id, data, priceOpen = data.priceOpen) {
879
942
  priceStopLoss: data.priceStopLoss,
880
943
  minuteEstimatedTime: data.minuteEstimatedTime,
881
944
  };
882
- if (priceOpen) {
945
+ if (IS_VALID_PRICE_OPEN_FN(priceOpen)) {
883
946
  Object.assign(result, { priceOpen });
884
947
  }
885
948
  return result;
@@ -889,23 +952,18 @@ function toSignalDto(id, data, priceOpen = data.priceOpen) {
889
952
 
890
953
  const METHOD_NAME_RUN = "strategy.getSignal";
891
954
  const DEFAULT_ESTIMATED_TIME = 240;
892
- const GET_SOURCE_FN$1 = async (source) => {
893
- if (File.isFile(source)) {
894
- const code = await pine.pineCacheService.readFile(source.path, source.baseDir);
895
- return Code.fromString(code);
896
- }
897
- if (Code.isCode(source)) {
898
- return source;
899
- }
900
- throw new Error("Source must be a File or Code instance");
901
- };
902
955
  const SIGNAL_SCHEMA = {
903
956
  position: "Signal",
904
- priceOpen: "Close",
957
+ priceOpen: {
958
+ plot: "Close",
959
+ defaultValue: 0,
960
+ transform: (v) => v,
961
+ },
905
962
  priceTakeProfit: "TakeProfit",
906
963
  priceStopLoss: "StopLoss",
907
964
  minuteEstimatedTime: {
908
965
  plot: "EstimatedTime",
966
+ defaultValue: 0,
909
967
  transform: (v) => v || DEFAULT_ESTIMATED_TIME,
910
968
  },
911
969
  };
@@ -916,7 +974,7 @@ async function getSignal(source, { symbol, timeframe, limit, inputs = {} }) {
916
974
  timeframe,
917
975
  limit,
918
976
  });
919
- const { plots } = await pine.pineJobService.run(await GET_SOURCE_FN$1(source), symbol, timeframe, limit, inputs);
977
+ const { plots } = await pine.pineJobService.run(await getSourceCode(source), symbol, timeframe, limit, inputs);
920
978
  const resultId = randomString();
921
979
  const data = pine.pineDataService.extract(plots, SIGNAL_SCHEMA);
922
980
  return toSignalDto(resultId, data);
@@ -935,43 +993,6 @@ async function dumpPlotData(signalId, plots, mapping, taName, outputDir = `./dum
935
993
 
936
994
  const TO_MARKDOWN_METHOD_NAME = "markdown.toMarkdown";
937
995
  const MARKDOWN_METHOD_NAME = "markdown.markdown";
938
- const GET_SOURCE_FN = async (source) => {
939
- if (File.isFile(source)) {
940
- const code = await pine.pineCacheService.readFile(source.path, source.baseDir);
941
- return Code.fromString(code);
942
- }
943
- if (Code.isCode(source)) {
944
- return source;
945
- }
946
- throw new Error("Source must be a File or Code instance");
947
- };
948
- const BASE_RUNNER_FN = async (script, symbol, timeframe, limit, inputs) => await pine.pineJobService.run(script, symbol, timeframe, limit, inputs);
949
- const VALIDATE_NO_TRADING_FN = () => {
950
- if (ExecutionContextService.hasContext()) {
951
- throw new Error(str.newline("Time overrides are not allowed when running scripts in a trading context.", "Please remove the 'when' parameter from the run function call."));
952
- }
953
- };
954
- const CREATE_INFERENCE_FN = (script, symbol, timeframe, limit, inputs, exchangeName, when) => {
955
- let fn = () => BASE_RUNNER_FN(script, symbol, timeframe, limit, inputs);
956
- if (exchangeName) {
957
- fn = ExchangeContextService.runWithContext(fn, { exchangeName });
958
- }
959
- if (when) {
960
- fn = ExecutionContextService.runWithContext(fn, {
961
- when,
962
- symbol,
963
- backtest: true,
964
- });
965
- }
966
- return fn;
967
- };
968
- const RUN_INFERENCE_FN = async (script, symbol, timeframe, limit, inputs, exchangeName, when) => {
969
- if (when) {
970
- VALIDATE_NO_TRADING_FN();
971
- }
972
- const inference = CREATE_INFERENCE_FN(script, symbol, timeframe, limit, inputs, exchangeName, when);
973
- return await inference();
974
- };
975
996
  async function toMarkdown(signalId, plots, mapping, limit = Number.POSITIVE_INFINITY) {
976
997
  pine.loggerService.log(TO_MARKDOWN_METHOD_NAME, {
977
998
  signalId,
@@ -989,7 +1010,7 @@ async function markdown(signalId, source, { symbol, timeframe, limit, inputs = {
989
1010
  exchangeName,
990
1011
  when,
991
1012
  });
992
- const { plots } = await RUN_INFERENCE_FN(await GET_SOURCE_FN(source), symbol, timeframe, limit, inputs, exchangeName, when);
1013
+ const { plots } = await runInference(await getSourceCode(source), symbol, timeframe, limit, inputs, exchangeName, when);
993
1014
  return await pine.pineMarkdownService.getReport(signalId, plots, mapping, Number.POSITIVE_INFINITY);
994
1015
  }
995
1016
 
package/package.json CHANGED
@@ -1,90 +1,90 @@
1
- {
2
- "name": "@backtest-kit/pinets",
3
- "version": "14.0.0",
4
- "description": "Run TradingView Pine Script strategies in Node.js self hosted environment. Execute existing Pine Script indicators and generate trading signals with 1:1 syntax compatibility via PineTS runtime.",
5
- "author": {
6
- "name": "Petr Tripolsky",
7
- "email": "tripolskypetr@gmail.com",
8
- "url": "https://github.com/tripolskypetr"
9
- },
10
- "funding": {
11
- "type": "individual",
12
- "url": "http://paypal.me/tripolskypetr"
13
- },
14
- "license": "MIT",
15
- "homepage": "https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html",
16
- "keywords": [
17
- "pinescript",
18
- "pine-script",
19
- "tradingview",
20
- "pinets",
21
- "technical-analysis",
22
- "trading-bot",
23
- "algorithmic-trading",
24
- "backtest",
25
- "backtesting",
26
- "cryptocurrency",
27
- "forex",
28
- "strategy",
29
- "indicators",
30
- "rsi",
31
- "macd",
32
- "ema",
33
- "bollinger-bands"
34
- ],
35
- "files": [
36
- "build",
37
- "types.d.ts",
38
- "README.md"
39
- ],
40
- "repository": {
41
- "type": "git",
42
- "url": "https://github.com/tripolskypetr/backtest-kit",
43
- "documentation": "https://github.com/tripolskypetr/backtest-kit/tree/master/docs"
44
- },
45
- "bugs": {
46
- "url": "https://github.com/tripolskypetr/backtest-kit/issues"
47
- },
48
- "scripts": {
49
- "build": "rollup -c"
50
- },
51
- "main": "build/index.cjs",
52
- "module": "build/index.mjs",
53
- "source": "src/index.ts",
54
- "types": "./types.d.ts",
55
- "exports": {
56
- "require": "./build/index.cjs",
57
- "types": "./types.d.ts",
58
- "import": "./build/index.mjs",
59
- "default": "./build/index.cjs"
60
- },
61
- "devDependencies": {
62
- "@rollup/plugin-typescript": "11.1.6",
63
- "@types/node": "22.9.0",
64
- "glob": "11.0.1",
65
- "rimraf": "6.0.1",
66
- "rollup": "3.29.5",
67
- "rollup-plugin-dts": "6.1.1",
68
- "rollup-plugin-peer-deps-external": "2.2.4",
69
- "ts-morph": "27.0.2",
70
- "tslib": "2.7.0",
71
- "typedoc": "0.27.9",
72
- "pinets": "0.9.23",
73
- "backtest-kit": "14.0.0",
74
- "worker-testbed": "2.1.0"
75
- },
76
- "peerDependencies": {
77
- "backtest-kit": "^14.0.0",
78
- "pinets": "^0.9.23",
79
- "typescript": "^5.0.0"
80
- },
81
- "dependencies": {
82
- "di-kit": "^1.1.1",
83
- "di-scoped": "^1.0.21",
84
- "functools-kit": "^3.0.0",
85
- "get-moment-stamp": "^2.0.0"
86
- },
87
- "publishConfig": {
88
- "access": "public"
89
- }
90
- }
1
+ {
2
+ "name": "@backtest-kit/pinets",
3
+ "version": "15.0.0",
4
+ "description": "Run TradingView Pine Script strategies in Node.js self hosted environment. Execute existing Pine Script indicators and generate trading signals with 1:1 syntax compatibility via PineTS runtime.",
5
+ "author": {
6
+ "name": "Petr Tripolsky",
7
+ "email": "tripolskypetr@gmail.com",
8
+ "url": "https://github.com/tripolskypetr"
9
+ },
10
+ "funding": {
11
+ "type": "individual",
12
+ "url": "http://paypal.me/tripolskypetr"
13
+ },
14
+ "license": "MIT",
15
+ "homepage": "https://backtest-kit.github.io/documents/article_07_ai_news_trading_signals.html",
16
+ "keywords": [
17
+ "pinescript",
18
+ "pine-script",
19
+ "tradingview",
20
+ "pinets",
21
+ "technical-analysis",
22
+ "trading-bot",
23
+ "algorithmic-trading",
24
+ "backtest",
25
+ "backtesting",
26
+ "cryptocurrency",
27
+ "forex",
28
+ "strategy",
29
+ "indicators",
30
+ "rsi",
31
+ "macd",
32
+ "ema",
33
+ "bollinger-bands"
34
+ ],
35
+ "files": [
36
+ "build",
37
+ "types.d.ts",
38
+ "README.md"
39
+ ],
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/tripolskypetr/backtest-kit",
43
+ "documentation": "https://github.com/tripolskypetr/backtest-kit/tree/master/docs"
44
+ },
45
+ "bugs": {
46
+ "url": "https://github.com/tripolskypetr/backtest-kit/issues"
47
+ },
48
+ "scripts": {
49
+ "build": "rollup -c"
50
+ },
51
+ "main": "build/index.cjs",
52
+ "module": "build/index.mjs",
53
+ "source": "src/index.ts",
54
+ "types": "./types.d.ts",
55
+ "exports": {
56
+ "require": "./build/index.cjs",
57
+ "types": "./types.d.ts",
58
+ "import": "./build/index.mjs",
59
+ "default": "./build/index.cjs"
60
+ },
61
+ "devDependencies": {
62
+ "@rollup/plugin-typescript": "11.1.6",
63
+ "@types/node": "22.9.0",
64
+ "glob": "11.0.1",
65
+ "rimraf": "6.0.1",
66
+ "rollup": "3.29.5",
67
+ "rollup-plugin-dts": "6.1.1",
68
+ "rollup-plugin-peer-deps-external": "2.2.4",
69
+ "ts-morph": "27.0.2",
70
+ "tslib": "2.7.0",
71
+ "typedoc": "0.27.9",
72
+ "pinets": "0.9.27",
73
+ "backtest-kit": "15.0.0",
74
+ "worker-testbed": "3.0.0"
75
+ },
76
+ "peerDependencies": {
77
+ "backtest-kit": "^15.0.0",
78
+ "pinets": "^0.9.27",
79
+ "typescript": "^5.0.0"
80
+ },
81
+ "dependencies": {
82
+ "di-kit": "^1.1.1",
83
+ "di-scoped": "^1.0.21",
84
+ "functools-kit": "^4.0.0",
85
+ "get-moment-stamp": "^2.0.0"
86
+ },
87
+ "publishConfig": {
88
+ "access": "public"
89
+ }
90
+ }
package/types.d.ts CHANGED
@@ -66,6 +66,12 @@ declare function run(source: File | Code, { symbol, timeframe, limit, inputs }:
66
66
  type PlotExtractConfig<T = number> = {
67
67
  plot: string;
68
68
  barsBack?: number;
69
+ /**
70
+ * Fallback used when the plot is missing, empty or has no value at the
71
+ * requested position. Without it `extract` throws to surface plot-name
72
+ * mismatches instead of silently substituting zeros.
73
+ */
74
+ defaultValue?: number;
69
75
  transform?: (value: number) => T;
70
76
  };
71
77
  type PlotMapping = {