QuantForge https://quantforge.org Open Source Pine Script™ Runtime & Financial Charts Wed, 18 Mar 2026 22:54:03 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://quantforge.org/wp-content/uploads/2026/01/cropped-favicon-64-1-32x32.png QuantForge https://quantforge.org 32 32 QFChart v0.8: Harmonic Patterns, Fibonacci tools, Extensible Plugin System & Interactive Drawing Tools https://quantforge.org/qfchart-v0-8-harmonic-patterns-fibonacci-tools-extensible-plugin-system-interactive-drawing-tools/ https://quantforge.org/qfchart-v0-8-harmonic-patterns-fibonacci-tools-extensible-plugin-system-interactive-drawing-tools/#respond Wed, 18 Mar 2026 22:54:02 +0000 https://quantforge.org/?p=172 QFChart v0.8 spans six releases –v0.7.0 through v0.8.1 -and marks a significant shift in the library’s direction. While v0.6 established the foundation for drawing objects and plot styles, v0.8 transforms QFChart from a charting library into an interactive technical analysis platform. This release adds a complete suite of drawing tools (Fibonacci analysis, harmonic patterns, classical patterns), an extensible plugin architecture that supports third-party tools, and a long list of rendering improvements and bug fixes.


What’s Covered in v0.8

VersionHighlights
v0.7.0Box, polyline, and table drawing objects
v0.7.1Rendering hotfixes (plot color na handling, drawing tools clipping)
v0.7.2Gradient fill support
v0.7.3Resizable indicator panes, histogram rewrite
v0.8.0Canvas table renderer, lazy viewport expansion, rendering overhaul
v0.8.1Plugin system refactor, chart patterns, Fibonacci tools, snap-to-candle

Chart Pattern Drawing Tools

The most visible addition in v0.8 is a full suite of chart pattern drawing tools -the kind of tools you’d find in TradingView’s toolbar. Each tool is a multi-click interaction where you place points on the chart to define a pattern, and the tool draws the connecting structure with labels, Fibonacci ratio annotations, and shaded zones.

Harmonic Patterns

Harmonic patterns are price structures where each leg has a specific Fibonacci ratio relationship to the others. Traders use them to identify high-probability reversal zones. When the ratios align, the D point becomes a potential entry with a well-defined risk/reward setup.

ToolPointsDescription
XABCD Pattern5 clicksThe classic harmonic pattern (Gartley, Butterfly, Bat, Crab). Draws a zigzag X→A→B→C→D with filled XAB/BCD triangles and Fibonacci ratios on each leg (AB/XA, BC/AB, CD/BC, AD/XA).
ABCD Pattern4 clicksA simpler four-point harmonic. Draws A→B→C→D with BC/AB and CD/BC ratio annotations.
Cypher Pattern5 clicksFive-point Cypher with XC/XA and CD/XC ratio labels and a distinct color scheme.
Three Drives7 clicksThree progressively extending impulse moves with correction ratios (D2/D1, D3/D2) and drive-to-drive connector lines.

For example, to identify a Gartley pattern on a BTC/USDT daily chart, you would look for a swing where AB retraces 61.8% of XA, BC retraces 38.2-88.6% of AB, and CD extends to 127.2-161.8% of BC. The XABCD tool draws all five points, computes these ratios automatically, and displays them on the chart so you can quickly verify whether the structure qualifies as a valid Gartley, Butterfly, Bat, or Crab.

The ABCD pattern is useful as a standalone trade setup or as a building block for spotting larger harmonic structures. It appears frequently on all timeframes and assets. A common application is identifying potential continuation moves after a pullback: if BC retraces ~61.8% of AB, the CD leg often completes near the 127.2% extension of BC.

The Three Drives pattern helps identify trend exhaustion. Each successive drive extends further than the previous one (typically at 1.272 or 1.618 ratios), signaling that momentum is fading and a reversal may follow.

Classical Patterns

Classical chart patterns are visual structures that form during price consolidation or reversal phases. Unlike harmonic patterns which rely on precise Fibonacci ratios, classical patterns are based on the geometry of the price action itself.

ToolPointsDescription
Head & Shoulders7 clicksFull H&S structure with labeled vertices (LS, H, RS), filled shoulder and head regions, and an extended neckline.
Triangle5 clicksAlternating high/low points with converging upper and lower trendlines (extended with dashed projections), plus a filled interior.

The Head & Shoulders tool is one of the most widely used reversal pattern identifiers in technical analysis. On a weekly BTC chart, for instance, you might spot a left shoulder forming at $60K, a head at $69K, and a right shoulder at $62K, with a neckline connecting the two troughs around $55K. The tool draws the full structure, fills the shoulder and head regions for visual clarity, and extends the neckline across the chart. The neckline break is the key signal: once price drops below it, the measured move target equals the distance from the head to the neckline, projected downward from the break point.

The Triangle tool is useful for identifying consolidation patterns where price makes lower highs and higher lows (symmetrical triangle), lower highs against a flat support (descending), or higher lows against a flat resistance (ascending). The tool connects alternating swing points and draws extended trendlines to project where the apex (convergence point) lies. Breakout traders watch for price to exit the triangle on expanding volume.

All patterns support selection, drag-to-move, and individual point editing. You can drag any control point to adjust the pattern after placement.


Fibonacci Drawing Tools

Alongside the patterns, four new Fibonacci analysis tools round out the drawing toolkit. Together with the existing Fibonacci Retracement (from v0.5), QFChart now covers the five most commonly used Fibonacci tools in technical analysis.

Fibonacci Channel (3 clicks)

Click 1-2 define the baseline (typically connecting two swing lows or two swing highs), click 3 sets the channel width by clicking the opposite extreme (e.g., the swing high between the two lows). The tool draws parallel lines at Fibonacci ratios (0, 0.236, 0.382, 0.5, 0.618, 0.786, 1) offset perpendicular to the baseline. These are truly parallel, not just vertically shifted, so they follow the slope of the trend. Shaded zones fill the space between adjacent levels.

Fibonacci channels are commonly used for trending markets where price moves within a channel. In an uptrend, the baseline connects two swing lows and the 1.0 level passes through the swing high. Fibonacci levels between 0 and 1 act as potential support during pullbacks within the trend. If price breaks above 1.0, the 1.618 and 2.618 extensions (if displayed) become potential targets.

Fibonacci Trend-Based Extension (3 clicks)

Click 1-2 define the initial trend move (e.g., a rally from $40K to $60K), click 3 defines the retracement point (e.g., pullback to $50K). The tool projects extension levels beyond the initial move, including levels above 1.0 (1.272, 1.618, 2.0, 2.618), showing where price might reach if the trend resumes from the pullback. Horizontal lines span the chart width at each projected level with price labels.

This is one of the most practical tools for setting profit targets. For example, if BTC rallied $20K and then pulled back, the 1.0 extension from the pullback gives a target of the pullback price + $20K, the 1.618 extension gives pullback + $32.36K, and so on. Swing traders commonly take partial profits at the 1.0 and 1.272 levels, with the 1.618 level as an ambitious target.

Fibonacci Speed Resistance Fan (2 clicks)

Two clicks define the price/time rectangle (e.g., a significant low to a significant high). The tool draws diagonal fan rays from the start point at Fibonacci ratios of both the price axis and time axis, creating a spread of rays that radiate from the origin. Zones between rays are filled with semi-transparent color.

Speed resistance fans combine both time and price analysis. The price rays show what fraction of the total price move has been retraced, while the time rays show the fraction of total time elapsed. When price interacts with both a time ray and a price ray at the same point, it creates a stronger support or resistance zone. This tool is especially useful on higher timeframes (daily, weekly) for identifying long-term trend structure.

Existing Tools

The Measure Tool, Trend Line, and Fibonacci Retracement from v0.5 continue to work as before.


Snap to Candle (Ctrl / Cmd)

All drawing tools now support magnetic snapping. Hold Ctrl (or Cmd on Mac) while clicking or moving the cursor, and the point snaps to the nearest candle’s closest OHLC value (open, high, low, or close).

A small blue indicator circle appears at the snapped position, giving visual feedback before you click. This works during the entire drawing interaction -from the first point placement through the last.

The snap system is built into the AbstractPlugin base class, so it works for every built-in tool and any custom plugin that extends it.


Tool Groups

With 12 drawing tools available, the toolbar needs organization. The new ToolGroup class wraps multiple related plugins under a single toolbar button with a dropdown chevron:

const fibGroup = new QFChart.ToolGroup({
  name: 'Fibonacci Tools',
  icon: `<svg>...</svg>`,
});
fibGroup.add(new QFChart.FibonacciTool());
fibGroup.add(new QFChart.FibonacciChannelTool());
fibGroup.add(new QFChart.FibTrendExtensionTool());
fibGroup.add(new QFChart.FibSpeedResistanceFanTool());
chart.registerPlugin(fibGroup);

const patternGroup = new QFChart.ToolGroup({
  name: 'Patterns',
  icon: `<svg>...</svg>`,
});
patternGroup.add(new QFChart.XABCDPatternTool());
patternGroup.add(new QFChart.ABCDPatternTool());
patternGroup.add(new QFChart.CypherPatternTool());
patternGroup.add(new QFChart.HeadAndShouldersTool());
patternGroup.add(new QFChart.TrianglePatternTool());
patternGroup.add(new QFChart.ThreeDrivesPatternTool());
chart.registerPlugin(patternGroup);

Clicking the group button opens a dropdown menu. When a tool is selected, the group’s icon updates to show the active sub-tool.


Extensible Plugin Architecture

The biggest architectural change in v0.8 is the plugin system refactor. Previously, adding a new drawing type required modifying QFChart’s core rendering code. Now, each drawing tool is fully self-contained.

How it works

Each plugin lives in its own folder with two files:

  1. Tool -Handles user interaction (clicks, preview graphics, state machine)
  2. DrawingRenderer -Handles permanent chart rendering (ECharts graphic elements)

The plugin registers its renderer during initialization:

protected onInit(): void {
  this.context.registerDrawingRenderer(new MyDrawingRenderer());
}

Zero changes to QFChart.ts are needed to add a new drawing type. This means:

  • Third-party developers can create custom drawing tools as npm packages
  • The chart core has no knowledge of any specific drawing type
  • Adding or removing tools doesn’t affect the bundle size of unused tools

The DrawingType was widened from a fixed union ('line' | 'fibonacci') to an open string, so any plugin can define any type name.


Resizable Indicator Panes

Indicator pane borders are now interactively draggable. Hover over the boundary between the main chart and an indicator pane (or between two indicator panes) and the cursor changes to row-resize. Drag to redistribute height between neighboring panes in real time.

Minimum heights are enforced (10% for the main pane, 5% per indicator) to prevent accidentally collapsing a pane. Heights persist across re-renders.


Auto-Expanding Viewport

Previously, the chart allocated a fixed percentage of empty bars on each side for scrolling room. In v0.8, the chart starts with a minimal 5-bar buffer and automatically expands when the user scrolls near an edge.

When you scroll within 10 bars of either edge, the chart adds 50 bars of padding per side (up to a hard cap of 500 bars). The viewport position is preserved across expansions -there’s no visual jump. This means you can freely pan left into deep history or right past the last bar without hitting a wall.


Box, Polyline & Table Drawing Objects (v0.7.0)

Three new drawing object types were added, expanding the range of Pine Script features QFChart can render:

