Appearance

Personalize your experience

Mode
Accent Color
Direction
Writing Overlays in the IDE

Overlays

IDE

Overlays are visual elements drawn on top of the price chart. They include annotations, zones (supply/demand, support/resistance), drawing tools, and custom visualizations. Unlike indicators, overlays don't compute time-series values — they define shapes and regions anchored to price-time coordinates.

Creating Custom Overlays

All overlays in Quatick are written in and can be interactive — supporting , click, , and resize interactions.

1. Overlay Architecture

An overlay script exports the following:

  • — Identity (id, name, version, overlay_type, )
  • defaultConfig — Anchor mode, draggable, resizable, z-index,
  • defaultStyle — style (stroke, fill, dashPattern, font, label)
  • createInitialState(, style) — Factory for new overlay instances
  • hitTest(px, py, state, toPixel, tolerance) — Check if mouse is over the overlay
  • render(ctx, state, toPixel, ) — Canvas 2D function
  • serialize(state) / deserialize(json) — Save/load overlay

2. Overlay Types

TypePointsDescriptionExamples
POINT1Single marker at a time-price coordinatePivot point, signal dot, price marker, entry/exit marker
LINE2Line segment between two anchor points, support/resistance line, regression line
SHAPE2Rectangular or elliptical region defined by two corner Supply/demand zone, highlight, consolidation box
MULTIPOINT3+ or with multiple anchor pointsFibonacci , Andrew's Pitchfork,
TEXT1 annotation positioned at a single anchor point, , trade annotation, watermark
COMPLEX2+Multi-component with lines, shapes, and Fibonacci with levels + labels + zones,

3. Anchor Modes

Overlays can be to the chart in three different ways:

AnchorCoordinatesBehaviorUse Case
time + priceMoves with chart panning and zooming — always stays at the same price/timeSupport/resistance zones, trend lines, price annotations
pixelx + y pixels on screen — stays in the same position regardless of chart Watermarks, legends, info , logos
% of /heightResponsive — maintains relative position when chart resizesCorner badges, scale-independent labels
typescript
// Data anchor — most common for chart overlaysconst supportLine = {  anchor: 'data',  points: [    { time: 1700000000, price: 150.00 },  // Start point    { time: 1700100000, price: 150.00 },  // End point    ],};// Pixel anchor — fixed screen positionconst watermark = {  anchor: 'pixel',  points: [    { x: 50, y: 30 },  // 50px from left, 30px from top  ],};// Percentage anchor — responsiveconst cornerBadge = {  anchor: 'percentage',  points: [    { x: 0.95, y: 0.05 },  // 95% right, 5% from top  ],};

4. Overlay Style (IOverlayStyle)

Overlays have a rich style configuration for appearance:

