@backtest-kit/pinets 3.0.0 → 3.0.2

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 CHANGED
@@ -1,3 +1,5 @@
1
+ <img src="https://github.com/tripolskypetr/backtest-kit/raw/refs/heads/master/assets/heraldry.svg" height="45px" align="right">
2
+
1
3
  # 📜 @backtest-kit/pinets
2
4
 
3
5
  > Run TradingView Pine Script strategies in Node.js self hosted enviroment. Execute your existing Pine Script indicators and generate trading signals - pure technical analysis with 1:1 syntax compatibility.
package/build/index.cjs CHANGED
@@ -463,120 +463,80 @@ const GET_EXECUTION_CONTEXT_FN = () => {
463
463
  when: "",
464
464
  };
465
465
  };
466
- const DEFAULT_FORMAT = (v) => v !== null ? Number(v).toFixed(4) : "N/A";
467
- function isUnsafe(value) {
468
- if (value === null)
469
- return true;
470
- if (typeof value !== "number")
471
- return true;
472
- if (isNaN(value))
473
- return true;
474
- if (!isFinite(value))
475
- return true;
476
- return false;
466
+ function getPlotName(config) {
467
+ return typeof config === "string" ? config : config.plot;
477
468
  }
478
- function extractRowAtIndex(plots, keys, index) {
479
- let time = null;
480
- for (const key of keys) {
481
- const plotData = plots[key]?.data;
482
- if (plotData && plotData[index]) {
483
- time = plotData[index].time;
484
- break;
485
- }
486
- }
487
- if (time === null)
488
- return null;
489
- const row = { time };
490
- for (const key of keys) {
491
- const plotData = plots[key]?.data;
492
- if (plotData && plotData[index]) {
493
- const value = plotData[index].value;
494
- row[key] = isUnsafe(value) ? null : value;
495
- }
496
- else {
497
- row[key] = null;
498
- }
499
- }
500
- return row;
501
- }
502
- function isRowWarmedUp(row, keys) {
503
- for (const key of keys) {
504
- if (isUnsafe(row[key])) {
505
- return false;
506
- }
507
- }
508
- return true;
469
+ function isSafe(value) {
470
+ return typeof value === "number" && !isNaN(value) && isFinite(value);
509
471
  }
472
+ const DEFAULT_FORMAT = (v) => v !== null ? Number(v).toFixed(4) : "N/A";
510
473
  function generateMarkdownTable(rows, keys, signalId) {
511
- let markdown = "";
512
474
  const { when: createdAt } = GET_EXECUTION_CONTEXT_FN();
513
- markdown += `# PineScript Technical Analysis Dump\n\n`;
475
+ let markdown = `# PineScript Technical Analysis Dump\n\n`;
514
476
  markdown += `**Signal ID**: ${String(signalId)}\n`;
515
477
  if (createdAt) {
516
478
  markdown += `**Current datetime**: ${String(createdAt)}\n`;
517
479
  }
518
480
  markdown += "\n";
519
- const header = `| Timestamp | ${keys.join(" | ")} |\n`;
520
- const separator = `| --- | ${keys.map(() => "---").join(" | ")} |\n`;
521
- markdown += header;
522
- markdown += separator;
481
+ markdown += `| ${keys.join(" | ")} | timestamp |\n`;
482
+ markdown += `| --- | ${keys.map(() => "---").join(" | ")} |\n`;
523
483
  for (const row of rows) {
524
484
  const timestamp = new Date(row.time).toISOString();
525
485
  const cells = keys.map((key) => DEFAULT_FORMAT(row[key]));
526
- markdown += `| ${timestamp} | ${cells.join(" | ")} |\n`;
486
+ markdown += `| ${cells.join(" | ")} | ${timestamp} |\n`;
527
487
  }
528
488
  return markdown;
529
489
  }
530
490
  class PineMarkdownService {
531
491
  constructor() {
532
492
  this.loggerService = inject(TYPES.loggerService);
533
- this.getData = (plots) => {
534
- this.loggerService.log("pineMarkdownService getReport", {
493
+ this.getData = (plots, mapping) => {
494
+ this.loggerService.log("pineMarkdownService getData", {
535
495
  plotCount: Object.keys(plots).length,
536
496
  });
537
- const keys = Object.keys(plots);
538
- if (keys.length === 0) {
497
+ const entries = Object.entries(mapping);
498
+ if (entries.length === 0) {
539
499
  return [];
540
500
  }
541
- const firstPlot = plots[keys[0]];
542
- const dataLength = firstPlot?.data?.length ?? 0;
501
+ const plotNames = entries.map(([key, config]) => ({ key, plotName: getPlotName(config) }));
502
+ const dataLength = Math.max(...plotNames.map(({ plotName }) => plots[plotName]?.data?.length ?? 0));
543
503
  if (dataLength === 0) {
544
504
  return [];
545
505
  }
546
506
  const rows = [];
547
- let warmupComplete = false;
548
507
  for (let i = 0; i < dataLength; i++) {
549
- const row = extractRowAtIndex(plots, keys, i);
550
- if (!row)
551
- continue;
552
- if (!warmupComplete) {
553
- if (isRowWarmedUp(row, keys)) {
554
- warmupComplete = true;
555
- }
556
- else {
557
- continue;
508
+ let time = null;
509
+ const row = { time: 0 };
510
+ for (const { key, plotName } of plotNames) {
511
+ const point = plots[plotName]?.data?.[i];
512
+ if (time === null && point) {
513
+ time = point.time;
558
514
  }
515
+ row[key] = isSafe(point?.value) ? point.value : null;
516
+ }
517
+ if (time !== null) {
518
+ row.time = time;
519
+ rows.push(row);
559
520
  }
560
- rows.push(row);
561
521
  }
562
522
  return rows.slice(-TABLE_ROWS_LIMIT);
563
523
  };
564
- this.getReport = (signalId, plots) => {
524
+ this.getReport = (signalId, plots, mapping) => {
565
525
  this.loggerService.log("pineMarkdownService getReport", {
566
526
  signalId,
567
527
  plotCount: Object.keys(plots).length,
568
528
  });
569
- const rows = this.getData(plots);
570
- const keys = Object.keys(plots);
529
+ const keys = Object.keys(mapping);
530
+ const rows = this.getData(plots, mapping);
571
531
  return generateMarkdownTable(rows, keys, signalId);
572
532
  };
573
- this.dump = async (signalId, plots, taName, outputDir = `./dump/ta/${taName}`) => {
533
+ this.dump = async (signalId, plots, mapping, taName, outputDir = `./dump/ta/${taName}`) => {
574
534
  this.loggerService.log("pineMarkdownService dumpSignal", {
575
535
  signalId,
576
536
  plotCount: Object.keys(plots).length,
577
537
  outputDir,
578
538
  });
579
- const content = this.getReport(signalId, plots);
539
+ const content = this.getReport(signalId, plots, mapping);
580
540
  const { exchangeName, frameName, strategyName } = GET_METHOD_CONTEXT_FN();
581
541
  await backtestKit.Markdown.writeData(taName, content, {
582
542
  path: outputDir,
@@ -756,13 +716,24 @@ async function getSignal(source, { symbol, timeframe, limit }) {
756
716
  }
757
717
 
758
718
  const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
759
- async function dumpPlotData(signalId, plots, taName, outputDir = "./dump/ta") {
719
+ async function dumpPlotData(signalId, plots, mapping, taName, outputDir = "./dump/ta") {
760
720
  pine.loggerService.log(DUMP_SIGNAL_METHOD_NAME, {
761
721
  signalId,
762
722
  plotCount: Object.keys(plots).length,
723
+ mapping,
763
724
  outputDir,
764
725
  });
765
- return await pine.pineMarkdownService.dump(signalId, plots, taName, outputDir);
726
+ return await pine.pineMarkdownService.dump(signalId, plots, mapping, taName, outputDir);
727
+ }
728
+
729
+ const TO_MARKDOWN_METHOD_NAME = "markdown.toMarkdown";
730
+ async function toMarkdown(signalId, plots, mapping) {
731
+ pine.loggerService.log(TO_MARKDOWN_METHOD_NAME, {
732
+ signalId,
733
+ plotCount: Object.keys(plots).length,
734
+ mapping,
735
+ });
736
+ return await pine.pineMarkdownService.getReport(signalId, plots, mapping);
766
737
  }
767
738
 
768
739
  exports.AXIS_SYMBOL = AXIS_SYMBOL;
@@ -774,5 +745,6 @@ exports.getSignal = getSignal;
774
745
  exports.lib = pine;
775
746
  exports.run = run;
776
747
  exports.setLogger = setLogger;
748
+ exports.toMarkdown = toMarkdown;
777
749
  exports.toSignalDto = toSignalDto;
778
750
  exports.usePine = usePine;
package/build/index.mjs CHANGED
@@ -460,120 +460,80 @@ const GET_EXECUTION_CONTEXT_FN = () => {
460
460
  when: "",
461
461
  };
462
462
  };
463
- const DEFAULT_FORMAT = (v) => v !== null ? Number(v).toFixed(4) : "N/A";
464
- function isUnsafe(value) {
465
- if (value === null)
466
- return true;
467
- if (typeof value !== "number")
468
- return true;
469
- if (isNaN(value))
470
- return true;
471
- if (!isFinite(value))
472
- return true;
473
- return false;
463
+ function getPlotName(config) {
464
+ return typeof config === "string" ? config : config.plot;
474
465
  }
475
- function extractRowAtIndex(plots, keys, index) {
476
- let time = null;
477
- for (const key of keys) {
478
- const plotData = plots[key]?.data;
479
- if (plotData && plotData[index]) {
480
- time = plotData[index].time;
481
- break;
482
- }
483
- }
484
- if (time === null)
485
- return null;
486
- const row = { time };
487
- for (const key of keys) {
488
- const plotData = plots[key]?.data;
489
- if (plotData && plotData[index]) {
490
- const value = plotData[index].value;
491
- row[key] = isUnsafe(value) ? null : value;
492
- }
493
- else {
494
- row[key] = null;
495
- }
496
- }
497
- return row;
498
- }
499
- function isRowWarmedUp(row, keys) {
500
- for (const key of keys) {
501
- if (isUnsafe(row[key])) {
502
- return false;
503
- }
504
- }
505
- return true;
466
+ function isSafe(value) {
467
+ return typeof value === "number" && !isNaN(value) && isFinite(value);
506
468
  }
469
+ const DEFAULT_FORMAT = (v) => v !== null ? Number(v).toFixed(4) : "N/A";
507
470
  function generateMarkdownTable(rows, keys, signalId) {
508
- let markdown = "";
509
471
  const { when: createdAt } = GET_EXECUTION_CONTEXT_FN();
510
- markdown += `# PineScript Technical Analysis Dump\n\n`;
472
+ let markdown = `# PineScript Technical Analysis Dump\n\n`;
511
473
  markdown += `**Signal ID**: ${String(signalId)}\n`;
512
474
  if (createdAt) {
513
475
  markdown += `**Current datetime**: ${String(createdAt)}\n`;
514
476
  }
515
477
  markdown += "\n";
516
- const header = `| Timestamp | ${keys.join(" | ")} |\n`;
517
- const separator = `| --- | ${keys.map(() => "---").join(" | ")} |\n`;
518
- markdown += header;
519
- markdown += separator;
478
+ markdown += `| ${keys.join(" | ")} | timestamp |\n`;
479
+ markdown += `| --- | ${keys.map(() => "---").join(" | ")} |\n`;
520
480
  for (const row of rows) {
521
481
  const timestamp = new Date(row.time).toISOString();
522
482
  const cells = keys.map((key) => DEFAULT_FORMAT(row[key]));
523
- markdown += `| ${timestamp} | ${cells.join(" | ")} |\n`;
483
+ markdown += `| ${cells.join(" | ")} | ${timestamp} |\n`;
524
484
  }
525
485
  return markdown;
526
486
  }
527
487
  class PineMarkdownService {
528
488
  constructor() {
529
489
  this.loggerService = inject(TYPES.loggerService);
530
- this.getData = (plots) => {
531
- this.loggerService.log("pineMarkdownService getReport", {
490
+ this.getData = (plots, mapping) => {
491
+ this.loggerService.log("pineMarkdownService getData", {
532
492
  plotCount: Object.keys(plots).length,
533
493
  });
534
- const keys = Object.keys(plots);
535
- if (keys.length === 0) {
494
+ const entries = Object.entries(mapping);
495
+ if (entries.length === 0) {
536
496
  return [];
537
497
  }
538
- const firstPlot = plots[keys[0]];
539
- const dataLength = firstPlot?.data?.length ?? 0;
498
+ const plotNames = entries.map(([key, config]) => ({ key, plotName: getPlotName(config) }));
499
+ const dataLength = Math.max(...plotNames.map(({ plotName }) => plots[plotName]?.data?.length ?? 0));
540
500
  if (dataLength === 0) {
541
501
  return [];
542
502
  }
543
503
  const rows = [];
544
- let warmupComplete = false;
545
504
  for (let i = 0; i < dataLength; i++) {
546
- const row = extractRowAtIndex(plots, keys, i);
547
- if (!row)
548
- continue;
549
- if (!warmupComplete) {
550
- if (isRowWarmedUp(row, keys)) {
551
- warmupComplete = true;
552
- }
553
- else {
554
- continue;
505
+ let time = null;
506
+ const row = { time: 0 };
507
+ for (const { key, plotName } of plotNames) {
508
+ const point = plots[plotName]?.data?.[i];
509
+ if (time === null && point) {
510
+ time = point.time;
555
511
  }
512
+ row[key] = isSafe(point?.value) ? point.value : null;
513
+ }
514
+ if (time !== null) {
515
+ row.time = time;
516
+ rows.push(row);
556
517
  }
557
- rows.push(row);
558
518
  }
559
519
  return rows.slice(-TABLE_ROWS_LIMIT);
560
520
  };
561
- this.getReport = (signalId, plots) => {
521
+ this.getReport = (signalId, plots, mapping) => {
562
522
  this.loggerService.log("pineMarkdownService getReport", {
563
523
  signalId,
564
524
  plotCount: Object.keys(plots).length,
565
525
  });
566
- const rows = this.getData(plots);
567
- const keys = Object.keys(plots);
526
+ const keys = Object.keys(mapping);
527
+ const rows = this.getData(plots, mapping);
568
528
  return generateMarkdownTable(rows, keys, signalId);
569
529
  };
570
- this.dump = async (signalId, plots, taName, outputDir = `./dump/ta/${taName}`) => {
530
+ this.dump = async (signalId, plots, mapping, taName, outputDir = `./dump/ta/${taName}`) => {
571
531
  this.loggerService.log("pineMarkdownService dumpSignal", {
572
532
  signalId,
573
533
  plotCount: Object.keys(plots).length,
574
534
  outputDir,
575
535
  });
576
- const content = this.getReport(signalId, plots);
536
+ const content = this.getReport(signalId, plots, mapping);
577
537
  const { exchangeName, frameName, strategyName } = GET_METHOD_CONTEXT_FN();
578
538
  await Markdown.writeData(taName, content, {
579
539
  path: outputDir,
@@ -753,13 +713,24 @@ async function getSignal(source, { symbol, timeframe, limit }) {
753
713
  }
754
714
 
755
715
  const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
756
- async function dumpPlotData(signalId, plots, taName, outputDir = "./dump/ta") {
716
+ async function dumpPlotData(signalId, plots, mapping, taName, outputDir = "./dump/ta") {
757
717
  pine.loggerService.log(DUMP_SIGNAL_METHOD_NAME, {
758
718
  signalId,
759
719
  plotCount: Object.keys(plots).length,
720
+ mapping,
760
721
  outputDir,
761
722
  });
762
- return await pine.pineMarkdownService.dump(signalId, plots, taName, outputDir);
723
+ return await pine.pineMarkdownService.dump(signalId, plots, mapping, taName, outputDir);
724
+ }
725
+
726
+ const TO_MARKDOWN_METHOD_NAME = "markdown.toMarkdown";
727
+ async function toMarkdown(signalId, plots, mapping) {
728
+ pine.loggerService.log(TO_MARKDOWN_METHOD_NAME, {
729
+ signalId,
730
+ plotCount: Object.keys(plots).length,
731
+ mapping,
732
+ });
733
+ return await pine.pineMarkdownService.getReport(signalId, plots, mapping);
763
734
  }
764
735
 
765
- export { AXIS_SYMBOL, Code, File, dumpPlotData, extract, getSignal, pine as lib, run, setLogger, toSignalDto, usePine };
736
+ export { AXIS_SYMBOL, Code, File, dumpPlotData, extract, getSignal, pine as lib, run, setLogger, toMarkdown, toSignalDto, usePine };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/pinets",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
4
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
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -83,8 +83,11 @@ interface IParams {
83
83
  }
84
84
  declare function getSignal(source: File | Code, { symbol, timeframe, limit }: IParams): Promise<ISignalDto | null>;
85
85
 
86
+ type ResultId$3 = string | number;
87
+ declare function dumpPlotData<M extends PlotMapping>(signalId: ResultId$3, plots: PlotModel, mapping: M, taName: string, outputDir?: string): Promise<void>;
88
+
86
89
  type ResultId$2 = string | number;
87
- declare function dumpPlotData(signalId: ResultId$2, plots: PlotModel, taName: string, outputDir?: string): Promise<void>;
90
+ declare function toMarkdown<M extends PlotMapping>(signalId: ResultId$2, plots: PlotModel, mapping: M): Promise<string>;
88
91
 
89
92
  type ResultId$1 = string | number;
90
93
  interface SignalData {
@@ -169,9 +172,9 @@ interface IPlotRow {
169
172
  }
170
173
  declare class PineMarkdownService {
171
174
  private readonly loggerService;
172
- getData: (plots: PlotModel) => IPlotRow[];
173
- getReport: (signalId: ResultId, plots: PlotModel) => string;
174
- dump: (signalId: ResultId, plots: PlotModel, taName: string, outputDir?: string) => Promise<void>;
175
+ getData: <M extends PlotMapping>(plots: PlotModel, mapping: M) => IPlotRow[];
176
+ getReport: <M extends PlotMapping>(signalId: ResultId, plots: PlotModel, mapping: M) => string;
177
+ dump: <M extends PlotMapping>(signalId: ResultId, plots: PlotModel, mapping: M, taName: string, outputDir?: string) => Promise<void>;
175
178
  }
176
179
 
177
180
  declare const pine: {
@@ -185,4 +188,4 @@ declare const pine: {
185
188
  loggerService: LoggerService;
186
189
  };
187
190
 
188
- export { AXIS_SYMBOL, type CandleModel, Code, File, type ILogger, type IPine, type IProvider, type PlotExtractConfig, type PlotMapping, type PlotModel, type PlotRecord, type SymbolInfoModel, type TPineCtor, dumpPlotData, extract, getSignal, pine as lib, run, setLogger, toSignalDto, usePine };
191
+ export { AXIS_SYMBOL, type CandleModel, Code, File, type ILogger, type IPine, type IProvider, type PlotExtractConfig, type PlotMapping, type PlotModel, type PlotRecord, type SymbolInfoModel, type TPineCtor, dumpPlotData, extract, getSignal, pine as lib, run, setLogger, toMarkdown, toSignalDto, usePine };