Best Practices

This guide covers common mistakes and recommended patterns when working with PineTS.

View as Markdown

Best Practices and Common Pitfalls

This guide covers common mistakes and recommended patterns when working with PineTS.

Common Pitfalls

⚠️ Pitfall 1: Confusing Storage Order vs Access Order

Problem:

// Thinking arrays are stored in reverse
let lastValue = context.data.close[0]; // ❌ Actually OLDEST bar!
let currentValue = context.data.close[context.data.close.length - 1]; // ✅ Current bar

Explanation:

  • Storage: Forward order (oldest at [0], newest at [length-1])
  • Access via $.get() or Series: Pine Script semantics (0 = current, 1 = previous)

Solution:

// ✅ Always use $.get() or Series for Pine Script semantics
let currentValue = context.get(close, 0); // Current
let previousValue = context.get(close, 1); // Previous

// Or with Series
let currentValue = Series.from(close).get(0);
let previousValue = Series.from(close).get(1);

// ❌ Direct array access gives chronological order
close[0]; // Oldest bar (not current!)

⚠️ Pitfall 2: Modifying Transpiler Without Understanding Scope

Problem: Changing variable transformation logic can break scope isolation and cause variable collisions.

// ❌ Bad: Removing scope prefixes
$.let.x = $.init($.let.x, value); // Collisions possible!

// ✅ Good: Keep scope prefixes
$.let.glb1_x = $.init($.let.glb1_x, value);
$.let.fn2_x = $.init($.let.fn2_x, value); // Different variable

Solution:

  • Always test with the transpiler test suite
  • Understand the scope tree before modifying
  • Respect the ScopeManager's variable naming conventions

⚠️ Pitfall 3: Not Handling NaN Properly

Problem:

// ❌ JavaScript NaN behavior
if (value == NaN) { ... }  // Will NEVER be true in JavaScript
value === NaN;  // Always false

Solution:

// ✅ The transpiler automatically converts == to $.math.__eq()
if (value == NaN) { ... }  // Transpiled to $.math.__eq(value, NaN)

// ✅ Or use isNaN()
if (isNaN(value)) { ... }

// ✅ In TA functions, check before updating state
const current = Series.from(source).get(0);
if (!isNaN(current)) {
    state.sum += current;  // Avoid NaN corruption
}

⚠️ Pitfall 4: Sharing State Across Function Calls

Problem:

// Two calls with same parameters should be independent
let ema1 = ta.ema(close, 9);
let ema2 = ta.ema(close, 9); // ❌ Could share state without call IDs

Explanation: Without unique call IDs, both calls would use the same state key (ema_9), causing them to return identical values.

Solution:

// ✅ Transpiler automatically injects unique call IDs
// Transpiled:
// ta.ema(close, 9, '_ta0')  // First call
// ta.ema(close, 9, '_ta1')  // Second call

// ✅ In TA function implementation, ALWAYS use _callId
export function ema(context: any) {
    return (source: any, period: any, _callId?: string) => {
        const stateKey = _callId || `ema_${period}`; // Use unique ID
        // ...
    };
}

⚠️ Pitfall 5: Not Initializing Variables with $.init()

Problem:

// ❌ Direct assignment bypasses Series initialization
$.let.var = someValue; // Won't work as time-series

Solution:

// ✅ Always use $.init() for assignments
$.let.var = $.init($.let.var, someValue);

// Note: Transpiler handles this automatically for user code

⚠️ Pitfall 6: Incorrect Tuple Return Format

Problem:

// ❌ Returning plain array (ambiguous with time-series)
export function myFunc(context: any) {
    return (source: any) => {
        return [value1, value2]; // Is this a tuple or time-series?
    };
}

Solution:

// ✅ Wrap tuple in double brackets
export function myFunc(context: any) {
    return (source: any) => {
        return [[value1, value2]]; // Clear tuple marker
    };
}

