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
_callIdfor unique state per function call - Extract values from Series using
.get(0)orSeries.from() - Maintain internal state (window, sum, etc.) for efficiency
- Return
NaNduring 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.trtota.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
NaNduring initialization period - Uses
_callIdfor state isolation - Uses
context.precision()for output - Handles
NaNinputs 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)