Box Renderer

Renders Pine Script box.* objects -filled rectangles with configurable border color/width/style, optional text, and extend modes. Supports 8-digit hex colors (#RRGGBBAA).

Polyline Renderer

Renders Pine Script polyline.* objects -multi-point connected paths from chart.point arrays, with straight or curved segments, optional closed shapes, and fill color.

Table Renderer (Canvas-Based)

Tables went through two iterations. v0.7.0 introduced TableOverlayRenderer (HTML-based DOM overlays). v0.8.0 completely rewrote it as TableCanvasRenderer -all table cells are now ECharts canvas graphics, which means:

  • Pixel-perfect sizing (Pine Script % maps directly to px)
  • Tables participate in the single render pipeline (exports, animations, resize)
  • Better performance for large tables
  • Correct z-ordering with other chart elements

Gradient Fills (v0.7.2)

Fill plots now support Pine Script’s gradient fill variant. When plotOptions.gradient === true, each polygon segment renders with a vertical linear gradient mapping top_color to the higher-value boundary and bottom_color to the lower-value boundary.


Rendering Improvements

Across v0.7 and v0.8, dozens of rendering bugs were fixed. The most impactful:

  • Custom candle colors -Fixed ECharts coercing string colors to NaN in custom series data
  • Fill rendering -Fixed multi-color fills, streaming reference data loss, and gradient support
  • Drawing object Y-axis decoupling -Drawing objects no longer force the Y-axis to encompass their coordinates, so the axis adapts naturally when scrolling through history
  • Render clipping -All custom series (lines, linefills, boxes, polylines) now properly clip to their grid area
  • Histogram rewrite -Histograms now support histbase (e.g., RSI centered on 50) and correctly distinguish thin histograms from thick columns
  • Live streaming -Fixed multiple issues with indicators losing colors, rendering options, and polyline coordinates during incremental updates
  • Drawing tool coordinates -Points are now stored as real data indices (not padded), so viewport expansion doesn’t invalidate drawing positions

Installation

npm install @qfo/qfchart@latest echarts

Or via CDN:

<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@qfo/qfchart/dist/qfchart.min.browser.js"></script>

]]>
https://quantforge.org/qfchart-v0-8-harmonic-patterns-fibonacci-tools-extensible-plugin-system-interactive-drawing-tools/feed/ 0
PineTS v0.9.0: Drawing Objects Complete – Box, Table & Polyline Namespaces https://quantforge.org/pinets-v0-9-0-drawing-objects-complete-box-table-polyline-namespaces/ https://quantforge.org/pinets-v0-9-0-drawing-objects-complete-box-table-polyline-namespaces/#respond Thu, 05 Mar 2026 13:57:06 +0000 https://quantforge.org/?p=164 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.

]]>
https://quantforge.org/pinets-v0-9-0-drawing-objects-complete-box-table-polyline-namespaces/feed/ 0
QFChart v0.6: Drawing Objects, New Plot Styles & Pine Script Rendering Parity https://quantforge.org/qfchart-v0-6-drawing-objects-new-plot-styles-pine-script-rendering-parity/ https://quantforge.org/qfchart-v0-6-drawing-objects-new-plot-styles-pine-script-rendering-parity/#respond Fri, 27 Feb 2026 21:31:39 +0000 https://quantforge.org/?p=150 QFChart v0.6 spans eight releases – v0.6.0 through v0.6.8 – that close the gap between QFChart and TradingView’s Pine Script rendering. This cycle adds the four remaining plot styles, fill() support, and a complete drawing object system (labels, lines, linefills). If you’ve been using QFChart since v0.5.0, this post covers every API addition, the architecture changes that enabled them, and the edge-case fixes that came along the way.


What’s Covered in v0.6


New Plot Styles (v0.6.0)

QFChart’s plotting system started with line, histogram, background, shape, and a few others. v0.6.0 added four new styles that complete Pine Script parity for the main visualization functions.

Heikin Ashi and Custom Candlesticks (style: 'candle')

The candle style lets you render a secondary candlestick series as an overlay or in a separate pane – the primary use case being Heikin Ashi candles on top of regular price data.

Each data point’s value is [open, high, low, close]:

// Standalone: Heikin Ashi candles as an overlay
const heikinAshiData = [
    { time: 1700000000000, value: [50000, 51000, 49500, 50500] },
    { time: 1700086400000, value: [50500, 52000, 50000, 51800] },
    { time: 1700172800000, value: [51800, 53000, 51200, 52600] },
    { time: 1700259200000, value: [52600, 53500, 51800, 52000] },
    { time: 1700345600000, value: [52000, 52800, 50800, 51200] },
];

chart.addIndicator('HA', {
    ha: {
        data: heikinAshiData,
        options: {
            style: 'candle',
            color: '#26a69a',   <em>// Bullish body fill</em>
            wickcolor: '#1a7a6e',
            bordercolor: '#145f57',
        },
    },
}, { overlay: true });

Per-point color overrides allow each candle to be colored individually – this is how Pine Script’s plotcandle() conditional coloring works:

// Standalone: conditional candle coloring
const haTrend = [
    {
        time: 1700000000000,
        value: [50000, 51000, 49500, 50500],
        options: { color: '#26a69a', wickcolor: '#1a7a6e' }, <em>// Bullish</em>
    },
    {
        time: 1700086400000,
        value: [50500, 52000, 50000, 51800],
        options: { color: '#26a69a', wickcolor: '#1a7a6e' }, <em>// Bullish</em>
    },
    {
        time: 1700172800000,
        value: [51800, 53000, 51200, 52600],
        options: { color: '#ef5350', wickcolor: '#b71c1c' }, <em>// Bearish reversal</em>
    },
];

chart.addIndicator('HA_Trend', {
    candles: { data: haTrend, options: { style: 'candle', color: '#888' } },
}, { overlay: true });

With PineTS – running a Heikin Ashi Pine Script indicator and feeding the output directly to QFChart:

//@version=6
indicator("Heikin Ashi", overlay=true)

haClose = (open + high + low + close) / 4
haOpen = float(na)
haOpen := na(haOpen[1]) ? (open + close) / 2 : (haOpen[1] + haClose[1]) / 2
haHigh = math.max(high, math.max(haOpen, haClose))
haLow  = math.min(low,  math.min(haOpen, haClose))

isBull = haClose > haOpen
clr = isBull ? color.teal : color.red

plotcandle(haOpen, haHigh, haLow, haClose, "HA", color=clr, wickcolor=clr)
// Feed PineTS output to QFChart
const pineTS = new PineTS(Provider.Binance, 'BTCUSDT', '60', null, startTime, endTime);
const { plots } = await pineTS.run(pineScript);

// PineTS emits plotcandle as style:'candle' with per-point color overrides
chart.addIndicator('HA', plots, { overlay: true });

OHLC Bar Charts (style: 'bar')

The bar style renders traditional OHLC bars – a vertical line from low to high with a left tick for open and a right tick for close. Same [open, high, low, close] data format as candle:

// Standalone: OHLC bars in a separate pane
const ohlcData = [
    { time: 1700000000000, value: [50000, 51000, 49500, 50500] },
    { time: 1700086400000, value: [50500, 52000, 50000, 51800] },
    { time: 1700172800000, value: [51800, 53000, 51200, 52600] },
];

chart.addIndicator('OHLC_Bars', {
    bars: {
        data: ohlcData,
        options: {
            style: 'bar',
            color: '#888888',
            wickcolor: '#555555',
        },
    },
}, { overlay: false, height: 20 });

Dynamic Candle Coloring (style: 'barcolor')

barcolor is unique – it doesn’t create any visual series at all. Instead it recolors the main chart’s candlesticks. This is how Pine Script’s barcolor() function works: a separate indicator output that modifies the appearance of existing candles.

// Standalone: color candles based on a condition
const marketData = [
    { time: 1700000000000, open: 50000, high: 51000, low: 49500, close: 50500, volume: 100 },
    { time: 1700086400000, open: 50500, high: 52000, low: 50000, close: 51800, volume: 140 },
    { time: 1700172800000, open: 51800, high: 52200, low: 50900, close: 51200, volume: 80 },
    { time: 1700259200000, open: 51200, high: 51800, low: 50000, close: 50400, volume: 120 },
    { time: 1700345600000, open: 50400, high: 51200, low: 49800, close: 51000, volume: 110 },
];

chart.setMarketData(marketData);

// Color candles based on whether volume is above average.
// The `value` field is not displayed — barcolor only uses per-point `options.color`
// to recolor the main candle. When `options.color` is absent, the candle keeps its
// default color. Any numeric value (e.g. 0) satisfies the data point requirement.
const volumeColors = {
    trend: {
        data: [
            { time: 1700000000000, value: 0 },                                      // No override — default candle color
            { time: 1700086400000, value: 0, options: { color: '#ff9800' } },       // High volume → orange
            { time: 1700172800000, value: 0 },                                      // No override
            { time: 1700259200000, value: 0, options: { color: '#ff9800' } },       // High volume → orange
            { time: 1700345600000, value: 0 },                                      // No override
        ],
        options: { style: 'barcolor', color: '#888888' },
    },
};

chart.addIndicator('Volume_Color', volumeColors, { overlay: false });

With PineTS – the barcolor() Pine Script call maps directly to style: 'barcolor':

//@version=6
indicator("RSI Bar Color", overlay=true)

rsi = ta.rsi(close, 14)
barcolor(rsi > 70 ? color.red : rsi < 30 ? color.green : na, title="RSI Color")
const { plots } = await pineTS.run(pineScript);
// plots['RSI Color'] will have style: 'barcolor'
chart.addIndicator('RSI_Color', plots, { overlay: true });

Tooltip-Only Data (style: 'char')

The char style – named after Pine Script’s plotchar() – exposes values in the chart tooltip without rendering any visual element. In Pine Script, plotchar() can display characters on the chart, but its most common use is publishing auxiliary data to the Data Window. QFChart’s char style mirrors that tooltip-only behavior. This is useful for debug values, ratios, or auxiliary data you want to inspect on hover without cluttering the chart.

// Standalone: display volume ratio in tooltip only
const volumeRatioData = [
    { time: 1700000000000, value: 1.25 }, // 25% above average
    { time: 1700086400000, value: 1.62 }, // 62% above average
    { time: 1700172800000, value: 0.73 }, // Below average
    { time: 1700259200000, value: 0.91 }, // Below average
    { time: 1700345600000, value: 1.08 }, // Slightly above average
];

chart.addIndicator('Volume_Stats', {
    volRatio: {
        data: volumeRatioData,
        options: { style: 'char', color: '#888888' },
    },
}, { overlay: true });

Plot Fill Between Lines (v0.6.4)

fill() shades the area between two existing plots. This is a must-have for Bollinger Bands, Keltner Channels, Ichimoku clouds, and any indicator with upper/lower bounds.

Unlike other plot styles, a fill plot has no data array – it references two other plots by their key names within the same indicator:

// Standalone: Bollinger Bands with fill
const now = 1700000000000;
const interval = 86400000; // 1 day