⚠️ Pitfall 7: Forgetting context.precision()

Problem:

// ❌ Returning raw floating point (inconsistent precision)
return sum / period; // 14.666666666666667

Solution:

// ✅ Use context.precision() for consistent rounding
return context.precision(sum / period); // 14.6666666667 (10 decimals)

⚠️ Pitfall 8: Recalculating Instead of Incremental Updates

Problem:

// ❌ Inefficient: Recalculate entire history every bar
export function sma(context: any) {
    return (source: any, period: any) => {
        let sum = 0;
        for (let i = 0; i < period; i++) {
            sum += Series.from(source).get(i); // O(n) per bar
        }
        return sum / period;
    };
}

Solution:

// ✅ Efficient: Incremental calculation with state
export function sma(context: any) {
    return (source: any, period: any, _callId?: string) => {
        const stateKey = _callId || `sma_${period}`;

        if (!context.taState[stateKey]) {
            context.taState[stateKey] = { window: [], sum: 0 };
        }

        const state = context.taState[stateKey];
        const current = Series.from(source).get(0);

        state.window.push(current);
        state.sum += current;

        if (state.window.length > period) {
            state.sum -= state.window.shift(); // O(1) per bar
        }

        return state.window.length >= period ? context.precision(state.sum / period) : NaN;
    };
}

Best Practices

✅ Best Practice 1: Always Use the Transpiler

Don't write transpiled code manually.

// ❌ Bad: Manually writing transpiled code
const ema = (context) => {
    $.let.glb1_ema = $.init($.let.glb1_ema, ta.ema(...));
};

// ✅ Good: Write user code, let transpiler transform it
const ema = (context) => {
    let ema = ta.ema(close, 9);
};

✅ Best Practice 2: Test with Multiple Scenarios

When modifying the transpiler or implementing TA functions, test with:

  • Simple variable assignments
  • Complex nested expressions
  • Multiple function calls with same parameters
  • Array operations and lookback
  • Conditional logic (if/else)
  • Loops (for/while)
  • Tuple returns
  • Edge cases (NaN, empty data, single bar)

✅ Best Practice 3: Understand the Context

Before debugging, understand what the context contains:

// Check context state
console.log('Variables:', context.let);
console.log('Parameters:', context.params);
console.log('TA State:', context.taState);
console.log('Current Index:', context.idx);
console.log('Market Data Length:', context.data.close.length);

✅ Best Practice 4: Respect the Scope Manager

The ScopeManager tracks:

  • Variable scopes and renaming
  • Context-bound variables
  • Loop variables
  • Array pattern elements
  • Parameter and cache ID generation

Don't bypass it or modify its state inconsistently.

✅ Best Practice 5: Implement Incremental TA Functions

Key Points:

  • Use _callId for unique state per function call
  • Extract values from Series using .get(0) or Series.from()
  • Maintain internal state (window, sum, etc.) for efficiency
  • Return NaN during initialization period (Pine Script behavior)
  • Use context.precision() for consistent decimal precision

Template:

export function myIndicator(context: any) {
    return (source: any, period: any, _callId?: string) => {
        // 1. Extract values
        const periodValue = Series.from(period).get(0);
        const currentValue = Series.from(source).get(0);

        // 2. Initialize state
        const stateKey = _callId || `myInd_${periodValue}`;
        if (!context.taState[stateKey]) {
            context.taState[stateKey] = {
                // Initial state
            };
        }

        const state = context.taState[stateKey];

        // 3. Handle NaN inputs
        if (isNaN(currentValue)) {
            return NaN;
        }

        // 4. Update state incrementally
        // ... your calculation logic ...

        // 5. Return with precision
        return context.precision(result);
    };
}

✅ Best Practice 6: Handle Initialization Periods

// ✅ Return NaN until enough data is available
if (state.window.length < period) {
    return NaN; // Pine Script behavior
}

