Appearance
Personalize your experience
Indicators
IDECopy 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
1// Minimal SMA — copy, paste, Run ▶, Apply ✓2export const metadata = { id: 'my-sma', name: 'My SMA', version: '1.0.0', category: 'trend' };34export const defaultConfig: IIndicatorConfig = {5 enabled: true, visible: true, zIndex: 200, opacity: 1,6 panel: 'overlay', // Draw on the price chart7 outputType: 'line',8 colors: ['#2962FF'], // Blue line9 lineWidths: [2],10 showValues: true, valuePrecision: 2, lookback: 20,11};1213export const defaultParams = { period: 20, source: 'close' };1415export function calculate(bars: BarData[], params = defaultParams): IIndicatorValue[] {16 const { period, source } = params;17 const result: IIndicatorValue[] = [];18 let sum = 0;1920 for (let i = 0; i < bars.length; i++) {21 sum += getPrice(bars[i], source);22 if (i >= period) sum -= getPrice(bars[i - period], source);23 result.push({24 time: bars[i].time,25 value: i >= period - 1 ? sum / period : NaN,26 });27 }28 return result;29}3031export function render(32 ctx: CanvasRenderingContext2D,33 values: IIndicatorValue[],34 toPixelX: (t: number) => number,35 toPixelY: (v: number) => number,36 config: IIndicatorConfig,37): void {38 const pts = values.filter(v => !isNaN(v.value));39 if (pts.length < 2) return;40 ctx.save();41 ctx.globalAlpha = config.opacity;42 ctx.strokeStyle = config.colors[0] || '#2962FF';43 ctx.lineWidth = config.lineWidths[0] || 2;44 ctx.lineJoin = 'round';45 ctx.beginPath();46 ctx.moveTo(toPixelX(pts[0].time), toPixelY(pts[0].value));47 for (let i = 1; i < pts.length; i++) {48 ctx.lineTo(toPixelX(pts[i].time), toPixelY(pts[i].value));49 }50 ctx.stroke();51 ctx.restore();52}🗂️ SDK Reference — getPrice() Sources
| Source string | Returns | Notes |
|---|---|---|
| "open" | bar.open | Opening price of the bar |
| "high" | bar.high | price of the bar |
| "low" | bar.low | price of the bar |
| "close" | bar.close | Closing price — most commonly used |
| "volume" | bar.volume | Volume in units |
| "hl2" | (high + low) / 2 | price |
| "hlc3" | (high + low + close) / 3 | Typical price |
| "" | (open + high + low + close) / 4 | Average price |
| "hlcc4" | (high + low + close*2) / 4 | Weighted 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:
1interface IIndicatorConfig {2 // === Visibility ===3 enabled: boolean; // Whether the indicator is active and calculating4 visible: boolean; // Whether it is drawn on the chart (can be enabled but hidden)5 zIndex: number; // Draw order — higher values render on top (default: 200)6 opacity: number; // Transparency: 0.0 (invisible) to 1.0 (fully opaque)78 // === Panel & Output ===9 panel: 'overlay' | 'separate';10 // 'overlay' → drawn directly on the price chart (SMA, EMA, Bollinger Bands, VWAP)11 // 'separate' → drawn in a dedicated panel below the chart (RSI, MACD, Stochastic)12 13 outputType: 'line' | 'multi-line' | 'histogram' | 'points' | 'area' | 'cloud';14 // 'line' → single continuous line (SMA, EMA)15 // 'multi-line' → multiple separate lines (Bollinger Bands upper/lower/middle)16 // 'histogram' → vertical bars from zero line (MACD histogram)17 // 'points' → discrete scatter points (pivot points, signals)18 // 'area' → filled region under/above a curve19 // 'cloud' → filled area between two lines (Ichimoku Cloud)2021 // === Styling ===22 colors: string[]; // Array of colors, one per output line/series23 lineWidths: number[]; // Array of line widths in pixels, one per output line24 25 // === Data Display ===26 showValues: boolean; // Whether to display values in the chart legend/tooltip27 valuePrecision: number; // Number of decimal places (e.g., 2 for 123.45)28 lookback: number; // Number of bars needed before the first valid output29}3.1 LINE — Single Line Output
The simplest output type. Draws one continuous line connecting indicator values across time. Used by most moving averages.
1// Configuration for a single line indicator2const config: IIndicatorConfig = {3 panel: 'overlay', // Show on price chart4 outputType: 'line',5 colors: ['#2962FF'], // One color for the line6 lineWidths: [2], // 2px line width7 lookback: 14,8};910// calculate() returns: { time, value }[]11// Example output: [12// { time: 1700000000, value: 152.34 },13// { time: 1700003600, value: 153.12 },14// ...15// ]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.
1// Bollinger Bands example — 3 lines2const config: IIndicatorConfig = {3 panel: 'overlay',4 outputType: 'multi-line',5 colors: ['#2962FF', '#787B86', '#2962FF'], // upper, middle, lower6 lineWidths: [1, 2, 1],7 lookback: 20,8};910// calculate() returns IIndicatorValue[] with extras:11// {12// time: 1700000000,13// value: 155.0, // Middle band (SMA)14// extras: {15// upper: 160.0, // Upper band16// lower: 150.0, // Lower band17// }18// }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.
1const config: IIndicatorConfig = {2 panel: 'separate',3 outputType: 'histogram',4 colors: ['#26a69a', '#ef5350'], // positive color, negative color5 lineWidths: [1],6 lookback: 26,7};89// Render logic checks value sign for coloring:10// value > 0 → colors[0] (green/bullish)11// 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.
1const config: IIndicatorConfig = {2 panel: 'overlay',3 outputType: 'points',4 colors: ['#26a69a', '#ef5350'], // buy signal, sell signal5 lineWidths: [8], // point radius in pixels6 lookback: 0,7};89// Use the signal property to differentiate:10// { time, value: price, signal: 1 } → buy marker (colors[0])11// { time, value: price, signal: -1 } → sell marker (colors[1])12// { 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.
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.
1const config: IIndicatorConfig = {2 panel: 'overlay',3 outputType: 'cloud',4 colors: [5 'rgba(38, 166, 154, 0.1)', // bullish cloud (line A above line B)6 'rgba(239, 83, 80, 0.1)', // bearish cloud (line B above line A)7 ],8 lineWidths: [1, 1],9 lookback: 52,10};1112// Return two values:13// { 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:
| Source | Formula | When to Use |
|---|---|---|
| close | bar.close | Most indicators (default) — reacts to closing price |
| open | bar.open | Gap analysis — tracks opening auction price |
| high | bar.high | Resistance detection — traded price in |
| low | bar.low | Support detection — traded price in |
| hl2 | (high + low) / 2 | Typical price — smoothed, good for mean reversion |
| hlc3 | (high + low + close) / 3 | Weighted typical — adds close weight |
| (open + high + low + close) / 4 | Average price — most smoothed |
1function getPrice(bar: BarData, source: string): number {2 switch (source) {3 case 'open': return bar.open;4 case 'high': return bar.high;5 case 'low': return bar.low;6 case 'close': return bar.close;7 case 'hl2': return (bar.high + bar.low) / 2;8 case 'hlc3': return (bar.high + bar.low + bar.close) / 3;9 case 'ohlc4': return (bar.open + bar.high + bar.low + bar.close) / 4;10 default: return bar.close;11 }12}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
1interface IIndicatorValue {2 time: number; // Must match the corresponding bar's timestamp3 value: number; // The calculated value (NaN during warmup)4 signal?: number; // Optional: 1 = buy, -1 = sell, 0 = neutral5 color?: string; // Optional: override the default color for this specific point6 extras?: Record<string, number>; // Additional named outputs (for multi-line)7}89// Example: Simple Moving Average10export function calculate(11 bars: BarData[],12 params: { period: number; source: string }13): IIndicatorValue[] {14 const { period, source } = params;15 const results: IIndicatorValue[] = [];1617 for (let i = 0; i < bars.length; i++) {18 if (i < period - 1) {19 // Warmup period — not enough data yet20 results.push({ time: bars[i].time, value: NaN });21 continue;22 }2324 // Compute sum of last 'period' prices25 let sum = 0;26 for (let j = i - period + 1; j <= i; j++) {27 sum += getPrice(bars[j], source);28 }2930 results.push({31 time: bars[i].time,32 value: sum / period,33 });34 }3536 return results;37}6. The render() Function
Custom rendering is done via the HTML5 Canvas 2D API. The engine provides coordinate conversion functions:
1export function render(2 ctx: CanvasRenderingContext2D, // Canvas context3 values: IIndicatorValue[], // Calculated values4 toPixelX: (time: number) => number, // Time → pixel X5 toPixelY: (value: number) => number, // Value → pixel Y6 config: IIndicatorConfig,7): void {8 const validValues = values.filter(v => !isNaN(v.value));9 if (validValues.length < 2) return;1011 ctx.save(); // ALWAYS save state before rendering1213 // Apply config14 ctx.globalAlpha = config.opacity;15 ctx.strokeStyle = config.colors[0] || '#2962FF';16 ctx.lineWidth = config.lineWidths[0] || 2;17 ctx.lineJoin = 'round'; // Smooth line joints18 ctx.lineCap = 'round'; // Smooth line ends1920 // Draw the line21 ctx.beginPath();22 ctx.moveTo(toPixelX(validValues[0].time), toPixelY(validValues[0].value));2324 for (let i = 1; i < validValues.length; i++) {25 ctx.lineTo(toPixelX(validValues[i].time), toPixelY(validValues[i].value));26 }27 ctx.stroke();2829 ctx.restore(); // ALWAYS restore state after rendering30}7. Indicator Categories
| Category | Purpose | Examples | Typical Panel |
|---|---|---|---|
| Trend | Identify market direction and | SMA, , WMA, HMA, | overlay |
| Momentum | rate of price change, overbought/oversold | , MACD, Stochastic, CCI, Williams %R, ROC | separate |
| Volatility | price dispersion and expected range | , , , Keltner | overlay/separate |
| Volume | Analyze trading volume patterns and accumulation | OBV, VWAP, , Accumulation/ | overlay/separate |
| Oscillator | Bounded range indicators for mean reversion | (0-100), Stochastic (0-100), Williams %R | separate |
| Custom | Any user-defined calculation | indicators, AI signals | either |
8. Complete Example: RSI Indicator
1/**2 * RSI (Relative Strength Index)3 * 4 * Momentum oscillator that measures speed and magnitude of price changes.5 * Values range from 0 to 100.6 * - Above 70: Overbought (potential sell signal)7 * - Below 30: Oversold (potential buy signal)8 */910export const metadata = {11 id: 'rsi',12 name: 'Relative Strength Index',13 version: '1.0.0',14 category: 'momentum',15};1617export const defaultConfig: IIndicatorConfig = {18 enabled: true,19 visible: true,20 zIndex: 200,21 opacity: 1.0,22 panel: 'separate', // RSI always goes in a separate panel23 outputType: 'line',24 colors: ['#7B1FA2'], // Purple line25 lineWidths: [2],26 showValues: true,27 valuePrecision: 2,28 lookback: 14,29};3031export const defaultParams = {32 period: 14,33 source: 'close',34 overbought: 70, // Reference line35 oversold: 30, // Reference line36};3738export function calculate(bars: BarData[], params = defaultParams) {39 const { period, source } = params;40 const results: IIndicatorValue[] = [];4142 // Track running average gain/loss for Wilder's smoothing43 let avgGain = 0;44 let avgLoss = 0;4546 for (let i = 0; i < bars.length; i++) {47 if (i === 0) {48 results.push({ time: bars[i].time, value: NaN });49 continue;50 }5152 const change = getPrice(bars[i], source) - getPrice(bars[i - 1], source);53 const gain = change > 0 ? change : 0;54 const loss = change < 0 ? -change : 0;5556 if (i < period) {57 // Accumulate for initial SMA58 avgGain += gain;59 avgLoss += loss;60 results.push({ time: bars[i].time, value: NaN });61 } else if (i === period) {62 // First RSI value — use SMA63 avgGain = (avgGain + gain) / period;64 avgLoss = (avgLoss + loss) / period;65 const rs = avgLoss === 0 ? Infinity : avgGain / avgLoss;66 const rsi = 100 - (100 / (1 + rs));67 results.push({68 time: bars[i].time,69 value: rsi,70 signal: rsi > params.overbought ? -1 : rsi < params.oversold ? 1 : 0,71 });72 } else {73 // Subsequent values — Wilder's smoothing (EMA-like)74 avgGain = (avgGain * (period - 1) + gain) / period;75 avgLoss = (avgLoss * (period - 1) + loss) / period;76 const rs = avgLoss === 0 ? Infinity : avgGain / avgLoss;77 const rsi = 100 - (100 / (1 + rs));78 results.push({79 time: bars[i].time,80 value: rsi,81 signal: rsi > params.overbought ? -1 : rsi < params.oversold ? 1 : 0,82 });83 }84 }8586 return results;87}9. Complete Example: Bollinger Bands (Multi-Line)
1export const defaultConfig: IIndicatorConfig = {2 panel: 'overlay',3 outputType: 'multi-line',4 colors: ['#2962FF', '#787B86', '#2962FF'], // upper, middle, lower5 lineWidths: [1, 2, 1],6 lookback: 20,7 // ... other defaults8};910export function calculate(bars: BarData[], params = { period: 20, stdDev: 2 }) {11 const { period, stdDev } = params;12 const results: IIndicatorValue[] = [];1314 for (let i = 0; i < bars.length; i++) {15 if (i < period - 1) {16 results.push({ time: bars[i].time, value: NaN });17 continue;18 }1920 // Calculate SMA (middle band)21 let sum = 0;22 for (let j = i - period + 1; j <= i; j++) sum += bars[j].close;23 const sma = sum / period;2425 // Calculate standard deviation26 let sumSq = 0;27 for (let j = i - period + 1; j <= i; j++) {28 sumSq += Math.pow(bars[j].close - sma, 2);29 }30 const sd = Math.sqrt(sumSq / period);3132 results.push({33 time: bars[i].time,34 value: sma, // Middle band = SMA35 extras: {36 upper: sma + stdDev * sd, // Upper band37 lower: sma - stdDev * sd, // Lower band38 },39 });40 }4142 return results;43}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
1// ❌ Slow — recomputes entire window for every bar2for (let i = period; i < bars.length; i++) {3 let sum = 0;4 for (let j = i - period; j < i; j++) sum += bars[j].close;5 results.push({ time: bars[i].time, value: sum / period });6}78// ✅ Fast — uses running sum (O(n) instead of O(n*period))9let sum = 0;10for (let i = 0; i < period; i++) sum += bars[i].close;11results.push({ time: bars[period - 1].time, value: sum / period });1213for (let i = period; i < bars.length; i++) {14 sum += bars[i].close - bars[i - period].close; // Slide window15 results.push({ time: bars[i].time, value: sum / period });16}Related articles
Related to Writing Indicators in the IDE
- 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.