const bbPlots = {
    upper: {
        data: [
            { time: now + 0 * interval, value: 52000 },
            { time: now + 1 * interval, value: 52400 },
            { time: now + 2 * interval, value: 52800 },
            { time: now + 3 * interval, value: 52500 },
        ],
        options: { style: 'line', color: '#2196F3', linewidth: 1 },
    },
    basis: {
        data: [
            { time: now + 0 * interval, value: 50500 },
            { time: now + 1 * interval, value: 50700 },
            { time: now + 2 * interval, value: 51000 },
            { time: now + 3 * interval, value: 50800 },
        ],
        options: { style: 'line', color: '#FFC107', linewidth: 2 },
    },
    lower: {
        data: [
            { time: now + 0 * interval, value: 49000 },
            { time: now + 1 * interval, value: 49000 },
            { time: now + 2 * interval, value: 49200 },
            { time: now + 3 * interval, value: 49100 },
        ],
        options: { style: 'line', color: '#2196F3', linewidth: 1 },
    },
    // Fill between upper and lower bands — references plot keys by name
    bbFill: {
        plot1: 'upper',
        plot2: 'lower',
        options: { style: 'fill', color: 'rgba(33, 150, 243, 0.15)' },
    },
};

chart.addIndicator('BB_20', bbPlots, { overlay: true });

With PineTS – fill() in Pine Script maps directly to style: 'fill' in QFChart. Both plot1 and plot2 reference the plot keys that correspond to the titles passed to plot():

//@version=6
indicator("Bollinger Bands", overlay=true)
length = input.int(20, "Length")
mult  = input.float(2.0, "Multiplier")

[middle, upper, lower] = ta.bb(close, length, mult)

plot(middle, "Basis", color=color.yellow, linewidth=2)
p1 = plot(upper, "Upper", color=color.blue, linewidth=1)
p2 = plot(lower, "Lower", color=color.blue, linewidth=1)
fill(p1, p2, title="BB Fill", color=color.new(color.blue, 85))
const { plots } = await pineTS.run(pineScript);
// PineTS emits: plots['Basis'], plots['Upper'], plots['Lower'], plots['BB Fill']
// plots['BB Fill'] = { plot1: 'Upper', plot2: 'Lower', options: { style: 'fill', color: ... } }
chart.addIndicator('BB_20', plots, { overlay: true });

Fills render at z=1 – behind plot lines (z=2) and candles (z=5), but above the grid background (z=0). This means fills are visible on both the main chart pane and in separate panes.

Keltner Channels – Two Fills in One Indicator

Fill can be used multiple times within the same indicator. This is how Keltner Channels are commonly displayed with a green zone above the midline and a red zone below:

// Standalone: Keltner Channels with dual fills
const kcPlots = {
    upper:  { data: upperData,  options: { style: 'line', color: '#4CAF50', linewidth: 1 } },
    middle: { data: middleData, options: { style: 'line', color: '#FFC107', linewidth: 2 } },
    lower:  { data: lowerData,  options: { style: 'line', color: '#F44336', linewidth: 1 } },
    fillUp: {
        plot1: 'middle',
        plot2: 'upper',
        options: { style: 'fill', color: 'rgba(76, 175, 80, 0.12)' },
    },
    fillDown: {
        plot1: 'middle',
        plot2: 'lower',
        options: { style: 'fill', color: 'rgba(244, 67, 54, 0.12)' },
    },
};

chart.addIndicator('KC_20', kcPlots, { overlay: true });

Labels (v0.6.7)

v0.6.7 adds full label rendering support, corresponding to Pine Script’s label.* namespace. Labels display text annotations anchored to specific bars and price levels – useful for marking entry/exit points, key levels, or any event you want to annotate directly on the chart.

Unlike regular plot styles, drawing objects (labels, lines, linefills) use reserved plot keys__labels____lines__, and __linefills__. All objects of the same type are stored in a single data entry whose time must be set to the first bar’s timestamp – this is how QFChart identifies the drawing object entry and associates it with the chart’s time range:

// Standalone: BUY/SELL labels at specific price levels
const marketData = [
    { time: 1700000000000, open: 50000, high: 51000, low: 49000, close: 50500, volume: 100 },
    { time: 1700086400000, open: 50500, high: 52000, low: 50000, close: 51800, volume: 140 },
    { time: 1700172800000, open: 51800, high: 53000, low: 51000, close: 52500, volume: 150 },
    { time: 1700259200000, open: 52500, high: 53500, low: 51500, close: 52000, volume: 130 },
    { time: 1700345600000, open: 52000, high: 52500, low: 50500, close: 51000, volume: 110 },
];

chart.setMarketData(marketData);

chart.addIndicator('Signals', {
    __labels__: {
        data: [{
            time: marketData[0].time,
            value: [
                {
                    x: 1, y: 51800,
                    text: 'BUY',
                    xloc: 'bar_index',
                    yloc: 'price',
                    color: '#26a69a',
                    style: 'style_label_up',
                    textcolor: '#ffffff',
                    size: 'normal',
                    _deleted: false,
                },
                {
                    x: 4, y: 51000,
                    text: 'SELL',
                    xloc: 'bar_index',
                    yloc: 'price',
                    color: '#ef5350',
                    style: 'style_label_down',
                    textcolor: '#ffffff',
                    size: 'normal',
                    _deleted: false,
                },
            ],
            options: { style: 'label' },
        }],
        options: { style: 'label', overlay: true },
    },
}, { overlay: true });

Labels support 15 styles from style_label_up and style_label_down to style_arrowupstyle_flagstyle_circlestyle_diamond, and more. The yloc property controls positioning:

  • 'price' – positioned at the exact y price value
  • 'abovebar' – positioned above the candle’s high (y is ignored)
  • 'belowbar' – positioned below the candle’s low (y is ignored)
// Standalone: signal arrows above/below bars (y is ignored with yloc)
chart.addIndicator('Arrows', {
    __labels__: {
        data: [{
            time: marketData[0].time,
            value: [
                {
                    x: 1, y: 0,         // y ignored for yloc:'belowbar'
                    text: '',
                    xloc: 'bar_index',
                    yloc: 'belowbar',
                    color: '#26a69a',
                    style: 'style_none',
                    textcolor: '#26a69a',
                    size: 'large',
                    _deleted: false,
                },
                {
                    x: 3, y: 0,         // y ignored for yloc:'abovebar'
                    text: '',
                    xloc: 'bar_index',
                    yloc: 'abovebar',
                    color: '#ef5350',
                    style: 'style_none',
                    textcolor: '#ef5350',
                    size: 'large',
                    _deleted: false,
                },
            ],
            options: { style: 'label' },
        }],
        options: { style: 'label', overlay: true },
    },
}, { overlay: true });

With PineTS – label.new() calls in Pine Script flow through the __labels__ reserved plot key automatically:

//@version=6
indicator("EMA Cross Labels", overlay=true)

fast = ta.ema(close, 9)
slow = ta.ema(close, 21)

crossUp   = ta.crossover(fast, slow)
crossDown = ta.crossunder(fast, slow)

if crossUp
    label.new(bar_index, low, "Buy",
        xloc=xloc.bar_index, yloc=yloc.belowbar,
        color=color.teal, style=label.style_label_up,
        textcolor=color.white)

if crossDown
    label.new(bar_index, high, "Sell",
        xloc=xloc.bar_index, yloc=yloc.abovebar,
        color=color.red, style=label.style_label_down,
        textcolor=color.white)

plot(fast, "Fast EMA", color=color.teal)
plot(slow, "Slow EMA", color=color.red)
const { plots } = await pineTS.run(pineScript);
// plots includes 'Fast EMA', 'Slow EMA' (line style) + '__labels__' (label style)
chart.addIndicator('EMA_Cross', plots, { overlay: true });

Drawing Lines (v0.6.8)

v0.6.8 adds DrawingLineRenderer for Pine Script’s line.* namespace. Lines connect two (x, y) points on the chart and support solid, dashed, dotted, and arrow styles, as well as extending to the chart edges.

The key distinction: drawing lines use style: 'drawing_line' (not 'line'). The plain 'line' style is for time-series plots. This avoids collisions between the two.

// Standalone: trend lines on a chart
const marketData = [
    { time: 1700000000000, open: 50000, high: 51000, low: 49000, close: 50500, volume: 100 },
    { time: 1700086400000, open: 50500, high: 52000, low: 50000, close: 51800, volume: 140 },
    { time: 1700172800000, open: 51800, high: 53000, low: 51000, close: 52500, volume: 150 },
    { time: 1700259200000, open: 52500, high: 53500, low: 51500, close: 52000, volume: 130 },
    { time: 1700345600000, open: 52000, high: 52500, low: 50500, close: 51000, volume: 110 },
];

chart.setMarketData(marketData);

chart.addIndicator('Trend', {
    __lines__: {
        data: [{
            time: marketData[0].time,
            value: [
                // Uptrend line connecting lows — extends to the right
                {
                    x1: 0, y1: 49000,
                    x2: 2, y2: 51000,
                    xloc: 'bar_index',
                    extend: 'right',
                    color: '#26a69a',
                    style: 'style_solid',
                    width: 2,
                    _deleted: false,
                },
                // Horizontal resistance — dashed, no extension
                {
                    x1: 0, y1: 53500,
                    x2: 4, y2: 53500,
                    xloc: 'bar_index',
                    extend: 'none',
                    color: '#ef5350',
                    style: 'style_dashed',
                    width: 1,
                    _deleted: false,
                },
            ],
            options: { style: 'drawing_line' },
        }],
        options: { style: 'drawing_line', overlay: true },
    },
}, { overlay: true });

Support & Resistance with Extended Lines

The extend: 'both' mode draws a level that spans the entire visible chart – ideal for key price levels:

// Standalone: support and resistance levels extending across the full chart
chart.addIndicator('Levels', {
    __lines__: {
        data: [{
            time: marketData[0].time,
            value: [
                {
                    x1: 0, y1: 49000, x2: 1, y2: 49000,
                    xloc: 'bar_index', extend: 'both',
                    color: '#26a69a', style: 'style_dotted', width: 1,
                    _deleted: false,
                },
                {
                    x1: 0, y1: 53500, x2: 1, y2: 53500,
                    xloc: 'bar_index', extend: 'both',
                    color: '#ef5350', style: 'style_dotted', width: 1,
                    _deleted: false,
                },
            ],
            options: { style: 'drawing_line' },
        }],
        options: { style: 'drawing_line', overlay: true },
    },
}, { overlay: true });

With PineTS – line.new() calls map directly to __lines__. The PineTS runtime stores all active lines as a single aggregated array (see implementation note below):

//@version=6
indicator("Support & Resistance", overlay=true)

// Draw a horizontal line at each pivot high/low
ph = ta.pivothigh(high, 5, 5)
pl = ta.pivotlow(low,  5, 5)

if not na(ph)
    line.new(bar_index[5], ph, bar_index, ph,
        xloc=xloc.bar_index, extend=extend.right,
        color=color.red, style=line.style_dashed, width=1)

if not na(pl)
    line.new(bar_index[5], pl, bar_index, pl,
        xloc=xloc.bar_index, extend=extend.right,
        color=color.green, style=line.style_dashed, width=1)
const { plots } = await pineTS.run(pineScript);
// plots['__lines__'] contains all line objects as a single aggregated entry
chart.addIndicator('SR_Levels', plots, { overlay: true });

Linefills (v0.6.8)

LinefillRenderer fills the polygon between two line objects. This is the chart equivalent of Pine Script’s linefill.new() – useful for highlighting price channels, ranges, and zones bounded by trend lines.

Linefills use __linefills__ as the plot key and reference full line objects (with all their coordinates) directly:

// Standalone: ascending price channel with fill</em>
const upperLine = {
    x1: 0, y1: 51000, x2: 4, y2: 54000,
    xloc: 'bar_index', extend: 'none',
    color: '#2196F3', style: 'style_solid', width: 2,
    _deleted: false,
};

const lowerLine = {
    x1: 0, y1: 49000, x2: 4, y2: 51500,
    xloc: 'bar_index', extend: 'none',
    color: '#2196F3', style: 'style_solid', width: 2,
    _deleted: false,
};

chart.addIndicator('Channel', {
    // Visible boundary lines</em>
    __lines__: {
        data: [{
            time: marketData[0].time,
            value: [upperLine, lowerLine],
            options: { style: 'drawing_line' },
        }],
        options: { style: 'drawing_line', overlay: true },
    },
    // Polygon fill between the two lines</em>
    __linefills__: {
        data: [{
            time: marketData[0].time,
            value: [{
                line1: upperLine,
                line2: lowerLine,
                color: 'rgba(33, 150, 243, 0.15)',
                _deleted: false,
            }],
            options: { style: 'linefill' },
        }],
        options: { style: 'linefill', overlay: true },
    },
}, { overlay: true });

The linefill renderer reads coordinates directly from the line objects – so if the lines use extend: 'right', the fill polygon extends accordingly. You can share the same line object reference between __lines__ and __linefills__ without duplicating data.

With PineTS – linefill.new(line1, line2, color) is fully supported:

//@version=6</em>
indicator("Price Channel", overlay=true)

// Simple channel: highest high and lowest low over N bars</em>
length = input.int(20, "Channel Length")

var l_upper = line.new(na, na, na, na, extend=extend.right,
    color=color.blue, style=line.style_solid, width=2)
var l_lower = line.new(na, na, na, na, extend=extend.right,
    color=color.blue, style=line.style_solid, width=2)
var lf = linefill.new(l_upper, l_lower, color=color.new(color.blue, 85))

if barstate.islast
    line.set_x1(l_upper, bar_index - length)
    line.set_y1(l_upper, ta.highest(high, length)[1])
    line.set_x2(l_upper, bar_index)
    line.set_y2(l_upper, ta.highest(high, length))

    line.set_x1(l_lower, bar_index - length)
    line.set_y1(l_lower, ta.lowest(low, length)[1])
    line.set_x2(l_lower, bar_index)
    line.set_y2(l_lower, ta.lowest(low, length))
const { plots } = await pineTS.run(pineScript);
// plots['__lines__'] and plots['__linefills__'] are both populated</em>
chart.addIndicator('Channel', plots, { overlay: true });

Mixing Drawing Objects with PineTS

Labels, lines, and linefills can coexist in the same indicator – and when using PineTS, they do so automatically. A Pine Script that uses label.new()line.new(), and linefill.new() together produces a single plots object containing __labels____lines__, and __linefills__ alongside any regular plot series. One call to addIndicator() renders everything:

const { plots } = await pineTS.run(channelScript);
// plots = {</em>
//   'Fast EMA':    { data: [...], options: { style: 'line', ... } },</em>
//   'Slow EMA':    { data: [...], options: { style: 'line', ... } },</em>
//   '__labels__':  { data: [...], options: { style: 'label', ... } },</em>
//   '__lines__':   { data: [...], options: { style: 'drawing_line', ... } },</em>
//   '__linefills__': { data: [...], options: { style: 'linefill', ... } },</em>
// }</em>
chart.addIndicator('Channel_Analysis', plots, { overlay: true });

No manual assembly required – the reserved keys are populated by the PineTS runtime and consumed by QFChart’s renderers as-is.


Z-Level Ordering

With so many overlay types, knowing what renders on top of what matters. Here’s the complete reference:

Z-LevelElement
0Grid background
1fill() between plots (style: 'fill')
2Plot lines (style: 'line''step', etc.)
5Main candlestick series
10Linefill polygons (style: 'linefill')
15Drawing lines (style: 'drawing_line')
20Labels (style: 'label')

Labels always render on top, ensuring text annotations are never obscured. Drawing lines sit above candles, and linefill polygons sit between – providing a natural visual layering without configuration.

One important fix in v0.6.8: the FillRenderer previously used z: -5, which rendered fills behind the grid background – completely invisible on the main price pane. This was fixed to z: 1, making fills visible across all panes.


Architecture Improvements (v0.6.5)

v0.6.5 was a pure internal refactor with no API changes. The key outcome: each plot style now has its own renderer module behind a central SeriesRendererFactory. This means adding a new style – whether it’s a custom visualization or a future Pine Script type – is a single file plus a factory registration, with no risk of breaking existing styles. The drawing object renderers added in v0.6.7–v0.6.8 (labels, lines, linefills) were the first beneficiaries of this architecture.


Reliability Fixes (v0.6.6)

v0.6.6 fixed two real-world edge cases that affected usability:

Small-Price Assets (e.g., meme tokens)

The last-price line was invisible for assets like PUMP/USDT (price ~0.002) because the Y-axis was using 2 decimal places, so the markLine value rounded to 0.00 and didn’t align with any candle. The fix introduced AxisUtils.autoDetectDecimals() which inspects the price magnitude:

// Auto-detection logic (conceptual):</em>
// Price 0.002  → 4-6 decimal places</em>
// Price 45,000 → 0-2 decimal places</em>
// Price 1.25   → 2 decimal places</em>

You can override this with the yAxisDecimalPlaces option:

chart = new QFChart(container, marketData, {
    yAxisDecimalPlaces: 8,  // Manual override for very small prices</em>
});

Overlay Y-axis Contamination

Indicators containing barcolor or background plots were causing the main price Y-axis to start at negative values. The fix ensures visual-only plots (barcolorbackground) always get their own hidden Y-axis with a fixed [0, 1] range, never contaminating the main price axis.


Installation

npm install qfchart@latest

QFChart requires Apache ECharts as a peer dependency:

npm install echarts

Or via CDN (the .min.browser.js bundle is the production build – minified, no sourcemaps):

<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/qfchart/dist/qfchart.min.browser.js"></script>

For development, use the unminified bundle with sourcemaps: qfchart.dev.browser.js.


What’s Next

The v0.6 cycle established drawing object infrastructure. Next focus areas:

  • box.* support – Rectangular drawing objects (Pine Script boxes)
  • table.* support – On-chart data tables
  • polyline.* support – Multi-point drawing objects
  • Strategy output rendering – Visual backtesting results (trades, equity curve)

Resources

]]>
https://quantforge.org/qfchart-v0-6-drawing-objects-new-plot-styles-pine-script-rendering-parity/feed/ 0
LuxAlgo Becomes an Official Sponsor of PineTS https://quantforge.org/luxalgo-becomes-an-official-sponsor-of-pinets/ https://quantforge.org/luxalgo-becomes-an-official-sponsor-of-pinets/#respond Wed, 25 Feb 2026 23:54:25 +0000 https://quantforge.org/?p=145 We are thrilled to announce that LuxAlgo – the world’s largest provider of trading indicators on TradingView – is now an official sponsor of PineTS.

This is a landmark moment for the project and the community. Let us tell you why.


Who Is LuxAlgo?

If you’ve spent any time on TradingView, you’ve almost certainly come across LuxAlgo’s work. With +1.2 Million followers – the largest profile on the entire platform – and recognition as TradingView’s first-ever Pine Script Wizard company, LuxAlgo has set the standard for what’s possible with Pine Script.

Their flagship indicator, Signals & Overlays, holds the record as the highest-rated paid indicator in TradingView history with over 38,000 likes. They’ve published 360+ scripts, and what makes them stand out is that roughly 95% of their work is published as free, open indicators for the community.

In short: LuxAlgo is the biggest name in Pine Script. And they just backed PineTS !


What This Means for PineTS

Validation from the Pine Script ecosystem

PineTS was built on a bold premise: that Pine Script shouldn’t be locked inside a single platform. Having LuxAlgo – a team that lives and breathes Pine Script at the highest level – sponsor this project sends a clear message: the idea of running Pine Script anywhere is real, it matters, and the industry leaders see its value.

Accelerated development

Sponsorship means more time and resources dedicated to PineTS. Concretely, this means faster progress on the roadmap items the community has been asking for:

  • Pine Script v6 full compatibility : closing the gap with the latest TradingView syntax
  • Better documentation and tooling : lowering the barrier for new users
  • Strategy backtesting engine : one of the most requested features
  • More data providers and trading connectors : expanding where PineTS can operate

Stronger bridge between TradingView and the JavaScript ecosystem

LuxAlgo’s involvement strengthens the connection between the Pine Script world and the broader developer ecosystem. Their deep expertise in Pine Script will help ensure that PineTS stays faithful to TradingView’s semantics while pushing the boundaries of what’s possible outside of it.


What This Means for the Community

Let’s be direct: this changes nothing about how PineTS works or how you use it. PineTS remains open-source and free for personal use, research, and open-source projects under the AGPL-3.0 license. The codebase stays on GitHub. Contributions are still welcome. The project’s direction is still driven by the community’s needs.

What does change is the pace. More features, faster releases, better quality. The kind of sustained investment that turns a promising open-source project into a reliable piece of infrastructure that developers and companies can build on with confidence.


Why This Matters for the Industry

Pine Script is the most widely-used language for technical analysis, with millions of scripts written by traders worldwide. But until now, that entire ecosystem was confined to a single platform. PineTS is changing that, turning Pine Script into a portable standard for technical analysis that can run on your servers, in your browser, inside your trading bots, or as part of your ML pipelines.

When industry leaders start investing in that vision, it’s a strong signal that the ecosystem is ready for it.


A Thank You

To the LuxAlgo team: thank you for believing in this vision and putting real support behind it. Your expertise and reputation in the Pine Script ecosystem make this partnership incredibly meaningful.

To the PineTS community: thank you for your contributions, bug reports, feature requests, and enthusiasm. This sponsorship is a direct result of the momentum you built. Every star, every issue, every pull request helped bring us here.

If your organization operates in the trading or fintech space and is interested in sponsoring PineTS, get in touch.

This is just the beginning. The best is ahead.


Links:

]]>
https://quantforge.org/luxalgo-becomes-an-official-sponsor-of-pinets/feed/ 0
Introducing pinets-cli: Run Pine Script Indicators from the Command Line https://quantforge.org/introducing-pinets-cli-run-pine-script-indicators-from-the-command-line/ https://quantforge.org/introducing-pinets-cli-run-pine-script-indicators-from-the-command-line/#respond Mon, 16 Feb 2026 14:00:48 +0000 https://quantforge.org/?p=102

We’re excited to announce a new tool in the QuantForge ecosystem: pinets-cli — a command-line interface that lets you run TradingView Pine Script indicators directly from your terminal. No JavaScript project to set up, no code to write. Just point it at a .pine file and go.

pinets run rsi.pine --symbol BTCUSDT --timeframe 60

Why a CLI?

PineTS is powerful as a library, but using it requires setting up a Node.js project, writing JavaScript, and managing imports. That’s great for building applications, but sometimes you just want to:

  • Quickly test an indicator against live data
  • Automate indicator calculations in a cron job or CI pipeline
  • Pipe results into Python, jq, or a database
  • Scan multiple symbols with a simple shell loop
  • Debug your Pine Script transpilation without touching JavaScript

pinets-cli makes all of this a one-liner.

Installation

