@backtest-kit/pinets 0.0.1 → 0.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/build/index.cjs CHANGED
@@ -99,6 +99,9 @@ const cacheServices$1 = {
99
99
  const connectionServices$1 = {
100
100
  pineConnectionService: Symbol("pineConnectionService"),
101
101
  };
102
+ const markdownServices$1 = {
103
+ pineMarkdownService: Symbol("pineMarkdownService"),
104
+ };
102
105
  const TYPES = {
103
106
  ...baseServices,
104
107
  ...providerServices$1,
@@ -106,6 +109,7 @@ const TYPES = {
106
109
  ...dataServices$1,
107
110
  ...cacheServices$1,
108
111
  ...connectionServices$1,
112
+ ...markdownServices$1,
109
113
  };
110
114
 
111
115
  const MS_PER_MINUTE = 60000;
@@ -438,6 +442,128 @@ class PineConnectionService {
438
442
  }
439
443
  }
440
444
 
445
+ const DEFAULT_FORMAT = (v) => v !== null ? Number(v).toFixed(4) : "N/A";
446
+ function isUnsafe(value) {
447
+ if (value === null)
448
+ return true;
449
+ if (typeof value !== "number")
450
+ return true;
451
+ if (isNaN(value))
452
+ return true;
453
+ if (!isFinite(value))
454
+ return true;
455
+ return false;
456
+ }
457
+ function extractRowAtIndex(plots, keys, index) {
458
+ let time = null;
459
+ for (const key of keys) {
460
+ const plotData = plots[key]?.data;
461
+ if (plotData && plotData[index]) {
462
+ time = plotData[index].time;
463
+ break;
464
+ }
465
+ }
466
+ if (time === null)
467
+ return null;
468
+ const row = { time };
469
+ for (const key of keys) {
470
+ const plotData = plots[key]?.data;
471
+ if (plotData && plotData[index]) {
472
+ const value = plotData[index].value;
473
+ row[key] = isUnsafe(value) ? null : value;
474
+ }
475
+ else {
476
+ row[key] = null;
477
+ }
478
+ }
479
+ return row;
480
+ }
481
+ function isRowWarmedUp(row, keys) {
482
+ for (const key of keys) {
483
+ if (!row[key]) {
484
+ return false;
485
+ }
486
+ }
487
+ return true;
488
+ }
489
+ function generateMarkdownTable(rows, keys, signalId) {
490
+ let markdown = "";
491
+ markdown += `# PineScript Technical Analysis Dump\n\n`;
492
+ markdown += `**Signal ID**: ${String(signalId)}\n\n`;
493
+ const header = `| Timestamp | ${keys.join(" | ")} |\n`;
494
+ const separator = `| --- | ${keys.map(() => "---").join(" | ")} |\n`;
495
+ markdown += header;
496
+ markdown += separator;
497
+ for (const row of rows) {
498
+ const timestamp = new Date(row.time).toISOString();
499
+ const cells = keys.map((key) => DEFAULT_FORMAT(row[key]));
500
+ markdown += `| ${timestamp} | ${cells.join(" | ")} |\n`;
501
+ }
502
+ return markdown;
503
+ }
504
+ class PineMarkdownService {
505
+ constructor() {
506
+ this.loggerService = inject(TYPES.loggerService);
507
+ this.getData = (plots) => {
508
+ this.loggerService.log("pineMarkdownService getReport", {
509
+ plotCount: Object.keys(plots).length,
510
+ });
511
+ const keys = Object.keys(plots);
512
+ if (keys.length === 0) {
513
+ return;
514
+ }
515
+ const firstPlot = plots[keys[0]];
516
+ const dataLength = firstPlot?.data?.length ?? 0;
517
+ if (dataLength === 0) {
518
+ return;
519
+ }
520
+ const rows = [];
521
+ let warmupComplete = false;
522
+ for (let i = 0; i < dataLength; i++) {
523
+ const row = extractRowAtIndex(plots, keys, i);
524
+ if (!row)
525
+ continue;
526
+ if (!warmupComplete) {
527
+ if (isRowWarmedUp(row, keys)) {
528
+ warmupComplete = true;
529
+ }
530
+ else {
531
+ continue;
532
+ }
533
+ }
534
+ rows.push(row);
535
+ }
536
+ return rows;
537
+ };
538
+ this.getReport = (signalId, plots) => {
539
+ this.loggerService.log("pineMarkdownService getReport", {
540
+ signalId,
541
+ plotCount: Object.keys(plots).length,
542
+ });
543
+ const rows = this.getData(plots);
544
+ const keys = Object.keys(plots);
545
+ return generateMarkdownTable(rows, keys, signalId);
546
+ };
547
+ this.dump = async (signalId, plots, taName, outputDir = `./dump/ta/${taName}`) => {
548
+ this.loggerService.log("pineMarkdownService dumpSignal", {
549
+ signalId,
550
+ plotCount: Object.keys(plots).length,
551
+ outputDir,
552
+ });
553
+ const content = this.getReport(signalId, plots);
554
+ await backtestKit.Markdown.writeData(taName, content, {
555
+ path: outputDir,
556
+ file: `${String(signalId)}.md`,
557
+ symbol: "",
558
+ signalId: String(signalId),
559
+ strategyName: "",
560
+ exchangeName: "",
561
+ frameName: "",
562
+ });
563
+ };
564
+ }
565
+ }
566
+
441
567
  {
442
568
  provide(TYPES.loggerService, () => new LoggerService());
443
569
  }
@@ -457,6 +583,9 @@ class PineConnectionService {
457
583
  {
458
584
  provide(TYPES.pineConnectionService, () => new PineConnectionService());
459
585
  }
586
+ {
587
+ provide(TYPES.pineMarkdownService, () => new PineMarkdownService());
588
+ }
460
589
 
461
590
  const commonServices = {
462
591
  loggerService: inject(TYPES.loggerService),
@@ -477,6 +606,9 @@ const cacheServices = {
477
606
  const connectionServices = {
478
607
  pineConnectionService: inject(TYPES.pineConnectionService),
479
608
  };
609
+ const markdownServices = {
610
+ pineMarkdownService: inject(TYPES.pineMarkdownService),
611
+ };
480
612
  const pine = {
481
613
  ...commonServices,
482
614
  ...providerServices,
@@ -484,6 +616,7 @@ const pine = {
484
616
  ...dataServices,
485
617
  ...cacheServices,
486
618
  ...connectionServices,
619
+ ...markdownServices,
487
620
  };
488
621
  init();
489
622
 
@@ -548,6 +681,7 @@ const SIGNAL_SCHEMA = {
548
681
  function toSignalDto(data) {
549
682
  if (data.position === 1) {
550
683
  return {
684
+ id: functoolsKit.randomString(),
551
685
  position: "long",
552
686
  priceOpen: data.priceOpen,
553
687
  priceTakeProfit: data.priceTakeProfit,
@@ -557,6 +691,7 @@ function toSignalDto(data) {
557
691
  }
558
692
  if (data.position === -1) {
559
693
  return {
694
+ id: functoolsKit.randomString(),
560
695
  position: "short",
561
696
  priceOpen: data.priceOpen,
562
697
  priceTakeProfit: data.priceTakeProfit,
@@ -578,9 +713,20 @@ async function getSignal(source, { symbol, timeframe, limit }) {
578
713
  return toSignalDto(data);
579
714
  }
580
715
 
716
+ const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
717
+ async function dumpPineData(signalId, plots, taName, outputDir = "./dump/ta") {
718
+ pine.loggerService.log(DUMP_SIGNAL_METHOD_NAME, {
719
+ signalId,
720
+ plotCount: Object.keys(plots).length,
721
+ outputDir,
722
+ });
723
+ return await pine.pineMarkdownService.dump(signalId, plots, taName, outputDir);
724
+ }
725
+
581
726
  exports.AXIS_SYMBOL = AXIS_SYMBOL;
582
727
  exports.Code = Code;
583
728
  exports.File = File;
729
+ exports.dumpPineData = dumpPineData;
584
730
  exports.getSignal = getSignal;
585
731
  exports.lib = pine;
586
732
  exports.run = run;
package/build/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { join } from 'path';
2
- import { getDate, getRawCandles } from 'backtest-kit';
2
+ import { getDate, getRawCandles, Markdown } from 'backtest-kit';
3
3
  import { createActivator } from 'di-kit';
4
- import { singleshot, memoize } from 'functools-kit';
4
+ import { singleshot, memoize, randomString } from 'functools-kit';
5
5
  import fs from 'fs/promises';
6
6
  import { createRequire } from 'module';
7
7
 
@@ -96,6 +96,9 @@ const cacheServices$1 = {
96
96
  const connectionServices$1 = {
97
97
  pineConnectionService: Symbol("pineConnectionService"),
98
98
  };
99
+ const markdownServices$1 = {
100
+ pineMarkdownService: Symbol("pineMarkdownService"),
101
+ };
99
102
  const TYPES = {
100
103
  ...baseServices,
101
104
  ...providerServices$1,
@@ -103,6 +106,7 @@ const TYPES = {
103
106
  ...dataServices$1,
104
107
  ...cacheServices$1,
105
108
  ...connectionServices$1,
109
+ ...markdownServices$1,
106
110
  };
107
111
 
108
112
  const MS_PER_MINUTE = 60000;
@@ -435,6 +439,128 @@ class PineConnectionService {
435
439
  }
436
440
  }
437
441
 
442
+ const DEFAULT_FORMAT = (v) => v !== null ? Number(v).toFixed(4) : "N/A";
443
+ function isUnsafe(value) {
444
+ if (value === null)
445
+ return true;
446
+ if (typeof value !== "number")
447
+ return true;
448
+ if (isNaN(value))
449
+ return true;
450
+ if (!isFinite(value))
451
+ return true;
452
+ return false;
453
+ }
454
+ function extractRowAtIndex(plots, keys, index) {
455
+ let time = null;
456
+ for (const key of keys) {
457
+ const plotData = plots[key]?.data;
458
+ if (plotData && plotData[index]) {
459
+ time = plotData[index].time;
460
+ break;
461
+ }
462
+ }
463
+ if (time === null)
464
+ return null;
465
+ const row = { time };
466
+ for (const key of keys) {
467
+ const plotData = plots[key]?.data;
468
+ if (plotData && plotData[index]) {
469
+ const value = plotData[index].value;
470
+ row[key] = isUnsafe(value) ? null : value;
471
+ }
472
+ else {
473
+ row[key] = null;
474
+ }
475
+ }
476
+ return row;
477
+ }
478
+ function isRowWarmedUp(row, keys) {
479
+ for (const key of keys) {
480
+ if (!row[key]) {
481
+ return false;
482
+ }
483
+ }
484
+ return true;
485
+ }
486
+ function generateMarkdownTable(rows, keys, signalId) {
487
+ let markdown = "";
488
+ markdown += `# PineScript Technical Analysis Dump\n\n`;
489
+ markdown += `**Signal ID**: ${String(signalId)}\n\n`;
490
+ const header = `| Timestamp | ${keys.join(" | ")} |\n`;
491
+ const separator = `| --- | ${keys.map(() => "---").join(" | ")} |\n`;
492
+ markdown += header;
493
+ markdown += separator;
494
+ for (const row of rows) {
495
+ const timestamp = new Date(row.time).toISOString();
496
+ const cells = keys.map((key) => DEFAULT_FORMAT(row[key]));
497
+ markdown += `| ${timestamp} | ${cells.join(" | ")} |\n`;
498
+ }
499
+ return markdown;
500
+ }
501
+ class PineMarkdownService {
502
+ constructor() {
503
+ this.loggerService = inject(TYPES.loggerService);
504
+ this.getData = (plots) => {
505
+ this.loggerService.log("pineMarkdownService getReport", {
506
+ plotCount: Object.keys(plots).length,
507
+ });
508
+ const keys = Object.keys(plots);
509
+ if (keys.length === 0) {
510
+ return;
511
+ }
512
+ const firstPlot = plots[keys[0]];
513
+ const dataLength = firstPlot?.data?.length ?? 0;
514
+ if (dataLength === 0) {
515
+ return;
516
+ }
517
+ const rows = [];
518
+ let warmupComplete = false;
519
+ for (let i = 0; i < dataLength; i++) {
520
+ const row = extractRowAtIndex(plots, keys, i);
521
+ if (!row)
522
+ continue;
523
+ if (!warmupComplete) {
524
+ if (isRowWarmedUp(row, keys)) {
525
+ warmupComplete = true;
526
+ }
527
+ else {
528
+ continue;
529
+ }
530
+ }
531
+ rows.push(row);
532
+ }
533
+ return rows;
534
+ };
535
+ this.getReport = (signalId, plots) => {
536
+ this.loggerService.log("pineMarkdownService getReport", {
537
+ signalId,
538
+ plotCount: Object.keys(plots).length,
539
+ });
540
+ const rows = this.getData(plots);
541
+ const keys = Object.keys(plots);
542
+ return generateMarkdownTable(rows, keys, signalId);
543
+ };
544
+ this.dump = async (signalId, plots, taName, outputDir = `./dump/ta/${taName}`) => {
545
+ this.loggerService.log("pineMarkdownService dumpSignal", {
546
+ signalId,
547
+ plotCount: Object.keys(plots).length,
548
+ outputDir,
549
+ });
550
+ const content = this.getReport(signalId, plots);
551
+ await Markdown.writeData(taName, content, {
552
+ path: outputDir,
553
+ file: `${String(signalId)}.md`,
554
+ symbol: "",
555
+ signalId: String(signalId),
556
+ strategyName: "",
557
+ exchangeName: "",
558
+ frameName: "",
559
+ });
560
+ };
561
+ }
562
+ }
563
+
438
564
  {
439
565
  provide(TYPES.loggerService, () => new LoggerService());
440
566
  }
@@ -454,6 +580,9 @@ class PineConnectionService {
454
580
  {
455
581
  provide(TYPES.pineConnectionService, () => new PineConnectionService());
456
582
  }
583
+ {
584
+ provide(TYPES.pineMarkdownService, () => new PineMarkdownService());
585
+ }
457
586
 
458
587
  const commonServices = {
459
588
  loggerService: inject(TYPES.loggerService),
@@ -474,6 +603,9 @@ const cacheServices = {
474
603
  const connectionServices = {
475
604
  pineConnectionService: inject(TYPES.pineConnectionService),
476
605
  };
606
+ const markdownServices = {
607
+ pineMarkdownService: inject(TYPES.pineMarkdownService),
608
+ };
477
609
  const pine = {
478
610
  ...commonServices,
479
611
  ...providerServices,
@@ -481,6 +613,7 @@ const pine = {
481
613
  ...dataServices,
482
614
  ...cacheServices,
483
615
  ...connectionServices,
616
+ ...markdownServices,
484
617
  };
485
618
  init();
486
619
 
@@ -545,6 +678,7 @@ const SIGNAL_SCHEMA = {
545
678
  function toSignalDto(data) {
546
679
  if (data.position === 1) {
547
680
  return {
681
+ id: randomString(),
548
682
  position: "long",
549
683
  priceOpen: data.priceOpen,
550
684
  priceTakeProfit: data.priceTakeProfit,
@@ -554,6 +688,7 @@ function toSignalDto(data) {
554
688
  }
555
689
  if (data.position === -1) {
556
690
  return {
691
+ id: randomString(),
557
692
  position: "short",
558
693
  priceOpen: data.priceOpen,
559
694
  priceTakeProfit: data.priceTakeProfit,
@@ -575,4 +710,14 @@ async function getSignal(source, { symbol, timeframe, limit }) {
575
710
  return toSignalDto(data);
576
711
  }
577
712
 
578
- export { AXIS_SYMBOL, Code, File, getSignal, pine as lib, run, setLogger, usePine };
713
+ const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
714
+ async function dumpPineData(signalId, plots, taName, outputDir = "./dump/ta") {
715
+ pine.loggerService.log(DUMP_SIGNAL_METHOD_NAME, {
716
+ signalId,
717
+ plotCount: Object.keys(plots).length,
718
+ outputDir,
719
+ });
720
+ return await pine.pineMarkdownService.dump(signalId, plots, taName, outputDir);
721
+ }
722
+
723
+ export { AXIS_SYMBOL, Code, File, dumpPineData, getSignal, pine as lib, run, setLogger, usePine };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/pinets",
3
- "version": "0.0.1",
3
+ "version": "0.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
@@ -1,4 +1,3 @@
1
- import { TPineCtor as TPineCtor$1 } from 'src/interface/Pine.interface';
2
1
  import { CandleInterval, ISignalDto } from 'backtest-kit';
3
2
 
4
3
  declare class Code {
@@ -18,8 +17,6 @@ declare class File {
18
17
  static isFile: (value: unknown) => value is File;
19
18
  }
20
19
 
21
- declare function usePine<T = TPineCtor$1>(ctor: T): void;
22
-
23
20
  type PlotData = {
24
21
  time: number;
25
22
  value: number;
@@ -32,6 +29,19 @@ type PlotRecord = {
32
29
  plots: PlotModel;
33
30
  };
34
31
 
32
+ interface IProvider {
33
+ getMarketData(tickerId: string, timeframe: string, limit?: number, sDate?: number, eDate?: number): Promise<any>;
34
+ getSymbolInfo(tickerId: string): Promise<any>;
35
+ }
36
+
37
+ type TPineCtor = (source: IProvider, tickerId: string, timeframe: string, limit: number) => IPine;
38
+ interface IPine {
39
+ ready(): Promise<void>;
40
+ run(code: string): Promise<PlotRecord>;
41
+ }
42
+
43
+ declare function usePine<T = TPineCtor>(ctor: T): void;
44
+
35
45
  type PlotExtractConfig<T = number> = {
36
46
  plot: string;
37
47
  barsBack?: number;
@@ -72,6 +82,9 @@ interface IParams {
72
82
  }
73
83
  declare function getSignal(source: File | Code, { symbol, timeframe, limit }: IParams): Promise<ISignalDto | null>;
74
84
 
85
+ type ResultId$1 = string | number;
86
+ declare function dumpPineData(signalId: ResultId$1, plots: PlotModel, taName: string, outputDir?: string): Promise<void>;
87
+
75
88
  interface CandleModel {
76
89
  openTime: number;
77
90
  open: number;
@@ -91,17 +104,6 @@ interface SymbolInfoModel {
91
104
  timezone: string;
92
105
  }
93
106
 
94
- interface IProvider {
95
- getMarketData(tickerId: string, timeframe: string, limit?: number, sDate?: number, eDate?: number): Promise<any>;
96
- getSymbolInfo(tickerId: string): Promise<any>;
97
- }
98
-
99
- type TPineCtor = (source: IProvider, tickerId: string, timeframe: string, limit: number) => IPine;
100
- interface IPine {
101
- ready(): Promise<void>;
102
- run(code: string): Promise<PlotRecord>;
103
- }
104
-
105
107
  declare const AXIS_SYMBOL = "_AXIS";
106
108
  declare class AxisProviderService implements IProvider {
107
109
  private readonly loggerService;
@@ -146,7 +148,20 @@ declare class PineCacheService {
146
148
  clear: (path?: string, baseDir?: string) => Promise<void>;
147
149
  }
148
150
 
151
+ type ResultId = string | number;
152
+ interface IPlotRow {
153
+ time: number;
154
+ [key: string]: number | null;
155
+ }
156
+ declare class PineMarkdownService {
157
+ private readonly loggerService;
158
+ getData: (plots: PlotModel) => IPlotRow[];
159
+ getReport: (signalId: ResultId, plots: PlotModel) => string;
160
+ dump: (signalId: ResultId, plots: PlotModel, taName: string, outputDir?: string) => Promise<void>;
161
+ }
162
+
149
163
  declare const pine: {
164
+ pineMarkdownService: PineMarkdownService;
150
165
  pineConnectionService: PineConnectionService;
151
166
  pineCacheService: PineCacheService;
152
167
  pineDataService: PineDataService;
@@ -156,4 +171,4 @@ declare const pine: {
156
171
  loggerService: LoggerService;
157
172
  };
158
173
 
159
- 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, getSignal, pine as lib, run, setLogger, usePine };
174
+ 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, dumpPineData, getSignal, pine as lib, run, setLogger, usePine };