@backtest-kit/pinets 0.0.1 → 0.0.3

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,155 @@ class PineConnectionService {
438
442
  }
439
443
  }
440
444
 
445
+ const TABLE_ROWS_LIMIT = 48;
446
+ const GET_METHOD_CONTEXT_FN = () => {
447
+ if (backtestKit.MethodContextService.hasContext()) {
448
+ const { exchangeName, frameName, strategyName } = backtestKit.lib.methodContextService.context;
449
+ return { exchangeName, frameName, strategyName };
450
+ }
451
+ return {
452
+ strategyName: "",
453
+ exchangeName: "",
454
+ frameName: "",
455
+ };
456
+ };
457
+ const GET_EXECUTION_CONTEXT_FN = () => {
458
+ if (backtestKit.ExecutionContextService.hasContext()) {
459
+ const { when } = backtestKit.lib.executionContextService.context;
460
+ return { when: when.toISOString() };
461
+ }
462
+ return {
463
+ when: "",
464
+ };
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;
477
+ }
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 (!row[key]) {
505
+ return false;
506
+ }
507
+ }
508
+ return true;
509
+ }
510
+ function generateMarkdownTable(rows, keys, signalId) {
511
+ let markdown = "";
512
+ const { when: createdAt } = GET_EXECUTION_CONTEXT_FN();
513
+ markdown += `# PineScript Technical Analysis Dump\n\n`;
514
+ markdown += `**Signal ID**: ${String(signalId)}\n`;
515
+ if (createdAt) {
516
+ markdown += `**Current datetime**: ${String(createdAt)}\n`;
517
+ }
518
+ markdown += "\n";
519
+ const header = `| Timestamp | ${keys.join(" | ")} |\n`;
520
+ const separator = `| --- | ${keys.map(() => "---").join(" | ")} |\n`;
521
+ markdown += header;
522
+ markdown += separator;
523
+ for (const row of rows) {
524
+ const timestamp = new Date(row.time).toISOString();
525
+ const cells = keys.map((key) => DEFAULT_FORMAT(row[key]));
526
+ markdown += `| ${timestamp} | ${cells.join(" | ")} |\n`;
527
+ }
528
+ return markdown;
529
+ }
530
+ class PineMarkdownService {
531
+ constructor() {
532
+ this.loggerService = inject(TYPES.loggerService);
533
+ this.getData = (plots) => {
534
+ this.loggerService.log("pineMarkdownService getReport", {
535
+ plotCount: Object.keys(plots).length,
536
+ });
537
+ const keys = Object.keys(plots);
538
+ if (keys.length === 0) {
539
+ return;
540
+ }
541
+ const firstPlot = plots[keys[0]];
542
+ const dataLength = firstPlot?.data?.length ?? 0;
543
+ if (dataLength === 0) {
544
+ return;
545
+ }
546
+ const rows = [];
547
+ let warmupComplete = false;
548
+ 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;
558
+ }
559
+ }
560
+ rows.push(row);
561
+ }
562
+ return rows.slice(-TABLE_ROWS_LIMIT);
563
+ };
564
+ this.getReport = (signalId, plots) => {
565
+ this.loggerService.log("pineMarkdownService getReport", {
566
+ signalId,
567
+ plotCount: Object.keys(plots).length,
568
+ });
569
+ const rows = this.getData(plots);
570
+ const keys = Object.keys(plots);
571
+ return generateMarkdownTable(rows, keys, signalId);
572
+ };
573
+ this.dump = async (signalId, plots, taName, outputDir = `./dump/ta/${taName}`) => {
574
+ this.loggerService.log("pineMarkdownService dumpSignal", {
575
+ signalId,
576
+ plotCount: Object.keys(plots).length,
577
+ outputDir,
578
+ });
579
+ const content = this.getReport(signalId, plots);
580
+ const { exchangeName, frameName, strategyName } = GET_METHOD_CONTEXT_FN();
581
+ await backtestKit.Markdown.writeData(taName, content, {
582
+ path: outputDir,
583
+ file: `${String(signalId)}.md`,
584
+ symbol: "",
585
+ signalId: String(signalId),
586
+ strategyName,
587
+ exchangeName,
588
+ frameName,
589
+ });
590
+ };
591
+ }
592
+ }
593
+
441
594
  {
442
595
  provide(TYPES.loggerService, () => new LoggerService());
443
596
  }
