PineTS v0.9.0: Drawing Objects Complete – Box, Table & Polyline Namespaces

, , ,
PineTS v0.9.0: Drawing Objects Complete – Box, Table & Polyline Namespaces

We’re excited to announce PineTS v0.9.0! This is our biggest drawing-objects release yet. After shipping line.*linefill.*, and label.* in the v0.8.x cycle, v0.9.0 completes the TradingView drawing primitives suite with full implementations of box.*table.*, and polyline.*. On top of that, 900+ test cases land in this release alongside a wave of precision fixes for TA functions, the transpiler, and request.security.


Drawing Objects: Now Complete

Box Namespace (box.*)

box.new() lets you draw rectangles on the chart defined by two corners and a time/price range – ideal for supply/demand zones, fair value gaps, or any rectangular region of interest.

The full box.* namespace is now implemented:

  • Creation & lifecyclebox.new()box.copy()box.delete()
  • Position settersset_leftset_rightset_topset_bottomset_extend
  • Style settersset_bgcolorset_border_colorset_border_widthset_border_style
  • Text settersset_textset_text_colorset_text_size
//@version=6
indicator("Supply & Demand Zones", overlay=true)

var box supplyZone = na
var box demandZone = na

// Draw a supply zone when we make a higher high
if ta.crossover(close, ta.highest(high, 20)[1])
    supplyZone := box.new(
        left    = bar_index - 10,
        top     = ta.highest(high, 10),
        right   = bar_index,
        bottom  = ta.highest(high, 10) - ta.atr(14),
        bgcolor = color.new(color.red, 80),
        border_color = color.red,
        border_width = 1,
        text    = "Supply",
        text_color = color.red,
        text_size  = size.small
    )

// Draw a demand zone on a lower low
if ta.crossunder(close, ta.lowest(low, 20)[1])
    demandZone := box.new(
        left    = bar_index - 10,
        top     = ta.lowest(low, 10) + ta.atr(14),
        right   = bar_index,
        bottom  = ta.lowest(low, 10),
        bgcolor = color.new(color.green, 80),
        border_color = color.green,
        border_width = 1,
        text    = "Demand",
        text_color = color.green,
        text_size  = size.small
    )