// Calculate and return
return context.precision(state.sum / period);

✅ Best Practice 7: Use Namespace-Specific param()

// ✅ Each namespace has its own param
ta.param(value, index, 'p0'); // For TA functions
math.param(value, index, 'p1'); // For math functions
request.param(value, index, 'p2'); // For request functions

// ❌ Don't use context.param for namespace functions
context.param(value, index, 'p0'); // Use namespace-specific one instead

✅ Best Practice 8: Document Complex Transformations

// ✅ Add comments explaining non-obvious behavior
export function complexIndicator(context: any) {
    return (source: any, _callId?: string) => {
        // State stores a rolling window of 14 bars for efficiency.
        // We maintain both sum and sum-of-squares to calculate
        // standard deviation in O(1) time per bar.
        const state = context.taState[_callId];
        // ...
    };
}

✅ Best Practice 9: Use TypeScript Types

// ✅ Type your state structures
interface EMAState {
    prevEma: number | null;
    initSum: number;
    initCount: number;
}

export function ema(context: any) {
    return (source: any, period: any, _callId?: string): number => {
        const state: EMAState = context.taState[_callId];
        // ... TypeScript will catch mistakes
    };
}

✅ Best Practice 10: Clean Up Temporary Files

If you create temporary files for debugging:

// After development, remove:
// - Debug console.log statements
// - Temporary test files
// - Commented-out code
// - Unused imports

✅ Best Practice 11: Implement All Namespace Members as Methods

Everything in namespaces is a method - even constants. The transpiler handles the conversion from property access to method calls:

// ✅ CORRECT: Indicator with optional parameter
export function tr(context: any) {
    return (handle_na?: any) => {
        const handleNa = handle_na !== undefined ? Series.from(handle_na).get(0) : true;
        // ... implementation
    };
}

// ✅ CORRECT: Constant (zero-parameter method)
export function pi(context: any) {
    return () => Math.PI;
}

// ❌ WRONG: Don't use JavaScript getters
// (These belong in getters/ directory which is deprecated)

Key Points:

  • Always implement in methods/ directory
  • Use optional parameters when needed
  • The transpiler automatically converts ta.tr to ta.tr()
  • No special cases - everything follows the same pattern
  • Simpler to maintain and extend

Performance Best Practices

1. Use Incremental Calculations

Avoid: O(n) per bar Prefer: O(1) per bar with state

2. Cache Expensive Computations

// ✅ Cache in context
if (!context.cache[cacheKey]) {
    context.cache[cacheKey] = expensiveCalculation();
}
return context.cache[cacheKey];

3. Avoid Redundant Series Wrapping

// ❌ Wasteful
for (let i = 0; i < 1000; i++) {
    Series.from(source).get(0); // Creates 1000 Series objects
}

// ✅ Efficient
const series = Series.from(source);
for (let i = 0; i < 1000; i++) {
    series.get(0); // Reuses Series object
}

4. Limit State Size

// ✅ Store only what you need
state.window = state.window.slice(-period); // Keep fixed size

// ❌ Don't store entire history
state.history.push(value); // Grows indefinitely

Code Organization

Structure for New TA Functions

src/namespaces/ta/methods/
    myindicator.ts          ← Implementation
tests/namespaces/
    ta.myindicator.test.ts  ← Unit tests

Regenerate Barrel Files

# After adding new method
npm run generate:ta-index
npm run generate:math-index
npm run generate:array-index
# etc.

Testing Checklist

Before submitting TA function implementations:

  • Returns NaN during initialization period
  • Uses _callId for state isolation
  • Uses context.precision() for output
  • Handles NaN inputs gracefully
  • Implements incremental calculation (if applicable)
  • Includes unit tests with expected values
  • Tested with multiple calls (same parameters)
  • Tested with edge cases (single bar, all NaN, etc.)
  • Regenerated barrel file (npm run generate:*-index)

On this page