Appearance
Personalize your experience
Overlays
IDEOverlays 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
| Type | Points | Description | Examples |
|---|---|---|---|
| POINT | 1 | Single marker at a time-price coordinate | Pivot point, signal dot, price marker, entry/exit marker |
| LINE | 2 | Line segment between two anchor points | , support/resistance line, regression line |
| SHAPE | 2 | Rectangular or elliptical region defined by two corner | Supply/demand zone, highlight, consolidation box |
| MULTIPOINT | 3+ | or with multiple anchor points | Fibonacci , Andrew's Pitchfork, |
| TEXT | 1 | annotation positioned at a single anchor point | , , trade annotation, watermark |
| COMPLEX | 2+ | Multi-component with lines, shapes, and | Fibonacci with levels + labels + zones, |
3. Anchor Modes
Overlays can be to the chart in three different ways:
| Anchor | Coordinates | Behavior | Use Case |
|---|---|---|---|
| time + price | Moves with chart panning and zooming — always stays at the same price/time | Support/resistance zones, trend lines, price annotations | |
| pixel | x + y pixels | on screen — stays in the same position regardless of chart | Watermarks, legends, info , logos |
| % of /height | Responsive — maintains relative position when chart resizes | Corner badges, scale-independent labels |
1// Data anchor — most common for chart overlays2const supportLine = {3 anchor: 'data',4 points: [5 { time: 1700000000, price: 150.00 }, // Start point6 { time: 1700100000, price: 150.00 }, // End point 7 ],8};910// Pixel anchor — fixed screen position11const watermark = {12 anchor: 'pixel',13 points: [14 { x: 50, y: 30 }, // 50px from left, 30px from top15 ],16};1718// Percentage anchor — responsive19const cornerBadge = {20 anchor: 'percentage',21 points: [22 { x: 0.95, y: 0.05 }, // 95% right, 5% from top23 ],24};4. Overlay Style (IOverlayStyle)
Overlays have a rich style configuration for appearance:
1interface OverlayStyle {2 // === Lines & Borders ===3 strokeColor: string; // Border/line color — any CSS color4 lineWidth: number; // Border width in pixels5 lineDash: number[]; // Dash pattern: [] = solid, [5,5] = dashed,6 // [2,8] = dotted, [10,5,2,5] = dash-dot78 // === Fill ===9 fillColor: string; // Fill color — USE RGBA for transparency!10 // 'rgba(41, 98, 255, 0.1)' = 10% opacity blue1112 // === Text ===13 font: string; // CSS font: '12px Inter, sans-serif'14 textColor: string; // Text color15 showLabel: boolean; // Whether to display a label16 labelText: string; // Label content17}1819// === Style Presets ===2021// Supply zone (bearish)22const supplyStyle: OverlayStyle = {23 strokeColor: '#ef5350',24 fillColor: 'rgba(239, 83, 80, 0.06)',25 lineWidth: 1,26 lineDash: [],27 font: '11px Inter, sans-serif',28 textColor: '#ef5350',29 showLabel: true,30 labelText: 'Supply Zone',31};3233// Demand zone (bullish)34const demandStyle: OverlayStyle = {35 strokeColor: '#26a69a',36 fillColor: 'rgba(38, 166, 154, 0.06)',37 lineWidth: 1,38 lineDash: [],39 font: '11px Inter, sans-serif',40 textColor: '#26a69a',41 showLabel: true,42 labelText: 'Demand Zone',43};4445// Trend line46const trendLineStyle: OverlayStyle = {47 strokeColor: '#2962FF',48 fillColor: 'transparent',49 lineWidth: 2,50 lineDash: [],51 font: '11px Inter, sans-serif',52 textColor: '#2962FF',53 showLabel: false,54 labelText: '',55};5657// Support/Resistance level58const levelStyle: OverlayStyle = {59 strokeColor: '#FF6D00',60 fillColor: 'transparent',61 lineWidth: 1,62 lineDash: [5, 5], // Dashed line63 font: '10px Inter, sans-serif',64 textColor: '#FF6D00',65 showLabel: true,66 labelText: 'S/R Level',67};5. Interaction States
Interactive overlays transition between states based on user interaction:
| State | Triggered By | Visual Feedback | Cursor |
|---|---|---|---|
| normal | Default / mouse leaves | Standard rendering | default |
| hovered | Mouse enters overlay bounds | Slightly brighter fill, increased line | pointer |
| selected | User clicks on overlay | Highlight border (gold), show resize handles | pointer |
| dragging | User drags selected overlay | Reduced (70%), movement | move |
| resizing | User drags a resize handle | Show dimension guides, minimum size enforced | nwse-resize etc. |
| locked | User locks the overlay | Dimmed appearance, no interaction handles | not-allowed |
1// Adjust style based on interaction state2function getInteractionStyle(3 baseStyle: OverlayStyle, 4 interaction: InteractionState5): { strokeColor: string; fillColor: string; lineWidth: number; opacity: number } {6 let { strokeColor, fillColor, lineWidth } = baseStyle;7 let opacity = 1.0;89 switch (interaction) {10 case 'hovered':11 lineWidth += 1;12 // Increase fill opacity slightly13 fillColor = fillColor.replace(/[\d.]+\)$/, '0.15)');14 break;15 case 'selected':16 strokeColor = '#FFD700'; // Gold highlight17 lineWidth += 1;18 break;19 case 'dragging':20 opacity = 0.7;21 break;22 case 'resizing':23 strokeColor = '#FFD700';24 lineWidth = 1;25 break;26 case 'locked':27 opacity = 0.5;28 strokeColor = '#787B86'; // Gray29 break;30 }3132 return { strokeColor, fillColor, lineWidth, opacity };33}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
1function hitTestRect(2 mouseX: number, mouseY: number,3 topLeft: PixelPoint, bottomRight: PixelPoint,4 tolerance: number = 85): boolean {6 return (7 mouseX >= Math.min(topLeft.x, bottomRight.x) - tolerance &&8 mouseX <= Math.max(topLeft.x, bottomRight.x) + tolerance &&9 mouseY >= Math.min(topLeft.y, bottomRight.y) - tolerance &&10 mouseY <= Math.max(topLeft.y, bottomRight.y) + tolerance11 );12}6.2 Line Hit Test (Point-to-Line Distance)
1function hitTestLine(2 mouseX: number, mouseY: number,3 lineStart: PixelPoint, lineEnd: PixelPoint,4 tolerance: number = 65): boolean {6 const dx = lineEnd.x - lineStart.x;7 const dy = lineEnd.y - lineStart.y;8 const lengthSq = dx * dx + dy * dy;910 if (lengthSq === 0) {11 // Line is a point12 const dist = Math.sqrt(13 Math.pow(mouseX - lineStart.x, 2) + Math.pow(mouseY - lineStart.y, 2)14 );15 return dist <= tolerance;16 }1718 // Project mouse point onto line segment19 let t = ((mouseX - lineStart.x) * dx + (mouseY - lineStart.y) * dy) / lengthSq;20 t = Math.max(0, Math.min(1, t)); // Clamp to segment2122 const projX = lineStart.x + t * dx;23 const projY = lineStart.y + t * dy;24 const distance = Math.sqrt(Math.pow(mouseX - projX, 2) + Math.pow(mouseY - projY, 2));2526 return distance <= tolerance;27}6.3 Ellipse Hit Test
1function hitTestEllipse(2 mouseX: number, mouseY: number,3 center: PixelPoint, radiusX: number, radiusY: number,4 tolerance: number = 65): boolean {6 // Point-in-ellipse test: (x/a)² + (y/b)² <= 17 const dx = (mouseX - center.x) / (radiusX + tolerance);8 const dy = (mouseY - center.y) / (radiusY + tolerance);9 return (dx * dx + dy * dy) <= 1;10}7.1 Horizontal Price Level
1function renderHorizontalLine(2 ctx: CanvasRenderingContext2D,3 price: number,4 toPixelY: (p: number) => number,5 chartWidth: number,6 style: OverlayStyle,7) {8 const y = toPixelY(price);9 10 ctx.save();11 ctx.strokeStyle = style.strokeColor;12 ctx.lineWidth = style.lineWidth;13 ctx.setLineDash(style.lineDash);14 15 // Draw the line across the full chart width16 ctx.beginPath();17 ctx.moveTo(0, y);18 ctx.lineTo(chartWidth, y);19 ctx.stroke();20 21 // Draw price label22 if (style.showLabel) {23 const text = style.labelText || price.toFixed(2);24 ctx.font = style.font;25 const metrics = ctx.measureText(text);26 const padding = 4;27 const labelWidth = metrics.width + padding * 2;28 const labelHeight = 16;29 30 // Label background31 ctx.fillStyle = style.strokeColor;32 ctx.fillRect(33 chartWidth - labelWidth - 4,34 y - labelHeight / 2,35 labelWidth,36 labelHeight37 );38 39 // Label text40 ctx.fillStyle = '#ffffff';41 ctx.textBaseline = 'middle';42 ctx.textAlign = 'right';43 ctx.fillText(text, chartWidth - 8, y);44 }45 46 ctx.restore();47}7.2 Supply/Demand Zone (Rectangle)
1function renderZone(2 ctx: CanvasRenderingContext2D,3 state: OverlayState,4 toPixel: (dp: DataPoint) => PixelPoint,5 config: OverlayConfig,6) {7 if (state.points.length < 2) return;8 9 const p1 = toPixel(state.points[0]);10 const p2 = toPixel(state.points[1]);11 const { style, interaction } = state;12 13 ctx.save();14 const iStyle = getInteractionStyle(style, interaction);15 ctx.globalAlpha = iStyle.opacity;16 17 const x = Math.min(p1.x, p2.x);18 const y = Math.min(p1.y, p2.y);19 const w = Math.abs(p2.x - p1.x);20 const h = Math.abs(p2.y - p1.y);21 22 // Fill23 ctx.fillStyle = iStyle.fillColor;24 ctx.fillRect(x, y, w, h);25 26 // Border27 ctx.strokeStyle = iStyle.strokeColor;28 ctx.lineWidth = iStyle.lineWidth;29 ctx.setLineDash(style.lineDash);30 ctx.strokeRect(x, y, w, h);31 32 // Top edge highlight (stronger line at zone boundary)33 ctx.lineWidth = style.lineWidth + 1;34 ctx.setLineDash([]);35 ctx.beginPath();36 ctx.moveTo(x, y); // Top-left37 ctx.lineTo(x + w, y); // Top-right38 ctx.stroke();39 40 // Label41 if (style.showLabel) {42 ctx.font = style.font;43 ctx.fillStyle = style.textColor;44 ctx.globalAlpha = 0.8;45 ctx.textBaseline = 'top';46 ctx.textAlign = 'left';47 ctx.fillText(style.labelText, x + 6, y + 4);48 }49 50 // Resize handles when selected51 if (interaction === 'selected' || interaction === 'resizing') {52 ctx.fillStyle = iStyle.strokeColor;53 const hs = 5;54 [[x, y], [x+w, y], [x, y+h], [x+w, y+h]].forEach(([cx, cy]) => {55 ctx.fillRect(cx - hs/2, cy - hs/2, hs, hs);56 });57 }58 59 ctx.restore();60}7.3 Trend Line with Extension
1function renderTrendLine(2 ctx: CanvasRenderingContext2D,3 state: OverlayState,4 toPixel: (dp: DataPoint) => PixelPoint,5 chartWidth: number,6 extend: boolean = true,7) {8 if (state.points.length < 2) return;9 10 const p1 = toPixel(state.points[0]);11 const p2 = toPixel(state.points[1]);12 13 ctx.save();14 ctx.strokeStyle = state.style.strokeColor;15 ctx.lineWidth = state.style.lineWidth;16 ctx.setLineDash(state.style.lineDash);17 ctx.lineJoin = 'round';18 ctx.lineCap = 'round';19 20 // Draw main line segment21 ctx.beginPath();22 ctx.moveTo(p1.x, p1.y);23 ctx.lineTo(p2.x, p2.y);24 ctx.stroke();25 26 // Extend line to chart edge27 if (extend) {28 const dx = p2.x - p1.x;29 const dy = p2.y - p1.y;30 31 if (dx !== 0) {32 const slope = dy / dx;33 const extendX = chartWidth;34 const extendY = p2.y + slope * (extendX - p2.x);35 36 ctx.globalAlpha = 0.3; // Dimmer for extension37 ctx.setLineDash([5, 5]);38 ctx.beginPath();39 ctx.moveTo(p2.x, p2.y);40 ctx.lineTo(extendX, extendY);41 ctx.stroke();42 }43 }44 45 // Draw anchor points46 ctx.globalAlpha = 1.0;47 ctx.setLineDash([]);48 ctx.fillStyle = state.style.strokeColor;49 [p1, p2].forEach(p => {50 ctx.beginPath();51 ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);52 ctx.fill();53 });54 55 ctx.restore();56}8. Fibonacci Retracement (Complex Overlay)
1const FIBONACCI_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1.0];2const FIBONACCI_COLORS = [3 '#787B86', '#2962FF', '#2962FF', '#FF6D00',4 '#2962FF', '#2962FF', '#787B86',5];67function renderFibonacci(8 ctx: CanvasRenderingContext2D,9 highPoint: DataPoint, // Swing high10 lowPoint: DataPoint, // Swing low11 toPixel: (dp: DataPoint) => PixelPoint,12 chartWidth: number,13) {14 const pHigh = toPixel(highPoint);15 const pLow = toPixel(lowPoint);16 const priceRange = highPoint.price - lowPoint.price;17 18 ctx.save();19 ctx.font = '10px Inter, sans-serif';20 ctx.textBaseline = 'middle';21 22 FIBONACCI_LEVELS.forEach((level, i) => {23 const price = lowPoint.price + priceRange * (1 - level);24 const y = toPixel({ time: highPoint.time, price }).y;25 const actualY = pLow.y + (pHigh.y - pLow.y) * level;26 27 // Horizontal level line28 ctx.strokeStyle = FIBONACCI_COLORS[i];29 ctx.lineWidth = level === 0.5 || level === 0.618 ? 1.5 : 0.5;30 ctx.setLineDash(level === 0.5 || level === 0 || level === 1 ? [] : [3, 3]);31 ctx.globalAlpha = 0.8;32 33 ctx.beginPath();34 ctx.moveTo(Math.min(pHigh.x, pLow.x), actualY);35 ctx.lineTo(chartWidth, actualY);36 ctx.stroke();37 38 // Fill zone between levels39 if (i > 0) {40 const prevY = pLow.y + (pHigh.y - pLow.y) * FIBONACCI_LEVELS[i - 1];41 ctx.fillStyle = `${FIBONACCI_COLORS[i]}08`; // Very transparent42 ctx.fillRect(43 Math.min(pHigh.x, pLow.x),44 Math.min(prevY, actualY),45 chartWidth - Math.min(pHigh.x, pLow.x),46 Math.abs(actualY - prevY)47 );48 }49 50 // Level label51 ctx.fillStyle = FIBONACCI_COLORS[i];52 ctx.textAlign = 'left';53 ctx.globalAlpha = 1.0;54 ctx.fillText(55 `${(level * 100).toFixed(1)}% (${price.toFixed(2)})`,56 Math.min(pHigh.x, pLow.x) + 4,57 actualY58 );59 });60 61 ctx.restore();62}9. Serialization
Overlays should be serializable so they can be saved to the backend and restored when the chart is reopened.
1// Serialize: Convert overlay state to a JSON string2export function serialize(state: OverlayState): string {3 return JSON.stringify({4 type: 'supply-demand-zone',5 version: '1.0',6 points: state.points,7 style: state.style,8 userData: state.userData,9 // Don't serialize interaction state — always starts as 'normal'10 });11}1213// Deserialize: Restore overlay from saved JSON14export function deserialize(json: string): OverlayState {15 const data = JSON.parse(json);16 return {17 points: data.points || [],18 interaction: 'normal', // Always reset to normal19 style: { ...defaultStyle, ...data.style },20 userData: data.userData || {},21 };22}2324// Example: Save/load cycle25const state = createInitialState(26 [{ time: 1700000000, price: 155 }, { time: 1700100000, price: 150 }],27 { labelText: 'Demand Zone', strokeColor: '#26a69a' }28);2930const saved = serialize(state); // Store in backend31const restored = deserialize(saved); // Reload later10. 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
1// Example: Auto-detect support/resistance levels2function detectLevels(bars: BarData[], lookback = 20, threshold = 0.02) {3 const overlays: OverlayState[] = [];4 5 for (let i = lookback; i < bars.length - lookback; i++) {6 const price = bars[i].close;7 const window = bars.slice(i - lookback, i + lookback + 1);8 9 const isResistance = price === Math.max(...window.map(b => b.high));10 const isSupport = price === Math.min(...window.map(b => b.low));11 12 if (isResistance || isSupport) {13 // Check if a similar level already exists14 const exists = overlays.some(o => 15 Math.abs(o.points[0].price - price) / price < threshold16 );17 18 if (!exists) {19 overlays.push(createInitialState(20 [{ time: bars[i].time, price }],21 {22 strokeColor: isResistance ? '#ef5350' : '#26a69a',23 labelText: isResistance 24 ? `R ${price.toFixed(2)}` 25 : `S ${price.toFixed(2)}`,26 lineDash: [5, 5],27 }28 ));29 }30 }31 }32 33 return overlays;34}Related articles
Related to Writing Overlays 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.
- 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…