QFChart renders boxes as filled rectangles with configurable fill color, border color/width/style, and optional text label. 8-digit hex colors (#RRGGBBAA) are automatically converted to rgba() for canvas compatibility.


Table Namespace (table.*)

Tables let you display structured data directly on the chart – dashboards, scorecards, signal summaries, and statistics panels.

The full table.* namespace is implemented:

  • Creation & lifecycletable.new()table.delete()
  • Cell managementtable.cell()table.cell_set_text()table.cell_set_bgcolor()table.cell_set_text_color()table.cell_set_text_size()table.cell_set_border_color()table.cell_set_border_width()

Tables are positioned at fixed screen locations (position.top_leftposition.bottom_right, etc.) and rendered as DOM overlays in QFChart – just like TradingView does it.

//@version=6
indicator("Indicator Dashboard", overlay=true)

rsi  = ta.rsi(close, 14)
macd_line = ta.ema(close, 12) - ta.ema(close, 26)
atr  = ta.atr(14)

var table dashboard = table.new(
    position   = position.top_right,
    columns    = 2,
    rows       = 4,
    bgcolor    = color.new(color.black, 70),
    border_color = color.gray,
    border_width = 1
)

// Header row
table.cell(dashboard, 0, 0, "Indicator", text_color=color.white, text_size=size.small)
table.cell(dashboard, 1, 0, "Value",     text_color=color.white, text_size=size.small)

// RSI row
rsiColor = rsi > 70 ? color.red : rsi < 30 ? color.green : color.white
table.cell(dashboard, 0, 1, "RSI(14)",   text_color=color.gray,  text_size=size.small)
table.cell(dashboard, 1, 1, str.tostring(math.round(rsi, 2)),
           text_color=rsiColor, text_size=size.small)

// MACD row
macdColor = macd_line >= 0 ? color.green : color.red
table.cell(dashboard, 0, 2, "MACD",      text_color=color.gray,  text_size=size.small)
table.cell(dashboard, 1, 2, str.tostring(math.round(macd_line, 4)),
           text_color=macdColor, text_size=size.small)

// ATR row
table.cell(dashboard, 0, 3, "ATR(14)",   text_color=color.gray,  text_size=size.small)
table.cell(dashboard, 1, 3, str.tostring(math.round(atr, 2)),    text_size=size.small)

Pine Script behavior preserved: When multiple tables are placed at the same screen position, only the last one (most recently created on the final bar) is displayed – exactly matching TradingView’s “last table wins” rule.


Polyline Namespace (polyline.*)

Polylines let you draw multi-point connected paths from arrays of chart.point objects – useful for custom waveforms, price projections, Elliott Wave annotations, or any connected line through arbitrary chart points.

//@version=6
indicator("Pivot Path", overlay=true)

// Collect recent pivot highs and connect them with a polyline
var pivotPoints = array.new<chart.point>()

ph = ta.pivothigh(high, 5, 5)
if not na(ph)
    array.push(pivotPoints, chart.point.from_index(bar_index[5], ph))
    if array.size(pivotPoints) > 6
        array.shift(pivotPoints)

var polyline pvLine = na
if barstate.islast and array.size(pivotPoints) >= 2
    polyline.delete(pvLine)
    pvLine := polyline.new(
        points       = pivotPoints,
        curved       = false,
        closed       = false,
        line_color   = color.new(color.orange, 0),
        line_width   = 2
    )

polyline.new() supports straight or curved segments, optional closed shapes (connecting the last point back to the first), line color, width, and fill color for closed polygons.


Pine Script v6: enum Keyword Support

Pine Script v6 introduced enum declarations for named constant groups. PineTS v0.9.0 adds full transpiler support for enum syntax:

//@version=6
indicator("Enum Signal Labels", overlay=true)
enum Signal
    Buy
    Sell
    Neutral
getSignal(rsi) =>
    if rsi < 30
        Signal.Buy
    else if rsi > 70
        Signal.Sell
    else
        Signal.Neutral
rsi = ta.rsi(close, 14)
sig = getSignal(rsi)
if sig == Signal.Buy
    label.new(bar_index, low, "BUY", color=color.green, textcolor=color.white, style=label.style_label_up, size=size.small)
if sig == Signal.Sell
    label.new(bar_index, high, "SELL", color=color.red, textcolor=color.white, style=label.style_label_down, size=size.small)


Primitive Type Cast Expressions

PineTS now correctly parses int()float(), and string() used as explicit type cast expressions – a common pattern in typed Pine Script code:

//@version=6
indicator("Type Casts", overlay=true)

x = 3.7
i = int(x)      // Cast to integer (truncates)
f = float(i)    // Cast back to float
s = string(str.tostring(i))  // Convert to string for table/label display

plot(i, "Int Value")

TA Precision & Correctness Fixes

TA Backfill in Conditional Blocks

This was a subtle but impactful bug. If a window-based TA function (ta.smata.highestta.lowestta.stdevta.cci, etc.) is called inside a conditional block, it was only executed on bars where the condition was true – leaving its internal rolling window incomplete on skipped bars. When the condition became true again, it would calculate from an incomplete window.

v0.9.0 fixes this with source-series backfill: when the function is called after a gap, it reconstructs the window from the source series history before returning the current value.

//@version=6
indicator("Conditional TA", overlay=true)

// This now correctly calculates even though sma is only needed sometimes
useEma = input.bool(true, "Use EMA")

result = if useEma
    ta.ema(close, 20)
else
    ta.sma(close, 20)  // Window now stays consistent even when this branch is inactive

plot(result, "MA")

The fix covers: smahighestloweststdevvariancedevwmalinregccimedianrocchangealma.

TA Function-Variable Hoisting

ta.obvta.tr, and similar TA members that behave as both a value and a function call must be evaluated on every bar to maintain accurate rolling state – regardless of whether they appear inside a conditional. These are now hoisted to the top of the context function, guaranteeing they always run.

//@version=6
indicator("OBV in Condition", overlay=true)

obv = ta.obv  // Always evaluated now, even if used inside an if block
signal = ta.ema(obv, 10)

// This correctly reflects the full OBV history
if barstate.islast
    label.new(bar_index, obv, str.tostring(math.round(obv)))

math.round – Pine Script Semantics

JavaScript’s Math.round() rounds half towards positive infinity (i.e., 0.5 → 1-0.5 → 0). Pine Script’s math.round() rounds half away from zero (i.e., 0.5 → 1-0.5 → -1). PineTS now matches Pine Script’s behavior:

//@version=6
indicator("Round Test")

// Pine Script: math.round(-0.5) = -1
// JavaScript:  Math.round(-0.5) = 0  ← was incorrectly using this
plot(math.round(-1.5), "Round -1.5")  // Correctly returns -2
plot(math.round(1.5),  "Round 1.5")   // Correctly returns 2

RSI Accuracy Fix

An edge case in the RSI calculation affecting certain initial bar configurations has been corrected. Results now match TradingView’s output for all tested datasets.


request.security Fixes

syminfo.tickerId with Provider Prefix

When syminfo.tickerId contains a provider prefix (e.g., "BINANCE:BTCUSDT"), request.security now correctly strips the prefix before lookup. Previously, the full string including the colon-separated prefix was passed as the symbol, causing lookups to fail silently.

//@version=6
indicator("MTF Example")

// syminfo.tickerId returns "BINANCE:BTCUSDT" from the Binance provider
// request.security now correctly resolves this to "BTCUSDT"
weeklyClose = request.security(syminfo.tickerId, "1W", close)
plot(weeklyClose, "Weekly Close")

Tuple Returns from request.security

request.security can now correctly unwrap and return tuples from the secondary context:

//@version=6
indicator("MTF Bollinger Bands", overlay=true)

[wMid, wUpper, wLower] = request.security(
    syminfo.tickerId, "1D",
    ta.bb(close, 20, 2.0)
)

plot(wMid,   "Daily BB Mid",   color=color.blue)
plot(wUpper, "Daily BB Upper", color=color.red)
plot(wLower, "Daily BB Lower", color=color.green)
fill(plot(wUpper, display=display.none), plot(wLower, display=display.none),
     color=color.new(color.blue, 90))

Transpiler Fixes

AreaFix
Multi-level nested conditionsDeeply nested if/else if/else chains spanning multiple indentation levels now transpile correctly
IIFE double-transformationAlready-transformed IIFE nodes are no longer re-processed, preventing corrupted output
Switch/case edge casesFixed missing default cases and complex multi-line case bodies
color.* edge casesSeveral color function edge cases for correct RGBA string generation
na == na equalityCorrectly returns false – the __eq() transpilation path was not applied consistently in all conditional paths

na == na Equality

In Pine Script, na == na is false – na is not equal to itself, matching IEEE 754 NaN semantics. PineTS was already generating $.pine.math.__eq() calls for == comparisons, but there was a code path where the transformation wasn’t applied consistently, causing na == na to incorrectly return true in some cases. This is now fixed across all branches.


Type Name Compliance

Internal constant names are now fully aligned with Pine Script’s exact naming convention. This matters because PineTS feeds constants directly into QFChart renderers – any mismatch would cause a drawing object to render with incorrect style or not at all.

Constants like label styles, line styles, shape types, and size presets now match Pine Script verbatim, so indicators copy-pasted from TradingView just work without any manual constant mapping.


QFChart v0.7.0: Rendering the New Drawing Primitives

PineTS v0.9.0 ships alongside QFChart v0.7.0, which adds the renderers needed to visualize the new namespaces:

  • BoxRenderer – Renders filled rectangles with configurable fill, border, and text
  • PolylineRenderer – Renders multi-point paths with curve and fill support
  • TableOverlayRenderer – Renders tables as DOM overlays anchored to fixed screen positions

QFChart v0.7.0 also fixes render clipping for all custom drawing objects (lines, linefills, boxes, polylines) – they now correctly clip to the chart grid area instead of potentially overflowing outside the plot bounds.


Installation & Upgrade

npm install pinets@latest

Or with the CLI:

npm install -g pinets-cli
pinets run my_indicator.pine --symbol BTCUSDT --timeframe 1D

Bug Fixes Summary

  • Fixed na == na equality returning true in some conditional paths
  • Fixed TA backfill for window-based functions in conditional closures
  • Fixed ta.obv and ta.tr not being evaluated on skipped bars
  • Fixed RSI calculation edge cases
  • Fixed math.round to use “half away from zero” semantics matching Pine Script
  • Fixed request.security dropping results when syminfo.tickerId contains a provider prefix
  • Fixed request.security failing to unwrap tuple returns
  • Fixed multi-level nested if/else if/else transpilation
  • Fixed IIFE double-transformation in transpiler
  • Fixed switch/case edge cases (missing defaults, complex multi-line bodies)
  • Fixed several color.* RGBA generation edge cases
  • Fixed hline consistency (contributed by @dcaoyuan)


Get Involved

PineTS is open-source (AGPL-3.0). We welcome contributions, bug reports, and feature requests.

Leave a Reply

Your email address will not be published. Required fields are marked *