npm install -g pinets-cli

Or run without installing:

npx pinets-cli run my_indicator.pine --symbol BTCUSDT --timeframe 60

Requires Node.js 20+. The package is self-contained, it bundles the entire PineTS runtime into a single file with no additional dependencies.

Getting Started

1. Create a Pine Script file

Create sma_cross.pine:

//@version=5
indicator("SMA Cross", overlay=true)
fast = ta.sma(close, 9)
slow = ta.sma(close, 21)
plot(fast, "Fast SMA", color=color.blue)
plot(slow, "Slow SMA", color=color.red)

2. Run it

pinets run sma_cross.pine --symbol BTCUSDT --timeframe 60

That’s it. You get structured JSON output with the calculated SMA values for the last 500 hourly candles:

{
  "indicator": {
    "title": "SMA Cross",
    "overlay": true
  },
  "plots": {
    "Fast SMA": {
      "title": "Fast SMA",
      "options": { "color": "#2196F3" },
      "data": [
        { "time": 1704067200000, "value": 42150.33 },
        { "time": 1704070800000, "value": 42180.67 }
      ]
    },
    "Slow SMA": { ... }
  }
}

Key Features

Live Binance Data

Fetch real-time market data from Binance — spot or perpetual futures — with no API key required:

# Spot market
pinets run rsi.pine --symbol BTCUSDT --timeframe 1D

# Perpetual futures (append .P)
pinets run rsi.pine --symbol ETHUSDT.P --timeframe 60

Supports all standard timeframes: 151530602401D1W1M.

Custom JSON Data

Bring your own data from any source — other exchanges, CSV conversions, or historical datasets:

pinets run my_indicator.pine --data ./candles.json

The JSON file is a simple array of OHLCV candle objects:

[
  {
    "openTime": 1704067200000,
    "open": 42000.50,
    "high": 42500.00,
    "low": 41800.00,
    "close": 42300.00,
    "volume": 1234.56
  }
]

This makes pinets-cli exchange-agnostic — use data from Coinbase, Kraken, Yahoo Finance, or any source you can format as JSON.

Indicator Warmup

Long-period indicators need historical data to initialize. The --warmup flag fetches extra candles that are processed but excluded from the output:

# 200-period EMA needs warmup. Fetch 700 candles, output only the last 500.
pinets run ema200.pine --symbol BTCUSDT --timeframe 60 --candles 500 --warmup 200

No more NaN values at the start of your output.

Piping and Scripting

pinets-cli outputs clean JSON to stdout, making it a first-class citizen in shell pipelines:

# Extract the latest RSI value with jq
pinets run rsi.pine -s BTCUSDT -t 60 -q | jq '.plots.RSI.data[-1].value'

# Get all plot names from a complex indicator
pinets run macd.pine -s BTCUSDT -q | jq '.plots | keys'

Signal Filtering

Indicators that generate signals (crossovers, plotshapes) produce mostly false/null values. Use --clean to filter them out, and --plots to select only the plots you care about:

<em># Get only actual Buy and Sell signals -- no false values, no extra plots</em>
pinets run signals.pine -s BTCUSDT --plots "Buy,Sell" --clean -q | jq '.plots'

Stdin Support

Pipe indicator code directly — useful for dynamic generation or inline testing:

echo '//@version=5
indicator("Quick RSI")
plot(ta.rsi(close, 14), "RSI")' | pinets run -s BTCUSDT -t 60 -n 10 -q --pretty

Real-World Workflows

Multi-Symbol Scanning

Scan the same indicator across multiple assets with a simple loop:

for symbol in BTCUSDT ETHUSDT SOLUSDT BNBUSDT; do
  echo "=== $symbol ==="
  pinets run rsi.pine -s $symbol -t 1D -n 1 -q | jq '.plots.RSI.data[0].value'
done

Multi-Timeframe Analysis

Run the same indicator at different timeframes:

for tf in 15 60 240 1D; do
  echo "--- Timeframe: $tf ---"
  pinets run rsi.pine -s BTCUSDT -t $tf -n 1 -q | jq '.plots.RSI.data[0].value'
done

Python Integration

Feed indicator data directly into pandas for further analysis:

import subprocess
import json
import pandas as pd

result = subprocess.run(
    ['pinets', 'run', 'sma.pine', '-s', 'BTCUSDT', '-t', '1D', '-f', 'full', '-q'],
    capture_output=True, text=True
)

data = json.loads(result.stdout)

df = pd.DataFrame(data['marketData'])
df['time'] = pd.to_datetime(df['openTime'], unit='ms')

for plot_name, plot_data in data['plots'].items():
    values = [p['value'] for p in plot_data['data']]
    df[plot_name] = values

print(df[['time', 'close', 'Fast SMA', 'Slow SMA']].tail(10))

Scheduled Automation

Set up a cron job to log RSI values every hour:

# crontab -e
0 * * * * pinets run /path/to/rsi.pine -s BTCUSDT -t 60 -n 1 -q | \
  jq -r '.plots.RSI.data[0] | "\(.time),\(.value)"' >> /var/log/btc_rsi.csv

Debug Transpilation

See exactly what PineTS generates from your Pine Script:

pinets run my_indicator.pine -s BTCUSDT --debug

The transpiled code is printed to stderr, so it doesn’t interfere with the JSON output.

Complete Option Reference

Data Source

OptionShortDescriptionDefault
--symbol <ticker>-sSymbol to query (e.g., BTCUSDTETHUSDT.P)
--timeframe <tf>-tTimeframe: 1515602401D, etc.60
--data <path>-dPath to a JSON data file

Output

OptionShortDescriptionDefault
--output <path>-oWrite output to a filestdout
--format <type>-fdefault (plots) or full (plots + market data)default
--prettyForce pretty-printed JSONauto
--cleanFilter out null, false, and empty values
--plots <names>Comma-separated list of plot namesall

Candle Control

OptionShortDescriptionDefault
--candles <count>-nNumber of candles in output500
--warmup <count>-wExtra warmup candles (not in output)0

Other

OptionShortDescription
--debugShow transpiled code
--quiet-qSuppress informational messages
--version-vShow version number
--help-hShow help

The QuantForge Ecosystem

pinets-cli joins the growing QuantForge family of open-source tools for quantitative trading:

  • PineTS — The core transpiler and runtime. Use it as a library in Node.js or the browser to execute Pine Script indicators programmatically.
  • QFChart — A charting library optimized for PineTS visualization. Render indicators with TradingView-like visual fidelity.
  • pinets-cli — Run Pine Script from the command line. Designed for automation, scripting, and quick analysis.

Together, these tools let you take any Pine Script indicator from TradingView and run it on your own infrastructure — whether that’s a Node.js server, a browser dashboard, or a shell script.

Get Started

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

Full documentation is available in the docs folder, including:

Get Involved

pinets-cli is open-source (AGPL-3.0) and we welcome contributions.


PineTS and pinets-cli are independent open-source projects and are not affiliated with, endorsed by, or associated with TradingView or Pine Script(tm). All trademarks belong to their respective owners.

]]>
https://quantforge.org/introducing-pinets-cli-run-pine-script-indicators-from-the-command-line/feed/ 0
PineTS v0.8.9: Transpiler Maturity, Switch Statements & Growing Community https://quantforge.org/pinets-v0-8-9-transpiler-maturity-switch-statements-growing-community/ https://quantforge.org/pinets-v0-8-9-transpiler-maturity-switch-statements-growing-community/#respond Mon, 16 Feb 2026 13:19:57 +0000 https://quantforge.org/?p=99

We’re excited to announce PineTS v0.8.9! This release cycle (v0.8.1 through v0.8.9) focused on transpiler robustnessPine Script language coverage, and critical bug fixes – many driven by our first community contributions. Nine patch releases later, PineTS is significantly more reliable for real-world Pine Script indicators and strategies.

What’s New from v0.8.0 to v0.8.9

Full Switch Statement Support

One of the most requested Pine Script features — switch statements — is now fully supported, including complex multiline cases, TA function calls inside cases, and both expression-based and condition-based variants.

Expression-Based Switch

Use switch to select between different calculation modes at runtime:

//@version=6
indicator("Dynamic Moving Average", overlay=true)

maType = input.string("EMA", "MA Type", options=["EMA", "SMA", "RMA", "WMA"])
maLength = input.int(14, "MA Length", minval=2)

dynamicMA(src, len, maType) =>
    switch maType
        "EMA" => ta.ema(src, len)
        "SMA" => ta.sma(src, len)
        "RMA" => ta.rma(src, len)
        "WMA" => ta.wma(src, len)
        => ta.sma(src, len)

result = dynamicMA(close, maLength, maType)
plot(result, "MA", color=color.blue)

Condition-Based Switch (No Expression)

Pine Script’s switch without an expression acts like an if/else if chain — PineTS now correctly transpiles this pattern:

//@version=6
indicator("RSI Signal")

rsi = ta.rsi(close, 14)
oversold = rsi < 30
overbought = rsi > 70

var signal = 0
var strength = 0.0

switch
    oversold =>
        signal := 1
        strength := (30 - rsi) / 30
    overbought =>
        signal := -1
        strength := (rsi - 70) / 30
    =>
        signal := 0
        strength := 0.0

plot(signal, "Signal")
plot(strength, "Strength")

Multiline Case Blocks

Switch cases can now contain multiple statements, local variable declarations, nested conditionals, and TA function calls:

//@version=6
indicator("TA in Multiline Switch")

indicator_type = "trend"

result = switch indicator_type
    "trend" =>
        fast = ta.ema(close, 9)
        slow = ta.ema(close, 21)
        fast - slow
    "momentum" =>
        rsi = ta.rsi(close, 14)
        rsi


plot(result, "Result")

Plot Fill Between Lines

The new fill() function lets you shade the area between two plot lines — essential for indicators like Bollinger Bands, Keltner Channels, and Ichimoku clouds:

//@version=6
indicator("Bollinger Bands", overlay=true)
length = input.int(20, "Length")
mult = input.float(2.0, "Multiplier")

[middle, upper, lower] = ta.bb(close, length, mult)

plot(middle, "Basis", color=color.blue)
p1 = plot(upper, "Upper", color=color.red)
p2 = plot(lower, "Lower", color=color.green)
fill(p1, p2, title="BB Fill", color=color.new(color.blue, 90))

The fill() function accepts the plot objects returned by plot(), along with optional color, title, and display parameters. When paired with a charting library like QFChart, the fill area renders as a semi-transparent overlay between the two lines.

Critical Transpiler Fix: User Function Variable Isolation

v0.8.4 fixed a critical issue where local variables inside user-defined functions were sharing state across different calls. Before this fix, calling the same function multiple times could produce incorrect results because variables like sumresult, or temp would bleed between calls.

The fix introduced dynamic scoping with local contexts ($$), ensuring each function invocation gets completely isolated variable storage:

//@version=6
indicator("Multi-MA Dashboard")

ma(float source, int length, simple string maType) =>
    switch maType
        "EMA" => ta.ema(source, length)
        "SMA" => ta.sma(source, length)
        "RMA" => ta.rma(source, length)
        => ta.sma(source, length)

// Each call gets isolated state - no variable collisions
fast = ma(close, 9, "EMA")
medium = ma(close, 21, "SMA")
slow = ma(close, 50, "EMA")

plot(fast, "Fast MA")
plot(medium, "Medium MA")
plot(slow, "Slow MA")