@@ -457,6 +610,9 @@ class PineConnectionService {
457
610
  {
458
611
  provide(TYPES.pineConnectionService, () => new PineConnectionService());
459
612
  }
613
+ {
614
+ provide(TYPES.pineMarkdownService, () => new PineMarkdownService());
615
+ }
460
616
 
461
617
  const commonServices = {
462
618
  loggerService: inject(TYPES.loggerService),
@@ -477,6 +633,9 @@ const cacheServices = {
477
633
  const connectionServices = {
478
634
  pineConnectionService: inject(TYPES.pineConnectionService),
479
635
  };
636
+ const markdownServices = {
637
+ pineMarkdownService: inject(TYPES.pineMarkdownService),
638
+ };
480
639
  const pine = {
481
640
  ...commonServices,
482
641
  ...providerServices,
@@ -484,6 +643,7 @@ const pine = {
484
643
  ...dataServices,
485
644
  ...cacheServices,
486
645
  ...connectionServices,
646
+ ...markdownServices,
487
647
  };
488
648
  init();
489
649
 
@@ -548,6 +708,7 @@ const SIGNAL_SCHEMA = {
548
708
  function toSignalDto(data) {
549
709
  if (data.position === 1) {
550
710
  return {
711
+ id: functoolsKit.randomString(),
551
712
  position: "long",
552
713
  priceOpen: data.priceOpen,
553
714
  priceTakeProfit: data.priceTakeProfit,
@@ -557,6 +718,7 @@ function toSignalDto(data) {
557
718
  }
558
719
  if (data.position === -1) {
559
720
  return {
721
+ id: functoolsKit.randomString(),
560
722
  position: "short",
561
723
  priceOpen: data.priceOpen,
562
724
  priceTakeProfit: data.priceTakeProfit,
@@ -578,9 +740,20 @@ async function getSignal(source, { symbol, timeframe, limit }) {
578
740
  return toSignalDto(data);
579
741
  }
580
742
 
743
+ const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
744
+ async function dumpPineData(signalId, plots, taName, outputDir = "./dump/ta") {
745
+ pine.loggerService.log(DUMP_SIGNAL_METHOD_NAME, {
746
+ signalId,
747
+ plotCount: Object.keys(plots).length,
748
+ outputDir,
749
+ });
750
+ return await pine.pineMarkdownService.dump(signalId, plots, taName, outputDir);
751
+ }
752
+
581
753
  exports.AXIS_SYMBOL = AXIS_SYMBOL;
582
754
  exports.Code = Code;
583
755
  exports.File = File;
756
+ exports.dumpPineData = dumpPineData;
584
757
  exports.getSignal = getSignal;
585
758
  exports.lib = pine;
586
759
  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, MethodContextService, lib, ExecutionContextService } 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,155 @@ class PineConnectionService {
435
439
  }
436
440
  }
437
441
 
442
+ const TABLE_ROWS_LIMIT = 48;
443
+ const GET_METHOD_CONTEXT_FN = () => {
444
+ if (MethodContextService.hasContext()) {
445
+ const { exchangeName, frameName, strategyName } = lib.methodContextService.context;
446
+ return { exchangeName, frameName, strategyName };
447
+ }
448
+ return {
449
+ strategyName: "",
450
+ exchangeName: "",
451
+ frameName: "",
452
+ };
453
+ };
454
+ const GET_EXECUTION_CONTEXT_FN = () => {
455
+ if (ExecutionContextService.hasContext()) {
456
+ const { when } = lib.executionContextService.context;
457
+ return { when: when.toISOString() };
458
+ }
459
+ return {
460
+ when: "",
461
+ };
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;
474
+ }
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 (!row[key]) {
502
+ return false;
503
+ }
504
+ }
505
+ return true;
506
+ }
507
+ function generateMarkdownTable(rows, keys, signalId) {
508
+ let markdown = "";
509
+ const { when: createdAt } = GET_EXECUTION_CONTEXT_FN();
510
+ markdown += `# PineScript Technical Analysis Dump\n\n`;
511
+ markdown += `**Signal ID**: ${String(signalId)}\n`;
512
+ if (createdAt) {
513
+ markdown += `**Current datetime**: ${String(createdAt)}\n`;
514
+ }
515
+ markdown += "\n";
516
+ const header = `| Timestamp | ${keys.join(" | ")} |\n`;
517
+ const separator = `| --- | ${keys.map(() => "---").join(" | ")} |\n`;
518
+ markdown += header;
519
+ markdown += separator;
520
+ for (const row of rows) {
521
+ const timestamp = new Date(row.time).toISOString();
522
+ const cells = keys.map((key) => DEFAULT_FORMAT(row[key]));
523
+ markdown += `| ${timestamp} | ${cells.join(" | ")} |\n`;
524
+ }
525
+ return markdown;
526
+ }
527
+ class PineMarkdownService {
528
+ constructor() {
529
+ this.loggerService = inject(TYPES.loggerService);
530
+ this.getData = (plots) => {
531
+ this.loggerService.log("pineMarkdownService getReport", {
532
+ plotCount: Object.keys(plots).length,
533
+ });
534
+ const keys = Object.keys(plots);
535
+ if (keys.length === 0) {
536
+ return;
537
+ }
538
+ const firstPlot = plots[keys[0]];
539
+ const dataLength = firstPlot?.data?.length ?? 0;
540
+ if (dataLength === 0) {
541
+ return;
542
+ }
543
+ const rows = [];
544
+ let warmupComplete = false;
545
+ 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;
555
+ }
556
+ }
557
+ rows.push(row);
558
+ }
559
+ return rows.slice(-TABLE_ROWS_LIMIT);
560
+ };
561
+ this.getReport = (signalId, plots) => {
562
+ this.loggerService.log("pineMarkdownService getReport", {
563
+ signalId,
564
+ plotCount: Object.keys(plots).length,
565
+ });
566
+ const rows = this.getData(plots);
567
+ const keys = Object.keys(plots);
568
+ return generateMarkdownTable(rows, keys, signalId);
569
+ };
570
+ this.dump = async (signalId, plots, taName, outputDir = `./dump/ta/${taName}`) => {
571
+ this.loggerService.log("pineMarkdownService dumpSignal", {
572
+ signalId,
573
+ plotCount: Object.keys(plots).length,
574
+ outputDir,
575
+ });
576
+ const content = this.getReport(signalId, plots);
577
+ const { exchangeName, frameName, strategyName } = GET_METHOD_CONTEXT_FN();
578
+ await Markdown.writeData(taName, content, {
579
+ path: outputDir,
580
+ file: `${String(signalId)}.md`,
581
+ symbol: "",
582
+ signalId: String(signalId),
583
+ strategyName,
584
+ exchangeName,
585
+ frameName,
586
+ });
587
+ };
588
+ }
589
+ }
590
+
438
591
  {
439
592
  provide(TYPES.loggerService, () => new LoggerService());
440
593
  }
@@ -454,6 +607,9 @@ class PineConnectionService {
454
607
  {
455
608
  provide(TYPES.pineConnectionService, () => new PineConnectionService());
456
609
  }
610
+ {
611
+ provide(TYPES.pineMarkdownService, () => new PineMarkdownService());
612
+ }
457
613
 
458
614
  const commonServices = {
459
615
  loggerService: inject(TYPES.loggerService),
@@ -474,6 +630,9 @@ const cacheServices = {
474
630
  const connectionServices = {
475
631
  pineConnectionService: inject(TYPES.pineConnectionService),
476
632
  };
633
+ const markdownServices = {
634
+ pineMarkdownService: inject(TYPES.pineMarkdownService),
635
+ };
477
636
  const pine = {
478
637
  ...commonServices,
479
638
  ...providerServices,
@@ -481,6 +640,7 @@ const pine = {
481
640
  ...dataServices,
482
641
  ...cacheServices,
483
642
  ...connectionServices,
643
+ ...markdownServices,
484
644
  };
485
645
  init();
486
646
 
@@ -545,6 +705,7 @@ const SIGNAL_SCHEMA = {
545
705
  function toSignalDto(data) {
546
706
  if (data.position === 1) {
547
707
  return {
708
+ id: randomString(),
548
709
  position: "long",
549
710
  priceOpen: data.priceOpen,
550
711
  priceTakeProfit: data.priceTakeProfit,
@@ -554,6 +715,7 @@ function toSignalDto(data) {
554
715
  }
555
716
  if (data.position === -1) {
556
717
  return {
718
+ id: randomString(),
557
719
  position: "short",
558
720
  priceOpen: data.priceOpen,
559
721
  priceTakeProfit: data.priceTakeProfit,
@@ -575,4 +737,14 @@ async function getSignal(source, { symbol, timeframe, limit }) {
575
737
  return toSignalDto(data);
576
738
  }
577
739
 
578
- export { AXIS_SYMBOL, Code, File, getSignal, pine as lib, run, setLogger, usePine };
740
+ const DUMP_SIGNAL_METHOD_NAME = "dump.dumpSignal";
741
+ async function dumpPineData(signalId, plots, taName, outputDir = "./dump/ta") {
742
+ pine.loggerService.log(DUMP_SIGNAL_METHOD_NAME, {
743
+ signalId,
744
+ plotCount: Object.keys(plots).length,
745
+ outputDir,
746
+ });
747
+ return await pine.pineMarkdownService.dump(signalId, plots, taName, outputDir);
748
+ }
749
+
750
+ 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.3",
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 };