Debugging Guide

This guide covers practical debugging techniques for PineTS development.

View as Markdown

This guide covers practical debugging techniques for PineTS development.

Viewing Transpiled Code

To see what the transpiler generates:

const transformer = transpile.bind(context);
const transpiledFn = transformer(userCode);
console.log(transpiledFn.toString());

This shows the actual JavaScript that will execute, helping identify transformation issues.

Inspecting Context State

Market Data (Forward Storage)

// Check raw market data (forward arrays: oldest to newest)
const close = context.data.close;
console.log('Close array:', close);
console.log('Current close:', close[close.length - 1]); // Last element = current
console.log('Previous close:', close[close.length - 2]); // Second-to-last = previous
console.log('Oldest close:', close[0]); // First element = oldest

Using $.get() (Pine Script Semantics)

// Pine Script-style access (reverse indexing)
console.log('close[0] (current):', context.get(close, 0));
console.log('close[1] (previous):', context.get(close, 1));
console.log('close[2] (2 bars ago):', context.get(close, 2));

User Variables

// Check user-defined variables
console.log('All let variables:', context.let);
console.log('All const variables:', context.const);
console.log('All var variables:', context.var);

// Check specific variable history (forward array)
console.log('ema9 series:', context.let.glb1_ema9);
console.log('ema9 current:', context.let.glb1_ema9[context.let.glb1_ema9.length - 1]);

Function State

// Check TA function internal state
console.log('All TA state:', context.taState);
console.log('Specific EMA state:', context.taState['_ta0']);

Parameter Cache

// Check param() transformed values
console.log('All params:', context.params);
console.log('Specific param:', context.params['p0']);

Inspecting Series Objects

// Check Series wrapper details
const series = context.ta.param(close, 1, 'test');
console.log('Series data (forward array):', series.data);
console.log('Series offset (lookback):', series.offset);
console.log('Series.get(0):', series.get(0)); // Value with offset applied
console.log('Actual index accessed:', series.data.length - 1 - series.offset);

Common Debug Patterns

Pattern 1: Value Mismatch

Problem: Calculated value doesn't match expected.

Debug:

// In your TA function
export function myIndicator(context: any) {
    return (source: any, period: any, _callId?: string) => {
        const currentValue = Series.from(source).get(0);
        console.log(`[${_callId}] Current value:`, currentValue);

        const periodValue = Series.from(period).get(0);
        console.log(`[${_callId}] Period:`, periodValue);

        const state = context.taState[_callId];
        console.log(`[${_callId}] State:`, state);

        // ... calculation
        const result = /* ... */;
        console.log(`[${_callId}] Result:`, result);
        return result;
    };
}

Pattern 2: Incorrect Array Access

Problem: Getting wrong historical values.

Debug:

const close = context.data.close;

// Forward storage check
console.log('Total bars:', close.length);
console.log('Last 5 (newest to oldest):', [
    close[close.length - 1], // Current
    close[close.length - 2], // Previous
    close[close.length - 3],
    close[close.length - 4],
    close[close.length - 5],
]);

// Pine Script access check
console.log('Last 5 via $.get():', [
    context.get(close, 0), // Current
    context.get(close, 1), // Previous
    context.get(close, 2),
    context.get(close, 3),
    context.get(close, 4),
]);

// These should match!

Pattern 3: State Corruption

Problem: Function state becomes NaN or corrupted.

Debug:

export function ema(context: any) {
    return (source: any, period: any, _callId?: string) => {
        const stateKey = _callId || `ema_${period}`;
        const state = context.taState[stateKey];

        const currentValue = Series.from(source).get(0);

        // Debug NaN propagation
        if (isNaN(currentValue)) {
            console.warn(`[${stateKey}] NaN input detected at idx ${context.idx}`);
        }

        if (state && isNaN(state.prevEma)) {
            console.error(`[${stateKey}] State corrupted! prevEma is NaN`);
            console.log('State:', state);
        }

        // ... rest of calculation
    };
}

Pattern 4: Scope Issues

Problem: Variable not found or accessing wrong scope.

Debug:

// Check what variables exist in each scope
console.log(
    'Global variables (glb1_*):',
    Object.keys(context.let).filter((k) => k.startsWith('glb1_'))
);
console.log(
    'Function scope variables (fn*):',
    Object.keys(context.let).filter((k) => k.match(/^fn\d+_/))
);
console.log(
    'If scope variables (if*):',
    Object.keys(context.let).filter((k) => k.match(/^if\d+_/))
);

Debugging Transpiler Issues

Check Scope Manager State

// In transformer code
console.log('Current scope:', scopeManager.currentScope);
console.log('Scope tree:', scopeManager.scopeTree);
console.log('Variable mapping:', scopeManager.variableMap);
console.log('Context-bound vars:', scopeManager.contextBoundVariables);