This fix is transparent to users — the same Pine Script code just works correctly now. Under the hood, each function call is wrapped with $.call(fn, callId, ...args) and gets its own scoped context.

Enhanced For Loop Support

v0.8.7 added support for tuple destructuring in for...in loops, a commonly used pattern in Pine Script for iterating over arrays with indices:

//@version=5
indicator("Array Iteration")

processMatrix(X) =>
    for [idx, value] in X
        // idx is the array index, value is the element
        j = 0
    0

plot(close)

Additional fixes ensure that for...in and for...of loops work correctly with Pine Script arrays, including proper destructuring support and resolution of function/variable name collisions in loop contexts.

Method Call Syntax

v0.8.7 introduced proper handling of user-defined method calls. When you define a function and call it with method syntax (e.g., obj.method()), PineTS now correctly transforms it into a function call with the object as the first argument:

//@version=6
indicator("Method Syntax")

arr = array.new_float(0)
arr.push(close)
size = arr.size()

plot(size)

This also works for user-defined functions used as methods, where obj.myFunc(args) is correctly transpiled to myFunc(obj, args).

Math Namespace Additions

v0.8.4 added two new math functions:

  • math.todegrees(radians) — Converts radians to degrees
  • math.toradians(degrees) — Converts degrees to radians

These are useful for indicators involving angular calculations, cycle analysis, or trigonometric patterns.

Transpiler Reliability Improvements

This release cycle brought a wave of transpiler hardening. Here are the most impactful fixes:

VersionFixImpact
v0.8.1Operator precedence: (a + b) * c was losing parenthesesArithmetic expression correctness
v0.8.2Variable name collisions in transpilerPrevented incorrect variable renaming
v0.8.2Logical expressions (&&||) in function argumentsProper evaluation in nested calls
v0.8.3Scientific notation (10e101.2e-5) parsingLexer correctly tokenizes literals
v0.8.3Double parentheses in return statements (math.max()())Namespace calls in returns work
v0.8.4Array access in ternary expressionsCorrect scope keys in expressions
v0.8.4SMA NaN contamination in rolling windowFalls back to recalculation
v0.8.5Multiline Pine Script condition parsingIndentation errors resolved
v0.8.7Unary operators with function calls (!func())Proper AST transformation
v0.8.7Method chains (func(arg).method())Arguments transformed correctly
v0.8.8ta.bb return order [middle, upper, lower]Matches Pine Script behavior
v0.8.8color.new() RGBA conversionCorrect color string output
v0.8.9Typed variable declarationsPine Script v5 compatibility

Bollinger Bands Return Order Fix

A notable fix in v0.8.8ta.bb() now returns [middle, upper, lower] to match Pine Script’s actual return order. Previously, the order was incorrect, which could cause subtle bugs in indicators that destructure the result:

// Now correctly matches Pine Script behavior
[middle, upper, lower] = ta.bb(close, 20, 2.0)

Community Contributions

We’re thrilled to highlight our first community contributions! PineTS is growing beyond a solo project:

@dcaoyuan contributed:

  • Fixed color.new() RGBA string generation (v0.8.8)
  • Fixed ta.bb return order to match Pine Script (v0.8.8)
  • Fixed ta.highest / ta.lowest to accept length as first argument (v0.8.9)
  • Removed non-standard colors from the color palette (v0.8.9)
  • Fixed unary operator handling in transpiler (v0.8.7)

@C9Bad contributed:

  • Fixed parser to allow comments between if block and else (v0.8.8)
  • Fixed parser to allow inline comments after type fields (v0.8.8)

Thank you both for making PineTS better! Community contributions are what drive open-source projects forward.

Installation & Upgrade

Install or upgrade to the latest version:

npm install pinets@latest

Bug Fixes Summary

Across nine patch releases, here’s the full list of fixes:

  • v0.8.1: Operator precedence in complex arithmetic expressions
  • v0.8.2: Variable name collisions, logical expressions in function arguments
  • v0.8.3: Scientific notation parsing, double-parentheses in return statements
  • v0.8.4: User function variable scope isolation, SMA NaN handling, math.round precision, array access in expressions
  • v0.8.5: Multiline conditions parsing, switch syntax conversion, deprecation warning false positives
  • v0.8.6: Binance provider stream data handling
  • v0.8.7: For loop destructuring, method calls, switch IIFEs, unary operators, matrix operations
  • v0.8.8: Color conversion, comment handling, Bollinger Bands return order
  • v0.8.9: Typed variable declarations, ta.highest/ta.lowest argument handling, standard color list

What’s Next

The next release cycle will focus on:

  • Drawing primitiveslabel.*line.*box.*table.*linefill.*, and polyline.* for full TradingView drawing parity
  • Strategy namespace: Expanding strategy support for backtesting workflows
  • Performance: Continued optimization for large datasets and live streaming scenarios

Get Involved

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


]]>
https://quantforge.org/pinets-v0-8-9-transpiler-maturity-switch-statements-growing-community/feed/ 0
Tutorial : Run Pine Script on the Web, How to Build Indicators with PineTS and QFChart https://quantforge.org/tutorial-run-pine-script-on-the-web-how-to-build-indicators-with-pinets-and-qfchart/ https://quantforge.org/tutorial-run-pine-script-on-the-web-how-to-build-indicators-with-pinets-and-qfchart/#respond Sat, 24 Jan 2026 20:06:07 +0000 https://quantforge.org/?p=91 In the world of quantitative trading, Pine Script has become the de facto language for strategy development and technical analysis on TradingView. However, developers often face a significant hurdle when they want to bring those same indicators into their own web applications, proprietary dashboards, or fintech platforms.

At QuantForge, we’ve built a powerful ecosystem to solve this. In this comprehensive tutorial, we will walk you through how to use PineTS – our high-performance Pine Script execution engine – and QFChart to create a professional-grade, interactive technical analysis tool directly in the browser.

At the end of this tutorial you will be able to run overlay indicators in a candlestick chart like this :

What is PineTS and QFChart?

Before diving into the code, it is essential to understand the roles of each library in our stack:

  • PineTS: This is a specialized TypeScript-based runtime. Its primary job is to take raw Pine Script code, fetch the necessary market data (from sources like Binance), and execute the script logic to produce numerical results (plots). It effectively allows you to run “TradingView logic” anywhere JavaScript can run.
  • QFChart: While PineTS handles the “math,” QFChart handles the “visuals.” It is a charting library optimized for financial data. Built on top of the robust Apache ECharts engine, it provides high-performance rendering for OHLCV (Open, High, Low, Close, Volume) data and seamless overlaying of technical indicators.

Prerequisites

To follow this tutorial, you will need:

  1. A basic understanding of modern JavaScript (Promises, async/await).
  2. A Pine Script source file (we will use a script called cross-signal.pine).
  3. Access to the PineTS and QFChart libraries (available via our CDN or as NPM packages).

Step 1: Setting up the HTML Structure

A clean environment is the foundation of a good dashboard. We start by importing the core dependencies. Note that ECharts must be loaded before QFChart as it serves as the rendering backbone.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>QuantForge - PineScript Web Demo</title>
    
    <!-- External Dependencies -->
    <script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
    
    <!-- QuantForge Libraries -->
    <script src="https://cdn.jsdelivr.net/npm/pinets/dist/pinets.min.browser.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@qfo/qfchart/dist/qfchart.min.browser.js"></script>

    <style>
        body { font-family: 'Inter', sans-serif; margin: 0; padding: 20px; background: #f0f2f5; }
        h1 { color: #1a1a1a; text-align: center; }
        #chart-container { 
            width: 100%; 
            height: 650px; 
            background: #ffffff; 
            border-radius: 12px; 
            box-shadow: 0 10px 25px rgba(0,0,0,0.05);
            overflow: hidden;
        }
    </style>
</head>
<body>
    <h1>Advanced Financial Indicator Deployment</h1>
    <div id="chart-container">Initialising visualization engine...</div>
</body>
</html>

Step 2: Fetching Market Data and Executing Pine Script

The true power of PineTS lies in its abstraction. You don’t need to manually manage WebSocket connections or REST API calls for historical data.

In this step, we create an execution helper. It initializes a PineTS instance pointing to a specific provider (Binance in this case) and a symbol. When run() is called, the library fetches the historical candles and processes the Pine Script logic in a sandboxed environment.

async function runIndicator(pineCode) {
    // PineTS handles the heavy lifting: data fetching and logic execution
    const pineTS = new PineTS(
        PineTS.Provider.Binance, 
        'BTCUSDT', 
        '1w',  // Timeframe: 1 week candles
        500    // Depth: Load last 500 candles
    );

    // This returns the result of the calculations, the plots (lines/shapes), 
    // and the raw market data used.
    const { result, plots, marketData } = await pineTS.run(pineCode);
    
    return { marketData, plots };
}

Step 3: Preparing Data for QFChart

Data interoperability is key. PineTS provides a rich object for market data, but QFChart requires a clean array of OHLCV objects. This transformation step ensures that the timestamp and price values are mapped correctly for the chart’s time-axis.

function formatOHLCV(marketData) {
    return marketData.map(k => ({
        time: k.openTime,
        open: k.open,
        high: k.high,
        low: k.low,
        close: k.close,
        volume: k.volume
    }));
}

Step 4: Initializing the Interactive Chart & DataBox

Now we configure the QFChart instance. A crucial part of the user experience is the DataBox (the overlay showing price details).

In the configuration below, we introduce the databox property. The position option is highly versatile:

  • right: Fixes the data information to the right side of the chart.
  • left: Fixes it to the left side.
  • floating: The box follows the mouse cursor, providing a more dynamic and “modern” feel.
async function init() {
    // 1. Fetch the raw Pine Script source code
    const response = await fetch('./data/cross-signal.pine');
    const pineCode = await response.text();

    // 2. Execute calculation via PineTS
    const { marketData, plots } = await runIndicator(pineCode);
    const ohlcvData = formatOHLCV(marketData);

    // 3. Initialize QFChart with custom configuration
    const container = document.getElementById('chart-container');
    const chart = new QFChart.QFChart(container, {
        title: 'Bitcoin / USDT - Weekly Forecast',
        height: '600px',
        databox: {
            position: 'floating', // Options: 'right', 'left', 'floating'
        },
        layout: {
            mainPaneHeight: '70%',
            gap: 10
        },
        dataZoom: { visible: true, position: 'bottom' }
    });

    // 4. Set the candle data
    chart.setMarketData(ohlcvData);

    // 5. Add the computed indicator plots
    chart.addIndicator('Pine Script Signal', plots, {
        isOverlay: true, 
        titleColor: '#2962FF'
    });
}

document.addEventListener('DOMContentLoaded', init);

Step 5: Enhancing User Interaction with Drawing Plugins

To move from a static view to a professional analysis tool, users need to interact with the price action. QFChart supports a plugin architecture for Drawing Tools.

These plugins allow users to manually annotate the chart, measure price movements, or identify support and resistance levels. You can register them individually depending on the needs of your application:

// Registering drawing capability plugins
chart.registerPlugin(new QFChart.MeasureTool());   // Tool to measure % and price change
chart.registerPlugin(new QFChart.LineTool());      // Support/Resistance and Trend lines
chart.registerPlugin(new QFChart.FibonacciTool()); // Auto-calculating Fibonacci levels

Conclusion

By leveraging the QuantForge ecosystem, you can deploy complex trading indicators to the web in minutes rather than weeks. PineTS removes the complexity of data management and script parsing, while QFChart provides a high-performance, customizable UI that rivals industry leaders.

Why choose this stack?

  • Zero Backend Footprint: All calculations can happen in the user’s browser, reducing server costs and latency.
  • Direct Portability: Your existing Pine Script library becomes an instant web asset.
  • Highly Flexible UI: From floating databoxes to custom drawing plugins, you have full control over the user experience.

Ready to start? Check out the documentation and contribute to our open-source mission:

Are you building something interesting? Share your projects with us at quantforge.org.

]]>
https://quantforge.org/tutorial-run-pine-script-on-the-web-how-to-build-indicators-with-pinets-and-qfchart/feed/ 0
PineTS v0.8.0 Release: Runtime Inputs, Live Streaming & Enhanced Visualization https://quantforge.org/pinets-v0-8-0-release-runtime-inputs-live-streaming-enhanced-visualization/ https://quantforge.org/pinets-v0-8-0-release-runtime-inputs-live-streaming-enhanced-visualization/#respond Sun, 11 Jan 2026 12:00:59 +0000 https://quantforge.org/?p=85