typescript
interface OverlayStyle {  // === Lines & Borders ===  strokeColor: string;     // Border/line color — any CSS color  lineWidth: number;       // Border width in pixels  lineDash: number[];      // Dash pattern: [] = solid, [5,5] = dashed,                           //   [2,8] = dotted, [10,5,2,5] = dash-dot  // === Fill ===  fillColor: string;       // Fill color — USE RGBA for transparency!                           //   'rgba(41, 98, 255, 0.1)' = 10% opacity blue  // === Text ===  font: string;            // CSS font: '12px Inter, sans-serif'  textColor: string;       // Text color  showLabel: boolean;      // Whether to display a label  labelText: string;       // Label content}// === Style Presets ===// Supply zone (bearish)const supplyStyle: OverlayStyle = {  strokeColor: '#ef5350',  fillColor: 'rgba(239, 83, 80, 0.06)',  lineWidth: 1,  lineDash: [],  font: '11px Inter, sans-serif',  textColor: '#ef5350',  showLabel: true,  labelText: 'Supply Zone',};// Demand zone (bullish)const demandStyle: OverlayStyle = {  strokeColor: '#26a69a',  fillColor: 'rgba(38, 166, 154, 0.06)',  lineWidth: 1,  lineDash: [],  font: '11px Inter, sans-serif',  textColor: '#26a69a',  showLabel: true,  labelText: 'Demand Zone',};// Trend lineconst trendLineStyle: OverlayStyle = {  strokeColor: '#2962FF',  fillColor: 'transparent',  lineWidth: 2,  lineDash: [],  font: '11px Inter, sans-serif',  textColor: '#2962FF',  showLabel: false,  labelText: '',};// Support/Resistance levelconst levelStyle: OverlayStyle = {  strokeColor: '#FF6D00',  fillColor: 'transparent',  lineWidth: 1,  lineDash: [5, 5],  // Dashed line  font: '10px Inter, sans-serif',  textColor: '#FF6D00',  showLabel: true,  labelText: 'S/R Level',};

5. Interaction States

Interactive overlays transition between states based on user interaction:

StateTriggered ByVisual FeedbackCursor
normalDefault / mouse leavesStandard renderingdefault
hoveredMouse enters overlay boundsSlightly brighter fill, increased line pointer
selectedUser clicks on overlayHighlight border (gold), show resize handlespointer
draggingUser drags selected overlayReduced (70%), movementmove
resizingUser drags a resize handleShow dimension guides, minimum size enforcednwse-resize etc.
lockedUser locks the overlayDimmed appearance, no interaction handlesnot-allowed
typescript
// Adjust style based on interaction statefunction getInteractionStyle(  baseStyle: OverlayStyle,   interaction: InteractionState): { strokeColor: string; fillColor: string; lineWidth: number; opacity: number } {  let { strokeColor, fillColor, lineWidth } = baseStyle;  let opacity = 1.0;  switch (interaction) {    case 'hovered':      lineWidth += 1;      // Increase fill opacity slightly      fillColor = fillColor.replace(/[\d.]+\)$/, '0.15)');      break;    case 'selected':      strokeColor = '#FFD700';  // Gold highlight      lineWidth += 1;      break;    case 'dragging':      opacity = 0.7;      break;    case 'resizing':      strokeColor = '#FFD700';      lineWidth = 1;      break;    case 'locked':      opacity = 0.5;      strokeColor = '#787B86';  // Gray      break;  }  return { strokeColor, fillColor, lineWidth, opacity };}

6. Hit Testing

Hit testing determines if a mouse coordinate falls within the overlay bounds. The accuracy of hit testing determines how responsive your overlay feels.

6.1 Rectangle Hit Test

typescript
function hitTestRect(  mouseX: number, mouseY: number,  topLeft: PixelPoint, bottomRight: PixelPoint,  tolerance: number = 8): boolean {  return (    mouseX >= Math.min(topLeft.x, bottomRight.x) - tolerance &&    mouseX <= Math.max(topLeft.x, bottomRight.x) + tolerance &&    mouseY >= Math.min(topLeft.y, bottomRight.y) - tolerance &&    mouseY <= Math.max(topLeft.y, bottomRight.y) + tolerance  );}

6.2 Line Hit Test (Point-to-Line Distance)

typescript
function hitTestLine(  mouseX: number, mouseY: number,  lineStart: PixelPoint, lineEnd: PixelPoint,  tolerance: number = 6): boolean {  const dx = lineEnd.x - lineStart.x;  const dy = lineEnd.y - lineStart.y;  const lengthSq = dx * dx + dy * dy;  if (lengthSq === 0) {    // Line is a point    const dist = Math.sqrt(      Math.pow(mouseX - lineStart.x, 2) + Math.pow(mouseY - lineStart.y, 2)    );    return dist <= tolerance;  }  // Project mouse point onto line segment  let t = ((mouseX - lineStart.x) * dx + (mouseY - lineStart.y) * dy) / lengthSq;  t = Math.max(0, Math.min(1, t));  // Clamp to segment  const projX = lineStart.x + t * dx;  const projY = lineStart.y + t * dy;  const distance = Math.sqrt(Math.pow(mouseX - projX, 2) + Math.pow(mouseY - projY, 2));  return distance <= tolerance;}

6.3 Ellipse Hit Test

typescript
function hitTestEllipse(  mouseX: number, mouseY: number,  center: PixelPoint, radiusX: number, radiusY: number,  tolerance: number = 6): boolean {  // Point-in-ellipse test: (x/a)² + (y/b)² <= 1  const dx = (mouseX - center.x) / (radiusX + tolerance);  const dy = (mouseY - center.y) / (radiusY + tolerance);  return (dx * dx + dy * dy) <= 1;}

7.1 Horizontal Price Level

typescript
function renderHorizontalLine(  ctx: CanvasRenderingContext2D,  price: number,  toPixelY: (p: number) => number,  chartWidth: number,  style: OverlayStyle,) {  const y = toPixelY(price);    ctx.save();  ctx.strokeStyle = style.strokeColor;  ctx.lineWidth = style.lineWidth;  ctx.setLineDash(style.lineDash);    // Draw the line across the full chart width  ctx.beginPath();  ctx.moveTo(0, y);  ctx.lineTo(chartWidth, y);  ctx.stroke();    // Draw price label  if (style.showLabel) {    const text = style.labelText || price.toFixed(2);    ctx.font = style.font;    const metrics = ctx.measureText(text);    const padding = 4;    const labelWidth = metrics.width + padding * 2;    const labelHeight = 16;        // Label background    ctx.fillStyle = style.strokeColor;    ctx.fillRect(      chartWidth - labelWidth - 4,      y - labelHeight / 2,      labelWidth,      labelHeight    );        // Label text    ctx.fillStyle = '#ffffff';    ctx.textBaseline = 'middle';    ctx.textAlign = 'right';    ctx.fillText(text, chartWidth - 8, y);  }    ctx.restore();}

7.2 Supply/Demand Zone (Rectangle)

typescript
function renderZone(  ctx: CanvasRenderingContext2D,  state: OverlayState,  toPixel: (dp: DataPoint) => PixelPoint,  config: OverlayConfig,) {  if (state.points.length < 2) return;    const p1 = toPixel(state.points[0]);  const p2 = toPixel(state.points[1]);  const { style, interaction } = state;    ctx.save();  const iStyle = getInteractionStyle(style, interaction);  ctx.globalAlpha = iStyle.opacity;    const x = Math.min(p1.x, p2.x);  const y = Math.min(p1.y, p2.y);  const w = Math.abs(p2.x - p1.x);  const h = Math.abs(p2.y - p1.y);    // Fill  ctx.fillStyle = iStyle.fillColor;  ctx.fillRect(x, y, w, h);    // Border  ctx.strokeStyle = iStyle.strokeColor;  ctx.lineWidth = iStyle.lineWidth;  ctx.setLineDash(style.lineDash);  ctx.strokeRect(x, y, w, h);    // Top edge highlight (stronger line at zone boundary)  ctx.lineWidth = style.lineWidth + 1;  ctx.setLineDash([]);  ctx.beginPath();  ctx.moveTo(x, y);           // Top-left  ctx.lineTo(x + w, y);       // Top-right  ctx.stroke();    // Label  if (style.showLabel) {    ctx.font = style.font;    ctx.fillStyle = style.textColor;    ctx.globalAlpha = 0.8;    ctx.textBaseline = 'top';    ctx.textAlign = 'left';    ctx.fillText(style.labelText, x + 6, y + 4);  }    // Resize handles when selected  if (interaction === 'selected' || interaction === 'resizing') {    ctx.fillStyle = iStyle.strokeColor;    const hs = 5;    [[x, y], [x+w, y], [x, y+h], [x+w, y+h]].forEach(([cx, cy]) => {      ctx.fillRect(cx - hs/2, cy - hs/2, hs, hs);    });  }    ctx.restore();}

7.3 Trend Line with Extension

typescript
function renderTrendLine(  ctx: CanvasRenderingContext2D,  state: OverlayState,  toPixel: (dp: DataPoint) => PixelPoint,  chartWidth: number,  extend: boolean = true,) {  if (state.points.length < 2) return;    const p1 = toPixel(state.points[0]);  const p2 = toPixel(state.points[1]);    ctx.save();  ctx.strokeStyle = state.style.strokeColor;  ctx.lineWidth = state.style.lineWidth;  ctx.setLineDash(state.style.lineDash);  ctx.lineJoin = 'round';  ctx.lineCap = 'round';    // Draw main line segment  ctx.beginPath();  ctx.moveTo(p1.x, p1.y);  ctx.lineTo(p2.x, p2.y);  ctx.stroke();    // Extend line to chart edge  if (extend) {    const dx = p2.x - p1.x;    const dy = p2.y - p1.y;        if (dx !== 0) {      const slope = dy / dx;      const extendX = chartWidth;      const extendY = p2.y + slope * (extendX - p2.x);            ctx.globalAlpha = 0.3;  // Dimmer for extension      ctx.setLineDash([5, 5]);      ctx.beginPath();      ctx.moveTo(p2.x, p2.y);      ctx.lineTo(extendX, extendY);      ctx.stroke();    }  }    // Draw anchor points  ctx.globalAlpha = 1.0;  ctx.setLineDash([]);  ctx.fillStyle = state.style.strokeColor;  [p1, p2].forEach(p => {    ctx.beginPath();    ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);    ctx.fill();  });    ctx.restore();}

8. Fibonacci Retracement (Complex Overlay)

typescript
const FIBONACCI_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0];const FIBONACCI_COLORS = [  '#787B86', '#2962FF', '#2962FF', '#FF6D00',  '#2962FF', '#2962FF', '#787B86',];function renderFibonacci(  ctx: CanvasRenderingContext2D,  highPoint: DataPoint,   // Swing high  lowPoint: DataPoint,    // Swing low  toPixel: (dp: DataPoint) => PixelPoint,  chartWidth: number,) {  const pHigh = toPixel(highPoint);  const pLow = toPixel(lowPoint);  const priceRange = highPoint.price - lowPoint.price;    ctx.save();  ctx.font = '10px Inter, sans-serif';  ctx.textBaseline = 'middle';    FIBONACCI_LEVELS.forEach((level, i) => {    const price = lowPoint.price + priceRange * (1 - level);    const y = toPixel({ time: highPoint.time, price }).y;    const actualY = pLow.y + (pHigh.y - pLow.y) * level;        // Horizontal level line    ctx.strokeStyle = FIBONACCI_COLORS[i];    ctx.lineWidth = level === 0.5 || level === 0.618 ? 1.5 : 0.5;    ctx.setLineDash(level === 0.5 || level === 0 || level === 1 ? [] : [3, 3]);    ctx.globalAlpha = 0.8;        ctx.beginPath();    ctx.moveTo(Math.min(pHigh.x, pLow.x), actualY);    ctx.lineTo(chartWidth, actualY);    ctx.stroke();        // Fill zone between levels    if (i > 0) {      const prevY = pLow.y + (pHigh.y - pLow.y) * FIBONACCI_LEVELS[i - 1];      ctx.fillStyle = `${FIBONACCI_COLORS[i]}08`;  // Very transparent      ctx.fillRect(        Math.min(pHigh.x, pLow.x),        Math.min(prevY, actualY),        chartWidth - Math.min(pHigh.x, pLow.x),        Math.abs(actualY - prevY)      );    }        // Level label    ctx.fillStyle = FIBONACCI_COLORS[i];    ctx.textAlign = 'left';    ctx.globalAlpha = 1.0;    ctx.fillText(      `${(level * 100).toFixed(1)}% (${price.toFixed(2)})`,      Math.min(pHigh.x, pLow.x) + 4,      actualY    );  });    ctx.restore();}

9. Serialization

Overlays should be serializable so they can be saved to the backend and restored when the chart is reopened.

typescript
// Serialize: Convert overlay state to a JSON stringexport function serialize(state: OverlayState): string {  return JSON.stringify({    type: 'supply-demand-zone',    version: '1.0',    points: state.points,    style: state.style,    userData: state.userData,    // Don't serialize interaction state — always starts as 'normal'  });}// Deserialize: Restore overlay from saved JSONexport function deserialize(json: string): OverlayState {  const data = JSON.parse(json);  return {    points: data.points || [],    interaction: 'normal',  // Always reset to normal    style: { ...defaultStyle, ...data.style },    userData: data.userData || {},  };}// Example: Save/load cycleconst state = createInitialState(  [{ time: 1700000000, price: 155 }, { time: 1700100000, price: 150 }],  { labelText: 'Demand Zone', strokeColor: '#26a69a' });const saved = serialize(state);     // Store in backendconst restored = deserialize(saved); // Reload later
!
Caution:Never serialize the interaction state or pixel coordinates. Always serialize coordinates (time/price) so overlays remain correct when the chart is resized or zoomed.

10. Auto-Detection Patterns

You can write overlay scripts that automatically detect and draw chart patterns:

  • Support/Resistance lines — Find price levels where reversals occur frequently
  • Supply/Demand zones — Detect areas of high volume accumulation/
  • Swing highs/lows — Find local maxima/minima for pivot points
  • Chart patterns — , double top/bottom, triangles, wedges
  • highlights — trading session times (Asia, London, New York)
  • Fibonacci auto-levels — Detect swing high/low and draw retracements
  • zones — Draw horizontal volume as overlay
typescript
// Example: Auto-detect support/resistance levelsfunction detectLevels(bars: BarData[], lookback = 20, threshold = 0.02) {  const overlays: OverlayState[] = [];    for (let i = lookback; i < bars.length - lookback; i++) {    const price = bars[i].close;    const window = bars.slice(i - lookback, i + lookback + 1);        const isResistance = price === Math.max(...window.map(b => b.high));    const isSupport = price === Math.min(...window.map(b => b.low));        if (isResistance || isSupport) {      // Check if a similar level already exists      const exists = overlays.some(o =>         Math.abs(o.points[0].price - price) / price < threshold      );            if (!exists) {        overlays.push(createInitialState(          [{ time: bars[i].time, price }],          {            strokeColor: isResistance ? '#ef5350' : '#26a69a',            labelText: isResistance               ? `R ${price.toFixed(2)}`               : `S ${price.toFixed(2)}`,            lineDash: [5, 5],          }        ));      }    }  }    return overlays;}

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.

  • Script Best Practices

    Coding / IDE / Best Practices

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

  • 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…

  • Chart Overlays

    Trading / Quick Trade / Display

    Chart Overlays BUY 42,000 Limit order SELL 41,980 Pending order ◀ Quick Order Line ◀ Quick Trade Line ↕ Drag to adjust

  • Ghost Feed

    Charts / Drawing Tools / Forecasting

    An overlay of another symbol's price data on the current chart. Ghost feeds compare correlated instruments, analyze relative strength, and identify…

  • 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…

  • Line Chart

    Charts / Chart Types / Line Charts

    The simplest and most versatile chart type — closing prices connected by a continuous line, providing an uncluttered view of price trends over time.

  • 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…

Browse all knowledge base articles →