@backtest-kit/pinets 0.0.6 → 3.0.1

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,8 +1,10 @@
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.
4
6
 
5
- ![bots](https://raw.githubusercontent.com/tripolskypetr/backtest-kit/HEAD/assets/bots.png)
7
+ ![screenshot](https://raw.githubusercontent.com/tripolskypetr/backtest-kit/HEAD/assets/screenshots/screenshot8.png)
6
8
 
7
9
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/tripolskypetr/backtest-kit)
8
10
  [![npm](https://img.shields.io/npm/v/@backtest-kit/pinets.svg?style=flat-square)](https://npmjs.org/package/@backtest-kit/pinets)
@@ -29,7 +31,11 @@ Port your TradingView strategies to backtest-kit with zero rewrite. Powered by [
29
31
  | Function | Description |
30
32
  |----------|-------------|
31
33
  | **`getSignal()`** | Run Pine Script and get structured `ISignalDto` (position, TP/SL, estimated time) |
32
- | **`run()`** | Run Pine Script with custom plot mapping for advanced extraction |
34
+ | **`run()`** | Run Pine Script and return raw plot data |
35
+ | **`extract()`** | Extract values from plots with custom mapping |
36
+ | **`dumpPlotData()`** | Dump plot data to markdown files for debugging |
37
+ | **`usePine()`** | Register custom Pine constructor |
38
+ | **`setLogger()`** | Configure custom logger |
33
39
  | **`File.fromPath()`** | Load Pine Script from `.pine` file |
34
40
  | **`Code.fromString()`** | Use inline Pine Script code |
35
41
 
@@ -141,7 +147,7 @@ const plots = await run(source, {
141
147
  limit: 200,
142
148
  });
143
149
 
144
- const data = extract(plots, {
150
+ const data = await extract(plots, {
145
151
  // Simple: plot name -> number
146
152
  rsi: 'RSI',
147
153
  macd: 'MACD',
@@ -160,6 +166,51 @@ const data = extract(plots, {
160
166
  // data = { rsi: 55.2, macd: 12.5, prevRsi: 52.1, trendStrength: 'strong' }
161
167
  ```
162
168
 
169
+ ### Debug with Plot Dump
170
+
171
+ Dump plot data to markdown files for analysis and debugging:
172
+
173
+ ```typescript
174
+ import { File, run, dumpPlotData } from '@backtest-kit/pinets';
175
+
176
+ const source = File.fromPath('strategy.pine');
177
+
178
+ const plots = await run(source, {
179
+ symbol: 'BTCUSDT',
180
+ timeframe: '1h',
181
+ limit: 100,
182
+ });
183
+
184
+ // Dump plots to ./dump/ta directory
185
+ await dumpPlotData('signal-001', plots, 'ema-cross', './dump/ta');
186
+ ```
187
+
188
+ ### Custom Pine Constructor
189
+
190
+ Register a custom Pine constructor for advanced configurations:
191
+
192
+ ```typescript
193
+ import { usePine } from '@backtest-kit/pinets';
194
+ import { Pine } from 'pinets';
195
+
196
+ // Use custom Pine instance
197
+ usePine(Pine);
198
+ ```
199
+
200
+ ### Custom Logger
201
+
202
+ Configure logging for debugging:
203
+
204
+ ```typescript
205
+ import { setLogger } from '@backtest-kit/pinets';
206
+
207
+ setLogger({
208
+ log: (method, data) => console.log(`[${method}]`, data),
209
+ info: (method, data) => console.info(`[${method}]`, data),
210
+ error: (method, data) => console.error(`[${method}]`, data),
211
+ });
212
+ ```
213
+
163
214
  ## 📜 Pine Script Conventions
164
215
 
165
216
  For `getSignal()` to work, your Pine Script must include these plots:
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,
@@ -690,26 +650,32 @@ function setLogger(logger) {
690
650
  pine.loggerService.setLogger(logger);
691
651
  }
692
652
 
693
- function toSignalDto(id, data) {
653
+ function toSignalDto(id, data, priceOpen = data.priceOpen) {
694
654
  if (data.position === 1) {
695
- return {
655
+ const result = {
696
656
  id: String(id),
697
657
  position: "long",
698
- priceOpen: data.priceOpen,
699
658
  priceTakeProfit: data.priceTakeProfit,
700
659
  priceStopLoss: data.priceStopLoss,
701
660
  minuteEstimatedTime: data.minuteEstimatedTime,
702
661
  };
662
+ if (priceOpen) {
663
+ Object.assign(result, { priceOpen });
664
+ }
665
+ return result;
703
666
  }
704
667
  if (data.position === -1) {
705
- return {
668
+ const result = {
706
669
  id: String(id),
707
670
  position: "short",
708
- priceOpen: data.priceOpen,
709
671
  priceTakeProfit: data.priceTakeProfit,
710
672
  priceStopLoss: data.priceStopLoss,
711
673
  minuteEstimatedTime: data.minuteEstimatedTime,
712
674
  };
675
+ if (priceOpen) {
676
+ Object.assign(result, { priceOpen });
677
+ }
678
+ return result;
713
679
  }
714
680
  return null;
715
681
  }
@@ -750,13 +716,14 @@ async function getSignal(source, { symbol, timeframe, limit }) {
750
716
  }
751
717
 
752
718
  const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
753
- async function dumpPlotData(signalId, plots, taName, outputDir = "./dump/ta") {
719
+ async function dumpPlotData(signalId, plots, mapping, taName, outputDir = "./dump/ta") {
754
720
  pine.loggerService.log(DUMP_SIGNAL_METHOD_NAME, {
755
721
  signalId,
756
722
  plotCount: Object.keys(plots).length,
723
+ mapping,
757
724
  outputDir,
758
725
  });
759
- return await pine.pineMarkdownService.dump(signalId, plots, taName, outputDir);
726
+ return await pine.pineMarkdownService.dump(signalId, plots, mapping, taName, outputDir);
760
727
  }
761
728
 
762
729
  exports.AXIS_SYMBOL = AXIS_SYMBOL;
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,
@@ -687,26 +647,32 @@ function setLogger(logger) {
687
647
  pine.loggerService.setLogger(logger);
688
648
  }
689
649
 
690
- function toSignalDto(id, data) {
650
+ function toSignalDto(id, data, priceOpen = data.priceOpen) {
691
651
  if (data.position === 1) {
692
- return {
652
+ const result = {
693
653
  id: String(id),
694
654
  position: "long",
695
- priceOpen: data.priceOpen,
696
655
  priceTakeProfit: data.priceTakeProfit,
697
656
  priceStopLoss: data.priceStopLoss,
698
657
  minuteEstimatedTime: data.minuteEstimatedTime,
699
658
  };
659
+ if (priceOpen) {
660
+ Object.assign(result, { priceOpen });
661
+ }
662
+ return result;
700
663
  }
701
664
  if (data.position === -1) {
702
- return {
665
+ const result = {
703
666
  id: String(id),
704
667
  position: "short",
705
- priceOpen: data.priceOpen,
706
668
  priceTakeProfit: data.priceTakeProfit,
707
669
  priceStopLoss: data.priceStopLoss,
708
670
  minuteEstimatedTime: data.minuteEstimatedTime,
709
671
  };
672
+ if (priceOpen) {
673
+ Object.assign(result, { priceOpen });
674
+ }
675
+ return result;
710
676
  }
711
677
  return null;
712
678
  }
@@ -747,13 +713,14 @@ async function getSignal(source, { symbol, timeframe, limit }) {
747
713
  }
748
714
 
749
715
  const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
750
- async function dumpPlotData(signalId, plots, taName, outputDir = "./dump/ta") {
716
+ async function dumpPlotData(signalId, plots, mapping, taName, outputDir = "./dump/ta") {
751
717
  pine.loggerService.log(DUMP_SIGNAL_METHOD_NAME, {
752
718
  signalId,
753
719
  plotCount: Object.keys(plots).length,
720
+ mapping,
754
721
  outputDir,
755
722
  });
756
- return await pine.pineMarkdownService.dump(signalId, plots, taName, outputDir);
723
+ return await pine.pineMarkdownService.dump(signalId, plots, mapping, taName, outputDir);
757
724
  }
758
725
 
759
726
  export { AXIS_SYMBOL, Code, File, dumpPlotData, extract, getSignal, pine as lib, run, setLogger, toSignalDto, usePine };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/pinets",
3
- "version": "0.0.6",
3
+ "version": "3.0.1",
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",
@@ -72,8 +72,8 @@
72
72
  "worker-testbed": "1.0.12"
73
73
  },
74
74
  "peerDependencies": {
75
- "backtest-kit": "^2.2.1",
76
- "pinets": "^0.8.3",
75
+ "backtest-kit": "^3.0.5",
76
+ "pinets": "^0.8.6",
77
77
  "typescript": "^5.0.0"
78
78
  },
79
79
  "dependencies": {
package/types.d.ts CHANGED
@@ -84,12 +84,12 @@ interface IParams {
84
84
  declare function getSignal(source: File | Code, { symbol, timeframe, limit }: IParams): Promise<ISignalDto | null>;
85
85
 
86
86
  type ResultId$2 = string | number;
87
- declare function dumpPlotData(signalId: ResultId$2, plots: PlotModel, taName: string, outputDir?: string): Promise<void>;
87
+ declare function dumpPlotData<M extends PlotMapping>(signalId: ResultId$2, plots: PlotModel, mapping: M, taName: string, outputDir?: string): Promise<void>;
88
88
 
89
89
  type ResultId$1 = string | number;
90
90
  interface SignalData {
91
91
  position: number;
92
- priceOpen: number;
92
+ priceOpen?: number;
93
93
  priceTakeProfit: number;
94
94
  priceStopLoss: number;
95
95
  minuteEstimatedTime: number;
@@ -97,7 +97,7 @@ interface SignalData {
97
97
  interface Signal extends ISignalDto {
98
98
  id: string;
99
99
  }
100
- declare function toSignalDto(id: ResultId$1, data: SignalData): Signal | null;
100
+ declare function toSignalDto(id: ResultId$1, data: SignalData, priceOpen?: number | null | undefined): Signal | null;
101
101
 
102
102
  interface CandleModel {
103
103
  openTime: number;
@@ -169,9 +169,9 @@ interface IPlotRow {
169
169
  }
170
170
  declare class PineMarkdownService {
171
171
  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>;
172
+ getData: <M extends PlotMapping>(plots: PlotModel, mapping: M) => IPlotRow[];
173
+ getReport: <M extends PlotMapping>(signalId: ResultId, plots: PlotModel, mapping: M) => string;
174
+ dump: <M extends PlotMapping>(signalId: ResultId, plots: PlotModel, mapping: M, taName: string, outputDir?: string) => Promise<void>;
175
175
  }
176
176
 
177
177
  declare const pine: {