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
16 changes: 8 additions & 8 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.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Change Log

## [0.9.8] - 2026-03-27 - TA Cross/CrossUnder, Matrix·Vector, Plot Serialization & Input Fixes

### Fixed

- **`ta.crossover` / `ta.crossunder`**: Boundary comparison now uses inclusive `<=` / `>=` where TradingView expects equality at the crossing bar (replaces strict `<` / `>`). Verified against TradingView reference logs.
- **`matrix.mult` (vector operand)**: Multiplying a matrix by a row/column vector now returns a **`PineArrayObject`** instead of a **`PineMatrixObject`**, matching Pine Script semantics and fixing polyline-style indicators (e.g. Spline Quantile Regression).
- **`plotchar` Signature**: Corrected `PLOTCHAR_SIGNATURE` so the **`char`** argument is in the proper parameter slot for dynamic `plotchar` calls.
- **`TYPE_CHECK.color`**: Accepts **Series-wrapped** color values so colors passed through variables are not rejected and lost at runtime.
- **`color.new` / `color.rgb`**: NaN / invalid transparency no longer produces malformed hex strings (e.g. `#787b86NAN00`).
- **Bool `input`**: Fixed bool input default/coercion edge cases.
- **UDT Return from User Functions**: Fixed user-defined functions that return a UDT instance.
- **Drawing Object Serialization / `context.plot`**: Plot serialization avoids **circular references** when drawing objects are present.

---

## [0.9.7] - 2026-03-23 - Alerts, Fill & Drawing Fixes, OOB Warnings (TV-Aligned)

