Best Practices
This guide covers common mistakes and recommended patterns when working with PineTS.
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 barExplanation:
- 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 variableSolution:
- 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 falseSolution:
// ✅ 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 IDsExplanation:
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-seriesSolution:
// ✅ 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.666666666666667Solution:
// ✅ 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 indefinitelyCode Organization
Structure for New TA Functions
src/namespaces/ta/methods/
myindicator.ts ← Implementation
tests/namespaces/
ta.myindicator.test.ts ← Unit testsRegenerate 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)