We’re excited to announce PineTS v0.8.0, featuring runtime input overrides, live data streaming, expanded plot functions, and full support for user-defined types. This release represents a major step forward in making PineTS production-ready for real-world trading applications.

What’s New from v0.7.5 to v0.8.0

Runtime Indicator Inputs

The headline feature of v0.8.0 is the new Indicator class, which allows you to override input parameters at runtime without modifying your Pine Script source code.

import { PineTS, Provider, Indicator } from 'pinets';

const pineTS = new PineTS(Provider.Binance, 'BTCUSDT', '1h', 100);

const code = `
//@version=6
indicator("Dynamic RSI")
len = input.int(14, "Length")
src = input.source(close, "Source")
plot(ta.rsi(src, len))
`;

// Override default input values at runtime
const indicator = new Indicator(code, {
    "Length": 50,
    "Source": "high"
});

const { result } = await pineTS.run(indicator);

This feature enables powerful use cases:

  • Strategy Optimization: Test multiple parameter combinations without code changes
  • User-Configurable Dashboards: Let users adjust indicator settings through your UI
  • A/B Testing: Run the same indicator with different parameters simultaneously
  • Multi-Asset Scanning: Apply consistent logic across symbols with custom parameters

The enhanced input.* namespace methods automatically resolve values from runtime inputs via context.inputs, falling back to default values when not provided.

Live Data Streaming

The PineTS.stream() method provides an event-driven interface for handling live market data and real-time indicator updates.

const pineTS = new PineTS(Provider.Binance, 'BTCUSDT', '1m', 100);

const stream = pineTS.stream(
    `
    //@version=6
    indicator("Live Momentum")
    rsi = ta.rsi(close, 14)
    ema = ta.ema(close, 20)
    
    bullish = ta.crossover(close, ema) and rsi < 50
    plotshape(bullish, "Buy", shape.triangleup, location.belowbar, color.green)
    `,
    {
        pageSize: 50,
        live: true,
        interval: 2000, // Poll every 2 seconds
    }
);

stream.on('data', (ctx) => {
    const { rsi, ema, bullish } = ctx.result;
    
    if (bullish[bullish.length - 1]) {
        console.log('Buy signal detected at', new Date());
        // Execute your trading logic here
    }
});

stream.on('error', (err) => console.error('Stream error:', err));

// Graceful shutdown
process.on('SIGINT', () => stream.stop());

The streaming interface handles pagination, state management, and WebSocket reconnections automatically, making it ideal for building real-time trading bots and monitoring dashboards.

Enhanced Plot Functions

PineTS now supports the complete suite of Pine Script visualization functions, enabling visual parity with TradingView when paired with a charting library like QFChart.

Candlestick Rendering

//@version=6
indicator("Heikin Ashi", overlay=true)

haClose = (open + high + low + close) / 4
haOpen = float(na)
haOpen := na(haOpen[1]) ? (open + close) / 2 : (haOpen[1] + haClose[1]) / 2
haHigh = math.max(high, math.max(haOpen, haClose))
haLow = math.min(low, math.min(haOpen, haClose))

plotcandle(haOpen, haHigh, haLow, haClose, "HA", color.blue)

Traditional Bar Charts

//@version=6
indicator("OHLC Bars", overlay=true)

plotbar(open, high, low, close, "Bars", color.gray)

Background Colors

//@version=6
indicator("Trend Background")

ema20 = ta.ema(close, 20)
bullish = close > ema20

bgcolor(bullish ? color.new(color.green, 90) : color.new(color.red, 90))

Dynamic Candle Colors

//@version=6
indicator("Volume Highlight", overlay=true)

avgVol = ta.sma(volume, 20)
highVol = volume > avgVol * 1.5

barcolor(highVol ? color.yellow : na)

These additions complete the visual rendering capabilities, allowing you to build custom charting interfaces that match TradingView’s appearance and behavior.

User-Defined Types Support

Full support for Pine Script v5/v6 User-Defined Types (UDTs) enables better code organization and data encapsulation.

//@version=6
indicator("Order Book Tracker")

type OrderLevel
    float price
    float quantity
    int timestamp

type OrderBook
    OrderLevel[] bids
    OrderLevel[] asks
    
updateBook(OrderBook book, float newPrice, float newQty) =>
    level = OrderLevel.new(newPrice, newQty, time)
    array.push(book.bids, level)
    book

var book = OrderBook.new(array.new<OrderLevel>(), array.new<OrderLevel>())
book := updateBook(book, close, volume)

UDTs now correctly transpile to Type({...}) syntax (fixed in v0.8.0), ensuring full compatibility with PineTS’s series system and state management.

Installation & Upgrade

Install or upgrade to the latest version:

npm install pinets@latest

Documentation

Complete documentation is available at quantforgeorg.github.io/PineTS, including:

Bug Fixes

This release includes several important fixes:

  • v0.8.0: Fixed UDT transpilation to use Type({...}) syntax instead of JavaScript classes for proper runtime compatibility
  • v0.7.9: Resolved cache collision issues in user-defined functions containing ta.* calls through improved context stack management
  • v0.7.7: Corrected live data processing to properly handle current vs committed candles in ta.* functions

What’s Next

The next release will focus on implementing the remaining visualization functions: chart.*, label.*, line.*, box.*, table.*, linefill.*, and polyline.*. These additions will complete PineTS’s drawing and annotation capabilities, bringing full visual parity with TradingView’s charting features.

Get Involved

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


PineTS is an independent open-source project and is not affiliated with, endorsed by, or associated with TradingView or Pine Script™. All trademarks belong to their respective owners.

]]>
https://quantforge.org/pinets-v0-8-0-release-runtime-inputs-live-streaming-enhanced-visualization/feed/ 0
Introducing QFChart: TypeScript Financial Charting for Candlesticks and Indicators https://quantforge.org/introducing-qfchart-typescript-financial-charting-for-candlesticks-and-indicators/ https://quantforge.org/introducing-qfchart-typescript-financial-charting-for-candlesticks-and-indicators/#respond Tue, 06 Jan 2026 12:14:22 +0000 https://quantforge.org/?p=87 Building a trading platform or financial dashboard shouldn’t require reinventing the charting wheel. Yet most developers face a tough choice: embed third-party charting solutions that lock you into their ecosystem, or build everything from scratch using low-level canvas APIs.

We built QFChart to eliminate that trade-off.

What is QFChart?

QFChart is a lightweight, high-performance financial charting library built on Apache ECharts. It provides an intuitive API specifically designed for OHLCV candlestick charts, technical indicators, and interactive drawing tools ; everything you need to build professional trading interfaces.

The library is open-source (Apache 2.0), written in TypeScript, and works seamlessly with PineTS for a complete Pine Script execution and visualization stack, though it’s fully independent and can be used with any data provider or technical indicator library.

Why We Built QFChart

When we created PineTS to run Pine Script outside TradingView, we faced an obvious next challenge: how do you visualize the results? General-purpose charting libraries like Chart.js and D3.js are powerful, but they’re not built for the specific needs of financial charting:

  • Time-series with gaps: Markets close. Weekends happen. Financial charts need to handle non-continuous time series elegantly.
  • Multi-pane layouts: Indicators like RSI and MACD need their own panes below the main chart, each with independent scaling.
  • Overlay indicators: Moving averages and Bollinger Bands need to render directly on top of candlesticks.
  • Real-time updates: Trading bots and dashboards need incremental updates without full chart re-renders.
  • Drawing tools: Traders need trend lines, Fibonacci retracements, and measurement tools.

QFChart solves all of these problems with a clean, developer-friendly API.

Core Features

Professional Candlestick Charts

High-performance rendering of OHLCV data with customizable colors and automatic scaling:

import { QFChart } from '@qfo/qfchart';

const chart = new QFChart(container, {
    title: 'BTC/USDT',
    height: '600px',
    backgroundColor: '#1e293b',
    upColor: '#00da3c',
    downColor: '#ec0000',
});

chart.setMarketData([
    {
        time: 1620000000000,
        open: 50000,
        high: 51000,
        low: 49000,
        close: 50500,
        volume: 100000,
    },
    // ... more candles
]);

Multi-Pane Indicator Support

Stack indicators in separate panes with customizable heights and independent scaling:

const macdPlots = {
    histogram: {
        data: [
            { time: 1748217600000, value: 513.11, options: { color: '#26A69A' } },
            // ...
        ],
        options: { style: 'histogram', color: '#26A69A' },
    },
    macd: {
        data: [/* ... */],
        options: { style: 'line', color: '#2962FF' },
    },
    signal: {
        data: [/* ... */],
        options: { style: 'line', color: '#FF6D00' },
    },
};

chart.addIndicator('MACD', macdPlots, {
    isOverlay: false,
    height: 15, // Percentage of chart height
    controls: { collapse: true, maximize: true },
});

Overlay Indicators

Add indicators directly on top of the main chart:

const smaPlots = {
    sma: {
        data: [
            { time: 1620000000000, value: 50200 },
            // ...
        ],
        options: {
            style: 'line',
            color: '#2962FF',
            linewidth: 2,
        },
    },
};

chart.addIndicator('SMA_20', smaPlots, {
    isOverlay: true,
});

Rich Plot Styles

QFChart supports multiple visualization styles for rendering technical indicators:

  • line: Classic line charts for moving averages and trends
  • step: Step-line charts for discrete value changes
  • histogram: Vertical bars for volume-like data
  • columns: Grouped vertical bars
  • circles: Scatter plots with circular markers
  • cross: Cross-shaped markers for signals
  • background: Fills background areas based on conditions (like bgcolor() in Pine Script)
  • shape: Arrows, triangles, labels with custom positioning
  • bar/candle: OHLC rendering for indicators like Heikin Ashi (equivalent to plotbar() and plotcandle())
  • barcolor: Colors main chart candlesticks based on conditions (equivalent to barcolor() in Pine Script)

Each style supports per-point customization for advanced visualizations.

Real-Time Updates

For live trading applications, updateData() provides incremental updates without full re-renders:

// Keep reference to indicator
const macdIndicator = chart.addIndicator('MACD', macdPlots, {
    isOverlay: false,
    height: 15,
});

