Appearance

Personalize your experience

Mode
Accent Color
Direction
Writing Indicators in the IDE

Indicators

IDE

Copy the template below into a new Indicator file, click Run to calculate, then Apply to draw it on the chart.

⚡ Quick Start — Minimal Working Indicator

typescript
// Minimal SMA — copy, paste, Run ▶, Apply ✓export const metadata = { id: 'my-sma', name: 'My SMA', version: '1.0.0', category: 'trend' };export const defaultConfig: IIndicatorConfig = {  enabled: true, visible: true, zIndex: 200, opacity: 1,  panel: 'overlay',        // Draw on the price chart  outputType: 'line',  colors: ['#2962FF'],     // Blue line  lineWidths: [2],  showValues: true, valuePrecision: 2, lookback: 20,};export const defaultParams = { period: 20, source: 'close' };export function calculate(bars: BarData[], params = defaultParams): IIndicatorValue[] {  const { period, source } = params;  const result: IIndicatorValue[] = [];  let sum = 0;  for (let i = 0; i < bars.length; i++) {    sum += getPrice(bars[i], source);    if (i >= period) sum -= getPrice(bars[i - period], source);    result.push({      time: bars[i].time,      value: i >= period - 1 ? sum / period : NaN,    });  }  return result;}export function render(  ctx: CanvasRenderingContext2D,  values: IIndicatorValue[],  toPixelX: (t: number) => number,  toPixelY: (v: number) => number,  config: IIndicatorConfig,): void {  const pts = values.filter(v => !isNaN(v.value));  if (pts.length < 2) return;  ctx.save();  ctx.globalAlpha = config.opacity;  ctx.strokeStyle = config.colors[0] || '#2962FF';  ctx.lineWidth = config.lineWidths[0] || 2;  ctx.lineJoin = 'round';  ctx.beginPath();  ctx.moveTo(toPixelX(pts[0].time), toPixelY(pts[0].value));  for (let i = 1; i < pts.length; i++) {    ctx.lineTo(toPixelX(pts[i].time), toPixelY(pts[i].value));  }  ctx.stroke();  ctx.restore();}
★
Pro tip:Workflow: write your calculate() logic → Run ▶ to see console output → Apply to render on the chart. The Apply button appears when script type is set to Indicator or Overlay.

🗂️ SDK Reference — getPrice() Sources

Source stringReturnsNotes
"open"bar.openOpening price of the bar
"high"bar.high price of the bar
"low"bar.low price of the bar
"close"bar.closeClosing price — most commonly used
"volume"bar.volumeVolume in units
"hl2"(high + low) / 2 price
"hlc3"(high + low + close) / 3Typical price
""(open + high + low + close) / 4Average price
"hlcc4"(high + low + close*2) / 4Weighted close

Creating Custom Indicators

Indicators are the fundamental building blocks of technical analysis in Quatick. They process (Open, High, Low, Close, Volume) bar and produce time-series values that are rendered on the chart as lines, histograms, areas, or markers.

All indicators in Quatick are written in and extend the charting engine's plugin architecture. They can render on the main price (overlaid on candlesticks) or in a separate panel below the chart.

1. Indicator Architecture Overview

Every indicator script is a self-contained module that exports the following:

  • metadata — Plugin identity (id, name, version, category, )
  • defaultConfig — Rendering configuration (, outputType, colors, lineWidths, etc.)
  • defaultParams — User-adjustable parameters (, source, multiplier, etc.)
  • calculate(bars, params) — Pure function that computes indicator values from
  • render(ctx, values, toPixelX, toPixelY, ) — Canvas 2D rendering function
  • toLightweightChartData(values) — Optional: export for lightweight-charts integration

2. Indicator Configuration (IIndicatorConfig)

The indicator configuration object controls how the indicator is rendered and behaves:

typescript
interface IIndicatorConfig {  // === Visibility ===  enabled: boolean;       // Whether the indicator is active and calculating  visible: boolean;       // Whether it is drawn on the chart (can be enabled but hidden)  zIndex: number;         // Draw order — higher values render on top (default: 200)  opacity: number;        // Transparency: 0.0 (invisible) to 1.0 (fully opaque)  // === Panel & Output ===  panel: 'overlay' | 'separate';  //   'overlay'  → drawn directly on the price chart (SMA, EMA, Bollinger Bands, VWAP)  //   'separate' → drawn in a dedicated panel below the chart (RSI, MACD, Stochastic)    outputType: 'line' | 'multi-line' | 'histogram' | 'points' | 'area' | 'cloud';  //   'line'       → single continuous line (SMA, EMA)  //   'multi-line' → multiple separate lines (Bollinger Bands upper/lower/middle)  //   'histogram'  → vertical bars from zero line (MACD histogram)  //   'points'     → discrete scatter points (pivot points, signals)  //   'area'       → filled region under/above a curve  //   'cloud'      → filled area between two lines (Ichimoku Cloud)  // === Styling ===  colors: string[];       // Array of colors, one per output line/series  lineWidths: number[];   // Array of line widths in pixels, one per output line    // === Data Display ===  showValues: boolean;    // Whether to display values in the chart legend/tooltip  valuePrecision: number; // Number of decimal places (e.g., 2 for 123.45)  lookback: number;       // Number of bars needed before the first valid output}

3.1 LINE — Single Line Output

The simplest output type. Draws one continuous line connecting indicator values across time. Used by most moving averages.

typescript
// Configuration for a single line indicatorconst config: IIndicatorConfig = {  panel: 'overlay',        // Show on price chart  outputType: 'line',  colors: ['#2962FF'],     // One color for the line  lineWidths: [2],         // 2px line width  lookback: 14,};// calculate() returns: { time, value }[]// Example output: [//   { time: 1700000000, value: 152.34 },//   { time: 1700003600, value: 153.12 },//   ...// ]

3.2 MULTI_LINE — Multiple Lines

Draws multiple lines from a single indicator. Each line gets its own color and . Used by (upper, middle, lower), Stochastic (%K, %D), etc.

typescript
// Bollinger Bands example — 3 linesconst config: IIndicatorConfig = {  panel: 'overlay',  outputType: 'multi-line',  colors: ['#2962FF', '#787B86', '#2962FF'],  // upper, middle, lower  lineWidths: [1, 2, 1],  lookback: 20,};// calculate() returns IIndicatorValue[] with extras:// {//   time: 1700000000,//   value: 155.0,              // Middle band (SMA)//   extras: {//     upper: 160.0,            // Upper band//     lower: 150.0,            // Lower band//   }// }

3.3 HISTOGRAM — Bar Chart

Renders bars extending from a zero line. Positive values go up, negative go down. Typically colored differently for positive/negative. Used by MACD , volume delta.

typescript
const config: IIndicatorConfig = {  panel: 'separate',  outputType: 'histogram',  colors: ['#26a69a', '#ef5350'],  // positive color, negative color  lineWidths: [1],  lookback: 26,};// Render logic checks value sign for coloring:// value > 0 → colors[0] (green/bullish)// value < 0 → colors[1] (red/bearish)

3.4 POINTS — Scatter Markers

Draws (circles, diamonds, arrows) at specific time-price coordinates. Used for signal markers, pivot points, pattern detections.

typescript
const config: IIndicatorConfig = {  panel: 'overlay',  outputType: 'points',  colors: ['#26a69a', '#ef5350'],  // buy signal, sell signal  lineWidths: [8],                  // point radius in pixels  lookback: 0,};// Use the signal property to differentiate:// { time, value: price, signal: 1 }   → buy marker (colors[0])// { time, value: price, signal: -1 }  → sell marker (colors[1])// { time, value: price, signal: 0 }   → neutral (no marker drawn)

3.5 AREA — Filled Region

the area between the indicator line and a reference level (usually zero or the chart bottom). Useful for volume profiles, range bands, or highlighting deviation.

typescript
const config: IIndicatorConfig = {
  panel: 'separate',
  outputType: 'area',
  colors: ['rgba(41, 98, 255, 0.15)'],  // Use rgba for transparency!
  lineWidths: [1],
  lookback: 14,
};

3.6 CLOUD — Region Between Two Lines

the area between two curves. The fill color can change depending on which line is on top. Used by Ichimoku Cloud, envelope channels.

typescript
const config: IIndicatorConfig = {  panel: 'overlay',  outputType: 'cloud',  colors: [    'rgba(38, 166, 154, 0.1)',   // bullish cloud (line A above line B)    'rgba(239, 83, 80, 0.1)',    // bearish cloud (line B above line A)  ],  lineWidths: [1, 1],  lookback: 52,};// Return two values:// { time, value: lineA, extras: { lineB: value } }

4. Price Sources

Indicators typically operate on a single price stream extracted from bars. The source parameter lets the user choose which price to use:

SourceFormulaWhen to Use
closebar.closeMost indicators (default) — reacts to closing price
openbar.openGap analysis — tracks opening auction price
highbar.highResistance detection — traded price in
lowbar.lowSupport detection — traded price in
hl2(high + low) / 2Typical price — smoothed, good for mean reversion
hlc3(high + low + close) / 3Weighted typical — adds close weight
(open + high + low + close) / 4Average price — most smoothed
typescript
function getPrice(bar: BarData, source: string): number {  switch (source) {    case 'open':  return bar.open;    case 'high':  return bar.high;    case 'low':   return bar.low;    case 'close': return bar.close;    case 'hl2':   return (bar.high + bar.low) / 2;    case 'hlc3':  return (bar.high + bar.low + bar.close) / 3;    case 'ohlc4': return (bar.open + bar.high + bar.low + bar.close) / 4;    default:      return bar.close;  }}

5. The calculate() Function

This is the heart of every indicator. It must be a pure function with no side effects:

  • Input: Array of objects sorted by time + parameter object
  • Output: Array of IIndicatorValue objects, same length as input, aligned by index
  • Values during the warmup (before enough ) must be NaN
  • Must handle edge cases: empty arrays, single bars, very datasets
  • Should not modify the input arrays
typescript
interface IIndicatorValue {  time: number;        // Must match the corresponding bar's timestamp  value: number;       // The calculated value (NaN during warmup)  signal?: number;     // Optional: 1 = buy, -1 = sell, 0 = neutral  color?: string;      // Optional: override the default color for this specific point  extras?: Record<string, number>;  // Additional named outputs (for multi-line)}// Example: Simple Moving Averageexport function calculate(  bars: BarData[],  params: { period: number; source: string }): IIndicatorValue[] {  const { period, source } = params;  const results: IIndicatorValue[] = [];  for (let i = 0; i < bars.length; i++) {    if (i < period - 1) {      // Warmup period — not enough data yet      results.push({ time: bars[i].time, value: NaN });      continue;    }    // Compute sum of last 'period' prices    let sum = 0;    for (let j = i - period + 1; j <= i; j++) {      sum += getPrice(bars[j], source);    }    results.push({      time: bars[i].time,      value: sum / period,    });  }  return results;}

6. The render() Function

Custom rendering is done via the HTML5 Canvas 2D API. The engine provides coordinate conversion functions:

typescript
export function render(  ctx: CanvasRenderingContext2D,  // Canvas context  values: IIndicatorValue[],      // Calculated values  toPixelX: (time: number) => number,   // Time → pixel X  toPixelY: (value: number) => number,  // Value → pixel Y  config: IIndicatorConfig,): void {  const validValues = values.filter(v => !isNaN(v.value));  if (validValues.length < 2) return;  ctx.save();  // ALWAYS save state before rendering  // Apply config  ctx.globalAlpha = config.opacity;  ctx.strokeStyle = config.colors[0] || '#2962FF';  ctx.lineWidth = config.lineWidths[0] || 2;  ctx.lineJoin = 'round';  // Smooth line joints  ctx.lineCap = 'round';   // Smooth line ends  // Draw the line  ctx.beginPath();  ctx.moveTo(toPixelX(validValues[0].time), toPixelY(validValues[0].value));  for (let i = 1; i < validValues.length; i++) {    ctx.lineTo(toPixelX(validValues[i].time), toPixelY(validValues[i].value));  }  ctx.stroke();  ctx.restore();  // ALWAYS restore state after rendering}
!
Caution:Always call ctx.save() before and ctx.restore() after your rendering code. This prevents your style changes (colors, line widths, transforms) from affecting other chart elements.

7. Indicator Categories

CategoryPurposeExamplesTypical Panel
TrendIdentify market direction and SMA, , WMA, HMA, overlay
Momentum rate of price change, overbought/oversold, MACD, Stochastic, CCI, Williams %R, ROCseparate
Volatility price dispersion and expected range, , , Keltneroverlay/separate
VolumeAnalyze trading volume patterns and accumulationOBV, VWAP, , Accumulation/overlay/separate
OscillatorBounded range indicators for mean reversion (0-100), Stochastic (0-100), Williams %Rseparate
CustomAny user-defined calculation indicators, AI signalseither

8. Complete Example: RSI Indicator

typescript
/** * RSI (Relative Strength Index) *  * Momentum oscillator that measures speed and magnitude of price changes. * Values range from 0 to 100. * - Above 70: Overbought (potential sell signal) * - Below 30: Oversold (potential buy signal) */export const metadata = {  id: 'rsi',  name: 'Relative Strength Index',  version: '1.0.0',  category: 'momentum',};export const defaultConfig: IIndicatorConfig = {  enabled: true,  visible: true,  zIndex: 200,  opacity: 1.0,  panel: 'separate',         // RSI always goes in a separate panel  outputType: 'line',  colors: ['#7B1FA2'],       // Purple line  lineWidths: [2],  showValues: true,  valuePrecision: 2,  lookback: 14,};export const defaultParams = {  period: 14,  source: 'close',  overbought: 70,   // Reference line  oversold: 30,     // Reference line};export function calculate(bars: BarData[], params = defaultParams) {  const { period, source } = params;  const results: IIndicatorValue[] = [];  // Track running average gain/loss for Wilder's smoothing  let avgGain = 0;  let avgLoss = 0;  for (let i = 0; i < bars.length; i++) {    if (i === 0) {      results.push({ time: bars[i].time, value: NaN });      continue;    }    const change = getPrice(bars[i], source) - getPrice(bars[i - 1], source);    const gain = change > 0 ? change : 0;    const loss = change < 0 ? -change : 0;    if (i < period) {      // Accumulate for initial SMA      avgGain += gain;      avgLoss += loss;      results.push({ time: bars[i].time, value: NaN });    } else if (i === period) {      // First RSI value — use SMA      avgGain = (avgGain + gain) / period;      avgLoss = (avgLoss + loss) / period;      const rs = avgLoss === 0 ? Infinity : avgGain / avgLoss;      const rsi = 100 - (100 / (1 + rs));      results.push({        time: bars[i].time,        value: rsi,        signal: rsi > params.overbought ? -1 : rsi < params.oversold ? 1 : 0,      });    } else {      // Subsequent values — Wilder's smoothing (EMA-like)      avgGain = (avgGain * (period - 1) + gain) / period;      avgLoss = (avgLoss * (period - 1) + loss) / period;      const rs = avgLoss === 0 ? Infinity : avgGain / avgLoss;      const rsi = 100 - (100 / (1 + rs));      results.push({        time: bars[i].time,        value: rsi,        signal: rsi > params.overbought ? -1 : rsi < params.oversold ? 1 : 0,      });    }  }  return results;}

9. Complete Example: Bollinger Bands (Multi-Line)

typescript
export const defaultConfig: IIndicatorConfig = {  panel: 'overlay',  outputType: 'multi-line',  colors: ['#2962FF', '#787B86', '#2962FF'],  // upper, middle, lower  lineWidths: [1, 2, 1],  lookback: 20,  // ... other defaults};export function calculate(bars: BarData[], params = { period: 20, stdDev: 2 }) {  const { period, stdDev } = params;  const results: IIndicatorValue[] = [];  for (let i = 0; i < bars.length; i++) {    if (i < period - 1) {      results.push({ time: bars[i].time, value: NaN });      continue;    }    // Calculate SMA (middle band)    let sum = 0;    for (let j = i - period + 1; j <= i; j++) sum += bars[j].close;    const sma = sum / period;    // Calculate standard deviation    let sumSq = 0;    for (let j = i - period + 1; j <= i; j++) {      sumSq += Math.pow(bars[j].close - sma, 2);    }    const sd = Math.sqrt(sumSq / period);    results.push({      time: bars[i].time,      value: sma,                  // Middle band = SMA      extras: {        upper: sma + stdDev * sd,  // Upper band        lower: sma - stdDev * sd,  // Lower band      },    });  }  return results;}

10. Performance Optimization

  • Pre-compute sums for windows instead of re-summing
  • Use TypedArrays (Float64Array) for datasets — 10x faster than regular arrays
  • Implement incremental calculation: update with just the latest bar instead of recomputing everything
  • Cache coordinate conversions in render() — avoid calling toPixelX/toPixelY repeatedly for the same values
  • Minimize Canvas state changes — batch similar operations together
  • Use requestAnimationFrame for rendering on high-frequency
  • Avoid allocating objects inside tight loops — pre-allocate result arrays
typescript
// ❌ Slow — recomputes entire window for every barfor (let i = period; i < bars.length; i++) {  let sum = 0;  for (let j = i - period; j < i; j++) sum += bars[j].close;  results.push({ time: bars[i].time, value: sum / period });}// ✅ Fast — uses running sum (O(n) instead of O(n*period))let sum = 0;for (let i = 0; i < period; i++) sum += bars[i].close;results.push({ time: bars[period - 1].time, value: sum / period });for (let i = period; i < bars.length; i++) {  sum += bars[i].close - bars[i - period].close;  // Slide window  results.push({ time: bars[i].time, value: sum / period });}
★
Pro tip:For indicators with thousands of bars, use the running sum technique above. It reduces from O(n × ) to O(n), which can mean the difference between 10ms and 500ms for a 10,000-bar dataset.

Related articles

  • Script API Reference

    Coding / IDE / Reference

    This reference covers all APIs, interfaces, and utilities available to indicator, overlay, and strategy scripts in Quatick IDE.

  • Building Custom Indicators

    Coding / Custom Indicators

    Composing new indicators from existing ones, the recursive shapes that need care, warm-up handling, and how to check a custom indicator is actually correct.

  • Built-in Indicators & Overlays

    Coding / IDE / Reference

    Quatick ships with a full library of pre-built indicators and overlays. You can use these out of the box, or study their implementations as reference for…

  • IDE Keyboard Shortcuts

    Coding / IDE / Keyboard

    Quatick IDE supports extensive keyboard shortcuts inspired by VS Code. Shortcuts are organized by area. On macOS, replace Ctrl with ⌘ and Alt with ⌥ where…

  • Python Scripting with Pyodide

    Coding / IDE / Python

    Quatick strategies run in Python 3.11 via Pyodide — a full CPython compiled to WebAssembly. This means your strategies execute entirely in the browser, with…

  • Quatick IDE — Getting Started

    Coding / IDE / Getting Started

    Quatick IDE is a browser-based development environment for creating custom trading indicators, overlays, and automated strategies. Scripts are written in…

  • Quatick Script SDK Reference

    Coding / IDE / Reference

    The Quatick IDE injects the SDK directly into TypeScript indicator, overlay, and strategy scripts at Apply time. You do not import SDK functions. Use the…

  • Script Best Practices

    Coding / IDE / Best Practices

    Follow these guidelines to write performant, reliable, and maintainable trading scripts.

  • Signal-based DCA

    Automation / Predefined Strategies / DCA Bot

    POST /dca-bot/webhook/<strategy id> with JSON {"token":"<webhook token>"} — from a TradingView alert, an indicator alert or any system.

Browse all knowledge base articles →