Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

## [0.8.1] - 2026-01-11 - Transpiler hotfix

### Fixed

- **Transpiler Math Operations**: Fixed operator precedence issue where parentheses were lost in complex arithmetic expressions (e.g., `(a + b) * c` becoming `a + b * c`).

## [0.8.0] - 2026-01-10 - Runtime Inputs & UDT Transpiler Fix

### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pinets",
"version": "0.8.0",
"version": "0.8.1",
"description": "Run Pine Script anywhere. PineTS is an open-source transpiler and runtime that brings Pine Script logic to Node.js and the browser with 1:1 syntax compatibility. Reliably write, port, and run indicators or strategies on your own infrastructure.",
"keywords": [
"Pine Script",
Expand Down
121 changes: 108 additions & 13 deletions src/transpiler/pineToJS/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -747,11 +747,18 @@ export class CodeGenerator {

// Generate BinaryExpression
generateBinaryExpression(node) {
const needsParens = this.needsParentheses(node);
const currentPrecedence = this.getPrecedence(node);

if (needsParens) this.write('(');
// Left child
const leftPrecedence = this.getPrecedence(node.left);
if (leftPrecedence < currentPrecedence) {
this.write('(');
this.generateExpression(node.left);
this.write(')');
} else {
this.generateExpression(node.left);
}

this.generateExpression(node.left);
this.write(' ');

// Convert PineScript operators to JavaScript
Expand All @@ -761,9 +768,27 @@ export class CodeGenerator {

this.write(op);
this.write(' ');
this.generateExpression(node.right);

if (needsParens) this.write(')');
// Right child
const rightPrecedence = this.getPrecedence(node.right);
let needsRightParens = rightPrecedence < currentPrecedence;

// Handle associativity for same precedence
if (rightPrecedence === currentPrecedence) {
// Subtraction, division, modulo are non-associative (left-associative)
// so we need parens for right operand
if (op === '-' || op === '/' || op === '%') {
needsRightParens = true;
}
}

if (needsRightParens) {
this.write('(');
this.generateExpression(node.right);
this.write(')');
} else {
this.generateExpression(node.right);
}
}

// Generate UnaryExpression
Expand All @@ -772,7 +797,16 @@ export class CodeGenerator {
if (op === 'not') op = '!';

this.write(op);
this.generateExpression(node.argument);

const argPrecedence = this.getPrecedence(node.argument);
// Unary is 15. If arg < 15, wrap.
if (argPrecedence < 15) {
this.write('(');
this.generateExpression(node.argument);
this.write(')');
} else {
this.generateExpression(node.argument);
}
}

// Generate AssignmentExpression
Expand Down Expand Up @@ -802,7 +836,15 @@ export class CodeGenerator {

// Generate CallExpression
generateCallExpression(node) {
this.generateExpression(node.callee);
const calleePrecedence = this.getPrecedence(node.callee);
if (calleePrecedence < 19) {
this.write('(');
this.generateExpression(node.callee);
this.write(')');
} else {
this.generateExpression(node.callee);
}

this.write('(');

for (let i = 0; i < node.arguments.length; i++) {
Expand All @@ -827,7 +869,14 @@ export class CodeGenerator {

// Generate MemberExpression
generateMemberExpression(node) {
this.generateExpression(node.object);
const objPrecedence = this.getPrecedence(node.object);
if (objPrecedence < 19) {
this.write('(');
this.generateExpression(node.object);
this.write(')');
} else {
this.generateExpression(node.object);
}

if (node.computed) {
this.write('[');
Expand Down Expand Up @@ -1288,10 +1337,56 @@ export class CodeGenerator {
this.write(')');
}

// Helper: determine if expression needs parentheses
needsParentheses(node) {
// For now, only add parens for nested binary expressions
// This could be enhanced with proper precedence checking
return false;
// Get operator precedence
getPrecedence(node) {
switch (node.type) {
case 'Literal':
case 'Identifier':
case 'ArrayExpression':
case 'ObjectExpression':
return 20;
case 'CallExpression':
case 'MemberExpression':
return 19;
case 'UnaryExpression':
case 'UpdateExpression':
return 15; // !, +, -, ++, --
case 'BinaryExpression':
case 'LogicalExpression':
switch (node.operator) {
case '*':
case '/':
case '%':
return 13;
case '+':
case '-':
return 12;
case '<':
case '<=':
case '>':
case '>=':
return 10;
case '==':
case '!=':
return 9;
case 'and': // PineScript 'and'
case '&&':
return 5;
case 'or': // PineScript 'or'
case '||':
return 4;
default:
return 0;
}
case 'ConditionalExpression':
return 3;
case 'AssignmentExpression':
case 'AssignmentPattern':
return 2;
case 'SequenceExpression':
return 1;
default:
return 0;
}
}
}
67 changes: 67 additions & 0 deletions tests/indicators/spread_ma.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';

import { PineTS, Provider } from 'index';

describe('Indicators', () => {
it('SPREAD_MA - Spread MA', async () => {
const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'W', null, new Date('2018-12-10').getTime(), new Date('2020-01-27').getTime());

const sourceCode = (context) => {
const { close, high, low, volume } = context.data;
const { ta, plotchar, math, input } = context.pine;

const length = input.int(5, 'Length');
const sma = ta.sma(close, length);
const ssma = (100 * (close - sma)) / sma;
const res = ssma;
plotchar(res, '_plot');
};

const pineCode = `
//@version=5
indicator("Spread Moving Average", overlay=false)

length = input.int(5, "Length")
sma = ta.sma(close, length)
ssma = 100 * (close - sma) / sma
plot(ssma, "_plot", color.blue, linewidth=2)
`;

const { result, plots } = await pineTS.run(pineCode);

let _plotdata = plots['_plot']?.data;
const startDate = new Date('2018-12-10').getTime();
const endDate = new Date('2019-03-16').getTime();

let plotdata_str = '';
for (let i = 0; i < _plotdata.length; i++) {
const time = _plotdata[i].time;
if (time < startDate || time > endDate) {
continue;
}

const str_time = new Date(time).toISOString().slice(0, -1) + '-00:00';
const res = _plotdata[i].value;
plotdata_str += `[${str_time}]: ${res}\n`;
}

const expected_plot = `[2018-12-10T00:00:00.000-00:00]: NaN
[2018-12-17T00:00:00.000-00:00]: NaN
[2018-12-24T00:00:00.000-00:00]: NaN
[2018-12-31T00:00:00.000-00:00]: NaN
[2019-01-07T00:00:00.000-00:00]: -5.273026266064024
[2019-01-14T00:00:00.000-00:00]: -6.258616447711691
[2019-01-21T00:00:00.000-00:00]: -4.2324871251793175
[2019-01-28T00:00:00.000-00:00]: -5.333921276613445
[2019-02-04T00:00:00.000-00:00]: 3.494395849760149
[2019-02-11T00:00:00.000-00:00]: 2.1507010977032515
[2019-02-18T00:00:00.000-00:00]: 3.6866654742382536
[2019-02-25T00:00:00.000-00:00]: 3.974265707830406
[2019-03-04T00:00:00.000-00:00]: 4.302199804859045
[2019-03-11T00:00:00.000-00:00]: 4.398461633201533`;

console.log('expected_plot', expected_plot);
console.log('plotdata_str', plotdata_str);
expect(plotdata_str.trim()).toEqual(expected_plot.trim());
});
});
24 changes: 24 additions & 0 deletions tests/transpiler/pinescript-to-js.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,30 @@ plot(sum)
expect(jsCode).toContain('/');
expect(jsCode).toContain('%');
});

it('should preserve parentheses in arithmetic precedence', () => {
const code = `
//@version=6
indicator("Precedence Test")

x = 10
y = 20
z = 30
res = (x + y) * z
res2 = 100 * (x - y) / z

plot(res)
`;

const result = transpile(code);
const jsCode = result.toString();

// (x + y) * z -> should have parens around addition
expect(jsCode).toMatch(/(\(.*\+.*\))\s*\*/);

// 100 * (x - y) / z -> should have parens around subtraction
expect(jsCode).toMatch(/100\s*\*\s*\(.*-.*\)\s*\//);
});
});

describe('Pine Script Transpilation - Series and Arrays', () => {
Expand Down