// WebSocket callback
function onNewTick(bar, indicators) {
    // Update indicator data first
    macdIndicator.updateData(indicators);
    
    // Update chart data (triggers re-render)
    chart.updateData([bar]);
}

This approach is significantly faster than calling setMarketData() and is essential for real-time trading dashboards.

Interactive Drawing Tools (Plugin System)

QFChart includes an extensible plugin system for adding interactive tools:

import { LineTool, FibonacciTool, MeasureTool } from '@qfo/qfchart';

chart.registerPlugin(new LineTool());
chart.registerPlugin(new FibonacciTool());
chart.registerPlugin(new MeasureTool());

Currently available plugins:

  • Line Drawing: Draw trend lines and support/resistance levels
  • Fibonacci Retracements: Interactive Fibonacci levels with automatic ratio calculations
  • Measure Tool: Measure price and time distances between any two points

The plugin architecture makes it easy to build custom tools specific to your trading strategy.

Flexible Layouts

Configurable data displays that don’t obstruct the chart:

const chart = new QFChart(container, {
    title: 'BTC/USDT',
    databox: {
        position: 'right', // or 'left', 'floating'
    },
});

The databox displays current OHLCV values and can be positioned to fit your UI design.

Full Customization

Every visual aspect of the chart can be customized:

const chart = new QFChart(container, {
    title: 'BTC/USDT',
    backgroundColor: '#1e293b',
    upColor: '#00da3c',
    downColor: '#ec0000',
    fontColor: '#cbd5e1',
    fontFamily: 'sans-serif',
    padding: 0.2, // Vertical padding for auto-scaling
    dataZoom: {
        visible: true,
        position: 'top',
        height: 6,
        start: 0,
        end: 100,
    },
    watermark: true, // Show "QFChart" watermark
    controls: {
        collapse: true,
        maximize: true,
        fullscreen: true,
    },
});

Perfect Pairing: QFChart + PineTS

QFChart was designed to work seamlessly with PineTS. Together, they provide a complete solution for running and visualizing Pine Script indicators:

import { PineTS, Provider } from 'pinets';
import { QFChart } from '@qfo/qfchart';

// Execute Pine Script indicator
const pineTS = new PineTS(Provider.Binance, 'BTCUSDT', '1h', 500);

const pineScript = `
//@version=6
indicator("EMA Cross")
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
plot(fast, "Fast EMA", color.blue)
plot(slow, "Slow EMA", color.red)
`;

const { plots, marketData } = await pineTS.run(pineScript);

// Visualize with QFChart
const chart = new QFChart(container, { title: 'BTC/USDT' });
chart.setMarketData(marketData);
chart.addIndicator('EMA_Cross', plots, { isOverlay: true });

This combination gives you the power to run any Pine Script indicator and render it exactly as it would appear in TradingView—but in your own application, under your control.

Installation

NPM

npm install @qfo/qfchart echarts

Browser (UMD)

<!-- 1. Include ECharts (Required) -->
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>

<!-- 2. Include QFChart -->
<script src="https://cdn.jsdelivr.net/npm/@qfo/qfchart/dist/qfchart.min.browser.js"></script>

Quick Start Example

Here’s a complete working example in less than 20 lines:

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@qfo/qfchart/dist/qfchart.min.browser.js"></script>
</head>
<body>
    <div id="chart" style="width: 100%; height: 600px;"></div>
    
    <script>
        const chart = new QFChart.QFChart(
            document.getElementById('chart'),
            { title: 'BTC/USDT' }
        );
        
        const data = [
            { time: 1620000000000, open: 50000, high: 51000, low: 49000, close: 50500, volume: 100 },
            // ... add more candles
        ];
        
        chart.setMarketData(data);
    </script>
</body>
</html>

Documentation & Resources

Use Cases

QFChart is perfect for:

  • Trading Platforms: Build custom charting interfaces for your users
  • Portfolio Dashboards: Visualize asset performance and indicators
  • Backtesting Visualizations: Display strategy results with indicators overlaid
  • Research Tools: Analyze market data with technical indicators
  • Educational Platforms: Teach technical analysis with interactive charts
  • Trading Bots: Monitor live strategy performance in real-time

What’s Next

We’re actively developing QFChart alongside PineTS. Upcoming features include:

  • Additional drawing tools (trend channels, rectangles, text annotations)
  • Enhanced customization options
  • Performance optimizations for massive datasets
  • More plugin examples and templates

Get Involved

QFChart is open-source and community-driven. We welcome contributions, bug reports, and feature requests:

The Complete Stack

QFChart is one piece of the QuantForge ecosystem:

  • PineTS: Run Pine Script indicators in JavaScript/TypeScript
  • QFChart: Visualize financial data and technical indicators
  • Together: A complete solution for building professional trading platforms

Whether you’re building a trading bot, a portfolio dashboard, or a full-fledged trading platform, QFChart gives you the charting foundation you need—without the vendor lock-in.


QFChart is open-source software licensed under Apache 2.0. Built with ❤ by QuantForge.

]]>
https://quantforge.org/introducing-qfchart-typescript-financial-charting-for-candlesticks-and-indicators/feed/ 0
Pine Script Unchained: How to Run Your Strategies Where TradingView Can’t https://quantforge.org/pine-script-unchained-how-to-run-your-strategies-where-tradingview-cant/ https://quantforge.org/pine-script-unchained-how-to-run-your-strategies-where-tradingview-cant/#respond Mon, 05 Jan 2026 16:56:04 +0000 https://quantforge.org/?p=33 The “Walled Garden” Problem

We’ve all been there. You spend hours refining a strategy in TradingView.

The backtest looks solid, the visual feedback is instant, and the syntax is incredibly intuitive.

But then you hit the wall!

You want to automate it for your algorithmic trading app, or your Quant research dashboard… but you can’t run the script on your own server. You want to build a custom dashboard for your clients, but you can’t embed the engine in your React app. You want to add market sentiment to your trigger condition, but you can’t fetch external data.

TradingView is fantastic, but it’s a walled garden. Your logic lives and dies on their servers.

Until now.

Meet PineTS: The Missing Engine

For the last few months, we’ve been building PineTS with a singular goal: to liberate your trading logic.

PineTS is the first open-source TypeScript implementation of the Pine Script™ runtime. It’s not a wrapper. It’s not a bridge to a headless browser. It is a raw, high-performance engine that understands Pine logic and runs it anywhere JavaScript runs.

  • Node.js Backend? Yes.
  • Client-side Browser? Yes.
  • Serverless Function? Yes.

Use Case 1: The “Headless” Backtester

This is the scenario that drives most quants crazy. You have a strategy, and you want to scan the entire NASDAQ-100 every 5 minutes. In TradingView, you’re clicking through charts manually.

With PineTS, you can run that same logic headless in a Node.js script.

Here is an actual example of how simple it is to initialize a context and run a script:

Assuming you have this PineScript

//@version=6
indicator("EMA Crossover Signals", overlay=true)

// Input parameters
fastLength = input.int(9, "Fast EMA Length", minval=1)
slowLength = input.int(13, "Slow EMA Length", minval=1)

// Calculate EMAs
fastEMA = ta.ema(close, fastLength)
slowEMA = ta.ema(close, slowLength)

// Plot EMAs
plot(fastEMA, "Fast EMA", color=color.blue, linewidth=2)
plot(slowEMA, "Slow EMA", color=color.red, linewidth=2)

// Detect crossovers
bullishCross = ta.crossover(fastEMA, slowEMA)
bearishCross = ta.crossunder(fastEMA, slowEMA)

// Plot buy signals (bullish cross)
plotshape(bullishCross, "Buy Signal", shape.arrowup, location.belowbar, 
          color=color.green, size=size.normal, text="Buy", textcolor=color.green)
// Plot sell signals (bearish cross)
plotshape(bearishCross, "Sell Signal", shape.arrowdown, location.abovebar, 
          color=color.red, size=size.small, text="Sell", textcolor=color.red)

You can run it using this PineTS code

import { PineTS, Provider } from 'pinets';

// Load the Pine Script source code
const response = await fetch('./data/cross-signal.pine');
const indicatorCode = await response.text();

//Run the indicator for BTC/USDT using weekly timeframe, fetch 500 last candles
const pineTS = new PineTS(PineTS.Provider.Binance, 'BTCUSDT', 'W', 500);
const { plots, marketData } = await pineTS.run(indicatorCode);

//marketData contains candles OHLCV data
//plots contain the calculated plot 

No browser. No UI overhead. Just pure math running at the speed of V8.

Alternatively you can also use PineTS syntax directly in your .ts code

import { PineTS, Provider } from 'pinets';

// Same indicator in PineTS syntax
const indicator = (context) => {
   indicator('EMA Crossover Signals', {overlay: true});

   let fastLength = input.int(9, 'Fast EMA Length', {minval: 1});
   let slowLength = input.int(13, 'Slow EMA Length', {minval: 1});
   let fastEMA = ta.ema(close, fastLength);
   let slowEMA = ta.ema(close, slowLength);

   plot(fastEMA, 'Fast EMA', {color: color.blue, linewidth: 2});
   plot(slowEMA, 'Slow EMA', {color: color.red, linewidth: 2});

   let bullishCross = ta.crossover(fastEMA, slowEMA);
   let bearishCross = ta.crossunder(fastEMA, slowEMA);

   plotshape(bullishCross, 'Buy Signal', shape.arrowup, location.belowbar, {color: color.green, size: size.normal, text: 'Buy', textcolor: color.green});
   plotshape(bearishCross, 'Sell Signal', shape.arrowdown, location.abovebar, {color: color.red, size: size.small, text: 'Sell', textcolor: color.red});

}

//Run the indicator for BTC/USDT using weekly timeframe, fetch 500 last candles
const pineTS = new PineTS(PineTS.Provider.Binance, 'BTCUSDT', 'W', 500);
const { plots, marketData } = await pineTS.run(indicator);

//marketData contains candles OHLCV data
//plots contain the calculated plot 

Use Case 2: Custom Dashboards (The “White Label” Dream)

If you are building a trading platform or a signal service, you don’t want to send your users to a 3rd party site to see charts. You want the charts inside your app.

By combining PineTS with our rendering engine, QFChart, you can build a fully custom charting interface.

  • You control the data feed.
  • You control the execution.
  • You control the UI.

Your users can tweak input parameters (like RSI Length) in your UI, and PineTS recalculates the indicators instantly in their browser—zero server lag required.

What PineTS Solves (That Others Don’t)

There are other libraries out there. Tulind is fast but hard to use. TechnicalIndicators.js is okay but lacks the “stateful” nature of Pine (like var variables or bar_index).

PineTS is different because it mimics the architecture of Pine Script:

  1. Series-based capability: We handle the “series” nature of data automatically. close[1] just works.
  2. State Management: Variables maintain state across bars exactly how you expect.
  3. Visual Parity: When paired with QFChart, an plot() in PineTS looks identical to a plot in standard charting tools.

The Future is Open

We believe financial logic shouldn’t be locked inside proprietary platforms. Whether you are a solo quant automating your strategy or a startup building the next big fintech app, you need control over your engine.

PineTS is fully open-source (AGPL-3.0). It’s still evolving—we just dropped experimental support for parsing native Pine Script v5/v6 strings directly—and we need you to break it, test it, and build with it.

Ready to break out of the walled garden?

]]>
https://quantforge.org/pine-script-unchained-how-to-run-your-strategies-where-tradingview-cant/feed/ 0