PineTS Technical Analysis (ta) Namespace

This directory contains the implementation of Pine Script’s ta.* namespace functions. These functions are designed to replicate Pine Script’s behavior in a JavaScript environment, with a strong focus on incremental calculation and Series compatibility.

Architecture Overview

Each TA function is implemented as a factory function that takes the execution context and returns the actual calculation function. This closure pattern allows functions to access the global state and other context methods efficiently.

export function ema(context: any) {
    return (source: any, period: any, callId?: string) => {
        // Implementation...
    };
}

The param() Method

The param() method is a critical component of the PineTS transpiler system. It is automatically injected by the transpiler to wrap arguments passed to namespace functions.

Purpose

  1. Normalization: Converts various input types (raw values, arrays, existing Series) into a unified Series object.
  2. Lookback Handling: Manages the offset logic when accessing historical data (e.g., close[1]).
  3. Caching: Generates unique IDs (p0, p1, etc.) to cache Series objects and avoid redundant object creation.

How it Works

When you write ta.ema(close, 14) in PineTS code, the transpiler converts it to:

ta.ema(ta.param(close, undefined, 'p0'), ta.param(14, undefined, 'p1'), '_ta0');

Inside a TA function implementation, inputs are always unwrapped using Series.from():

const value = Series.from(source).get(0); // Get current value
const length = Series.from(period).get(0); // Get period

Incremental Calculation

PineTS uses incremental calculation to process time-series data efficiently. Instead of recalculating indicators over the entire history for every new bar, functions maintain state and update it with the new bar’s data.

State Management

All TA functions store their state in context.taState. A unique callId (generated by the transpiler) is used as the key to isolate state for different calls to the same function.

Example (EMA):

// Unique key for this specific function call
const stateKey = _callId || `ema_${period}`;

// Initialize state if not present
if (!context.taState[stateKey]) {
    context.taState[stateKey] = { prevEma: null, initSum: 0, initCount: 0 };
}

const state = context.taState[stateKey];

// Update state with new value
const ema = calculateEma(currentValue, state.prevEma);
state.prevEma = ema;

Benefits

  • Performance: O(1) calculation per bar for most indicators.
  • Memory Efficiency: Only necessary state (e.g., previous value, running sum) is stored, not full history arrays for intermediate steps.

Implementation Specifics

1. Tuple Returns (Double Bracket Convention)

When a Pine Script function returns a tuple (multiple values), PineTS requires a specific return format to ensure it’s treated as a single “element” of a Series, rather than an array of values spread across time.

Rule: Return tuples wrapped in double brackets.

// Pine Script: [macd, signal, hist] = ta.macd(...)
// PineTS Implementation:
return [[macdLine, signalLine, histLine]];
  • Inner Array: The actual tuple values [a, b, c].
  • Outer Array: Wraps the tuple so the Series initialization logic ($.init()) treats the inner array as a single value for the current bar.

2. Precision

All numeric returns should be formatted using context.precision(). This ensures consistent decimal handling matching Pine Script defaults (usually 10 decimals).

return context.precision(result);

3. NaN Handling

Functions must gracefully handle NaN inputs and initialization phases.

  • Input: Check for NaN before updating state to avoid corruption (e.g., initSum += NaN results in permanent NaN).
  • Output: Return NaN during the initialization phase (warm-up period) before enough data is available.

4. Unique Call IDs

The optional _callId parameter is crucial. It allows multiple calls to the same function (e.g., ta.ema(close, 14) called in two different places) to maintain separate states. The transpiler automatically injects these IDs.

Example Implementation Structure

import { Series } from '../../../Series';

export function myIndicator(context: any) {
    return (source: any, length: any, _callId?: string) => {
        // 1. Unwrap inputs
        const period = Series.from(length).get(0);

        // 2. Initialize State
        if (!context.taState) context.taState = {};
        const stateKey = _callId || `myInd_${period}`;

        if (!context.taState[stateKey]) {
            context.taState[stateKey] = {
                /* initial state */
            };
        }

        // 3. Calculate
        const current = Series.from(source).get(0);
        // ... calculation logic ...

        // 4. Return result
        return context.precision(result);
    };
}

Getter-Like Methods in TA Namespace

Some Pine Script TA functions can be accessed both as properties and as methods. PineTS handles this through transpiler transformation.

Example: ta.tr (True Range)

Pine Script behavior:

// As property (getter)
tr1 = ta.tr  // Uses default behavior

// As method with parameter
tr2 = ta.tr(true)   // handle_na = true
tr3 = ta.tr(false)  // handle_na = false

PineTS implementation:

export function tr(context: any) {
    return (handle_na?: any) => {
        // Default to true for backward compatibility
        const handleNa = handle_na !== undefined ? Series.from(handle_na).get(0) : true;
        
        const high0 = context.get(context.data.high, 0);
        const low0 = context.get(context.data.low, 0);
        const close1 = context.get(context.data.close, 1);

        if (isNaN(close1)) {
            return handleNa ? high0 - low0 : NaN;
        }

        return Math.max(high0 - low0, Math.abs(high0 - close1), Math.abs(low0 - close1));
    };
}

Usage in PineTS:

// All these work correctly:
const tr1 = ta.tr;          // Auto-converted to ta.tr()
const tr2 = ta.tr();        // Explicit call with default
const tr3 = ta.tr(true);    // Explicit parameter
const tr4 = ta.tr(false);   // Different behavior

Implementation Guidelines

When implementing getter-like methods:

  1. Always implement as a method in methods/ directory (not getters/)
  2. Use optional parameters with sensible defaults
  3. Document the default behavior in comments
  4. Maintain backward compatibility when converting from getters

The transpiler automatically handles the conversion from property access to method calls for all known namespaces (ta, math, request, array, input).

Generating the Barrel File

When you add a new method (e.g., methods/newIndicator.ts), you must regenerate the namespace index file (ta.index.ts) to export it.

Command:

npm run generate:ta-index

This script automatically:

  1. Scans the methods/ and getters/ directories.
  2. Generates imports for all detected files.
  3. Updates ta.index.ts to register the new functions in the TechnicalAnalysis class.

Note: The getters/ directory is maintained for backward compatibility but new getter-like functions should be implemented as methods with optional parameters.