Trace AST Transformation

// Add logging to transformer
transform(node: any) {
    console.log('Transforming:', node.type, node);
    const result = this.transformNode(node);
    console.log('Result:', result);
    return result;
}

Performance Debugging

Measure Execution Time

const startTime = performance.now();
const result = await pineTS.run(code);
const endTime = performance.now();
console.log(`Execution time: ${endTime - startTime}ms`);

Check Memory Usage

const memBefore = process.memoryUsage();
const result = await pineTS.run(code);
const memAfter = process.memoryUsage();

console.log('Memory delta:', {
    heapUsed: (memAfter.heapUsed - memBefore.heapUsed) / 1024 / 1024,
    external: (memAfter.external - memBefore.external) / 1024 / 1024,
});

Profile TA Function Calls

// Wrap TA function with timing
const originalEma = context.ta.ema;
const calls = { count: 0, totalTime: 0 };

context.ta.ema = (...args) => {
    const start = performance.now();
    const result = originalEma(...args);
    const elapsed = performance.now() - start;

    calls.count++;
    calls.totalTime += elapsed;

    return result;
};

// After execution
console.log(`EMA called ${calls.count} times, avg: ${calls.totalTime / calls.count}ms`);

Common Issues and Solutions

Issue: NaN Results

Symptoms: Function returns NaN unexpectedly.

Debug:

// Check inputs
console.log('Source:', Series.from(source).get(0));
console.log('Is NaN?', isNaN(Series.from(source).get(0)));

// Check state
console.log('State:', context.taState[_callId]);

// Check initialization
if (!state.initialized) {
    console.log('Still initializing, need more data');
}

Common Causes:

  • Not enough data for initialization (e.g., EMA needs period bars)
  • State corruption from NaN input
  • Missing null/undefined checks

Issue: Wrong Historical Values

Symptoms: close[1] doesn't return previous bar.

Debug:

// Verify storage order
console.log(
    'Is forward storage?',
    close[0] < close[close.length - 1] // Should be true (oldest < newest)
);

// Verify Series offset
const series = Series.from(close);
console.log('Offset:', series.offset); // Should be 0 for close
console.log('get(1) index:', series.data.length - 1 - (series.offset + 1));

Common Causes:

  • Using direct array access instead of $.get()
  • Confusing forward storage with reverse access
  • Incorrect offset in Series wrapper

Issue: State Shared Between Calls

Symptoms: Two ta.ema(close, 9) calls return identical values.

Debug:

// Check call IDs
console.log('Call 1 ID:', _callId1);
console.log('Call 2 ID:', _callId2);
console.log('Are different?', _callId1 !== _callId2); // Should be true

// Check state keys
console.log('State keys:', Object.keys(context.taState));

Common Causes:

  • Missing _callId parameter in function signature
  • Not using _callId in state key
  • Transpiler not injecting unique IDs

Testing Strategies

Unit Test Pattern

it('should calculate correctly', async () => {
    const pineTS = new PineTS(Provider.Mock, 'BTCUSDC', 'D', null, startDate, endDate);

    const sourceCode = (context) => {
        const { close } = context.data;
        const { ta } = context.pine;

        const sma = ta.sma(close, 20);

        // Debug output
        console.log('Context at idx', context.idx);
        console.log('close:', context.get(close, 0));
        console.log('sma:', context.get(sma, 0));

        return { sma };
    };

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

    // Assertions
    expect(result.sma).toBeDefined();
    expect(result.sma[result.sma.length - 1]).toBeCloseTo(expectedValue);
});

Snapshot Testing

// Capture full context state
const snapshot = {
    idx: context.idx,
    close: context.data.close.slice(-5), // Last 5 bars
    variables: { ...context.let },
    taState: { ...context.taState },
};

// Compare with expected
expect(snapshot).toMatchSnapshot();

Visual Debugging Tools

Plot All Values

const sourceCode = (context) => {
    const { close } = context.data;
    const { ta, plotchar } = context.pine;

    const sma = ta.sma(close, 20);
    const ema = ta.ema(close, 20);

    // Plot for visual inspection
    plotchar(close, 'close', { color: 'blue' });
    plotchar(sma, 'sma', { color: 'red' });
    plotchar(ema, 'ema', { color: 'green' });

    return { sma, ema };
};

Export to CSV

const { result, plots } = await pineTS.run(sourceCode);

const csv = plots['sma'].data.map((p) => `${new Date(p.time).toISOString()},${p.value}`).join('\n');

fs.writeFileSync('debug.csv', 'time,sma\n' + csv);

On this page