### 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.9.7",
"version": "0.9.8",
"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
10 changes: 5 additions & 5 deletions src/Context.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ export class Context {
public lang: any;
public length: number = 0;

/** References to drawing helpers for streaming rollback */
public _drawingHelpers: { rollbackFromBar(barIdx: number): void }[] = [];
/** References to drawing helpers for streaming rollback and plot sync */
public _drawingHelpers: { rollbackFromBar(barIdx: number): void; syncToPlot?(): void }[] = [];

// Combined namespace and core functions - the default way to access everything
public pine: {
Expand Down Expand Up @@ -444,9 +444,6 @@ export class Context {
get: () => polylineHelper.all,
});

// Register drawing helpers for streaming rollback
this._drawingHelpers = [labelHelper, lineHelper, boxHelper, linefillHelper, polylineHelper];

// table namespace
const tableHelper = new TableHelper(this);
this.bindContextObject(
Expand Down Expand Up @@ -482,6 +479,9 @@ export class Context {
get: () => tableHelper.all,
});

// Register all drawing helpers for streaming rollback and plot sync
this._drawingHelpers = [labelHelper, lineHelper, boxHelper, linefillHelper, polylineHelper, tableHelper];

// color namespace
const colorHelper = new PineColor(this);
this.bindContextObject(
Expand Down
7 changes: 7 additions & 0 deletions src/PineTS.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,13 @@ export class PineTS {
context.result.push(result);
}

// Sync drawing object plots after all mutations for this bar.
// Serializes the current state of labels/lines/boxes/etc. into plain objects
// so that context.plots contains safe, JSON-serializable data.
for (const helper of context._drawingHelpers) {
if (helper.syncToPlot) helper.syncToPlot();
}

//shift context
const shiftVariables = (container: any) => {
for (let ctxVarName of contextVarNames) {
Expand Down
24 changes: 23 additions & 1 deletion src/namespaces/Plots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ const PLOT_SIGNATURE = [
'join', 'editable', 'show_last', 'display', 'format', 'precision', 'force_overlay',
];

//prettier-ignore
const PLOTCHAR_SIGNATURE = [
'series', 'title', 'char', 'location', 'color', 'offset', 'text', 'textcolor',
'editable', 'size', 'show_last', 'display', 'format', 'precision', 'force_overlay',
];

//prettier-ignore
const PLOTCHAR_ARGS_TYPES = {
series: 'series', title: 'string', char: 'string', location: 'string',
color: 'color', offset: 'number', text: 'string', textcolor: 'color',
editable: 'boolean', size: 'string', show_last: 'number', display: 'string',
format: 'string', precision: 'number', force_overlay: 'boolean',
};

//prettier-ignore
const PLOT_SHAPE_SIGNATURE = [
'series', 'title', 'style', 'location', 'color', 'offset', 'text', 'textcolor',
Expand Down Expand Up @@ -193,7 +207,7 @@ export class PlotHelper {
//in the current implementation, plot functions are only used to collect data for the plots array and map it to the market data
plotchar(...args) {
const callsiteId = extractCallsiteId(args);
const _parsed = parseArgsForPineParams<PlotOptions>(args, PLOT_SIGNATURE, PLOT_ARGS_TYPES);
const _parsed = parseArgsForPineParams<PlotCharOptions>(args, PLOTCHAR_SIGNATURE, PLOTCHAR_ARGS_TYPES);
const { series, title, ...others } = _parsed;
const options = this.extractPlotOptions(others);
const plotKey = this._resolvePlotKey(title, callsiteId);
Expand All @@ -208,6 +222,14 @@ export class PlotHelper {
title,
time: this.context.marketData[this.context.idx].openTime,
value: value,
options: {
char: options.char,
color: options.color,
textcolor: options.textcolor,
location: options.location,
size: options.size,
offset: options.offset,
},
});
return this.context.plots[plotKey];
}
Expand Down
10 changes: 5 additions & 5 deletions src/namespaces/box/BoxHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ export class BoxHelper {
}
}

private _syncToPlot() {
public syncToPlot() {
this._ensurePlotsEntry();
const time = this.context.marketData[0]?.openTime || 0;
this.context.plots['__boxes__'].data = [{
time,
value: this._boxes,
value: this._boxes.map(bx => bx.toPlotData()),
options: { style: 'drawing_box' },
}];
}
Expand Down Expand Up @@ -117,7 +117,7 @@ export class BoxHelper {
b._helper = this;
b._createdAtBar = this.context.idx;
this._boxes.push(b);
this._syncToPlot();
this.syncToPlot();
return b;
}

Expand Down Expand Up @@ -309,7 +309,7 @@ export class BoxHelper {
b._helper = this;
b._createdAtBar = this.context.idx;
this._boxes.push(b);
this._syncToPlot();
this.syncToPlot();
return b;
}

Expand All @@ -328,6 +328,6 @@ export class BoxHelper {
*/
rollbackFromBar(barIdx: number): void {
this._boxes = this._boxes.filter((b) => b._createdAtBar < barIdx);
this._syncToPlot();
this.syncToPlot();
}
}
14 changes: 14 additions & 0 deletions src/namespaces/box/BoxObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ export class BoxObject {
this._deleted = true;
}

toPlotData(): any {
return {
id: this.id,
left: this.left, top: this.top, right: this.right, bottom: this.bottom,
xloc: this.xloc, extend: this.extend,
border_color: this.border_color, border_style: this.border_style, border_width: this.border_width,
bgcolor: this.bgcolor,
text: this.text, text_color: this.text_color, text_size: this.text_size,
text_halign: this.text_halign, text_valign: this.text_valign,
text_wrap: this.text_wrap, text_font_family: this.text_font_family, text_formatting: this.text_formatting,
force_overlay: this.force_overlay, _deleted: this._deleted,
};
}

copy(): BoxObject {
const b = new BoxObject(
this.left, this.top, this.right, this.bottom,
Expand Down
5 changes: 5 additions & 0 deletions src/namespaces/color/PineColor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ export class PineColor {
// If not a string (e.g. NaN for na), return as-is
if (!color || typeof color !== 'string') return color;

// Treat NaN transparency as "no transparency specified" (keep original color)
if (typeof a === 'number' && isNaN(a)) a = undefined;

// Handle hexadecimal colors
if (color.startsWith('#')) {
const hex = color.slice(1);
Expand Down Expand Up @@ -164,6 +167,8 @@ export class PineColor {

// ── color.rgb(r, g, b, a?) ────────────────────────────────────────
rgb(r: number, g: number, b: number, a?: number) {
// Treat NaN transparency as "no transparency" (fully opaque)
if (typeof a === 'number' && isNaN(a)) a = undefined;
return a != null ? `rgba(${r}, ${g}, ${b}, ${(100 - a) / 100})` : `rgb(${r}, ${g}, ${b})`;
}

Expand Down
4 changes: 2 additions & 2 deletions src/namespaces/input/methods/bool.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-only

import { parseInputOptions } from '../utils';
import { parseInputOptions, resolveInput } from '../utils';

export function bool(context: any) {
return (...args: any[]) => {
const options = parseInputOptions(args);
return options.defval;
return resolveInput(context, options);
};
}
10 changes: 5 additions & 5 deletions src/namespaces/label/LabelHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class LabelHelper {
}
}

private _syncToPlot() {
public syncToPlot() {
this._ensurePlotsEntry();
// Store ALL labels as a single array value at the first bar's time.
// Using a live reference so setter mutations are reflected automatically.
Expand All @@ -51,7 +51,7 @@ export class LabelHelper {
const time = this.context.marketData[0]?.openTime || 0;
this.context.plots['__labels__'].data = [{
time,
value: this._labels,
value: this._labels.map(lbl => lbl.toPlotData()),
options: { style: 'label' },
}];
}
Expand Down Expand Up @@ -115,7 +115,7 @@ export class LabelHelper {
lbl._helper = this;
lbl._createdAtBar = this.context.idx;
this._labels.push(lbl);
this._syncToPlot();
this.syncToPlot();
return lbl;
}

Expand Down Expand Up @@ -248,7 +248,7 @@ export class LabelHelper {
lbl._helper = this;
lbl._createdAtBar = this.context.idx;
this._labels.push(lbl);
this._syncToPlot();
this.syncToPlot();
return lbl;
}

Expand All @@ -268,7 +268,7 @@ export class LabelHelper {
*/
rollbackFromBar(barIdx: number): void {
this._labels = this._labels.filter((l) => l._createdAtBar < barIdx);
this._syncToPlot();
this.syncToPlot();
}

// --- Style constants ---
Expand Down
1 change: 1 addition & 0 deletions src/namespaces/label/LabelObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export class LabelObject {
tooltip: this.tooltip,
text_font_family: this.text_font_family,
force_overlay: this.force_overlay,
_deleted: this._deleted,
};
}
}
10 changes: 5 additions & 5 deletions src/namespaces/line/LineHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class LineHelper {
}
}

private _syncToPlot() {
public syncToPlot() {
this._ensurePlotsEntry();
// Store ALL lines as a single array value at the first bar's time.
// Using a live reference so setter mutations are reflected automatically.
Expand All @@ -48,7 +48,7 @@ export class LineHelper {
const time = this.context.marketData[0]?.openTime || 0;
this.context.plots['__lines__'].data = [{
time,
value: this._lines,
value: this._lines.map(ln => ln.toPlotData()),
options: { style: 'drawing_line' },
}];
}
Expand Down Expand Up @@ -107,7 +107,7 @@ export class LineHelper {
ln._helper = this;
ln._createdAtBar = this.context.idx;
this._lines.push(ln);
this._syncToPlot();
this.syncToPlot();
return ln;
}

Expand Down Expand Up @@ -268,7 +268,7 @@ export class LineHelper {
ln._helper = this;
ln._createdAtBar = this.context.idx;
this._lines.push(ln);
this._syncToPlot();
this.syncToPlot();
return ln;
}

Expand All @@ -288,7 +288,7 @@ export class LineHelper {
*/
rollbackFromBar(barIdx: number): void {
this._lines = this._lines.filter((l) => l._createdAtBar < barIdx);
this._syncToPlot();
this.syncToPlot();
}

// --- Style constants ---
Expand Down
1 change: 1 addition & 0 deletions src/namespaces/line/LineObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export class LineObject {
style: this.style,
width: this.width,
force_overlay: this.force_overlay,
_deleted: this._deleted,
};
}
}
8 changes: 4 additions & 4 deletions src/namespaces/linefill/LinefillHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ export class LinefillHelper {
}
}

private _syncToPlot() {
public syncToPlot() {
this._ensurePlotsEntry();
// Store ALL linefills as a single array value at the first bar's time.
// Same aggregation pattern as lines and labels.
const time = this.context.marketData[0]?.openTime || 0;
this.context.plots['__linefills__'].data = [{
time,
value: this._linefills,
value: this._linefills.map(lf => lf.toPlotData()),
options: { style: 'linefill' },
}];
}
Expand Down Expand Up @@ -68,7 +68,7 @@ export class LinefillHelper {
const lf = new LinefillObject(resolvedLine1, resolvedLine2, resolvedColor);
lf._createdAtBar = this.context.idx;
this._linefills.push(lf);
this._syncToPlot();
this.syncToPlot();
return lf;
}

Expand Down Expand Up @@ -110,6 +110,6 @@ export class LinefillHelper {
*/
rollbackFromBar(barIdx: number): void {
this._linefills = this._linefills.filter((lf) => lf._createdAtBar < barIdx);
this._syncToPlot();
this.syncToPlot();
}
}
Loading
Loading