# SXVNT Script SDK — Complete Reference Every script running on the SXVNT charting engine receives a `ctx` object exposing price data, 20+ technical indicators, plotting, strategy execution, and live trading APIs. This document lists every function available, with signatures, parameters, return types, and runnable examples. **For AI assistants:** paste this entire file into Claude, ChatGPT, or any other AI tool and ask it to build a custom indicator or strategy for the SXVNT platform. Output should be plain JavaScript using only the `ctx.*` APIs documented below. Total functions documented: **78** --- ## Table of contents - **Inputs** (6) - `ctx.input` - `ctx.input.time` - `ctx.input.price` - `ctx.input.point` - `ctx.input.points` - `ctx.log` - **Price** (2) - `ctx.price` - `ctx.price.last` - **Technical Analysis** (13) - `ctx.ta.sma` - `ctx.ta.ema` - `ctx.ta.wma` - `ctx.ta.vwma` - `ctx.ta.hull` - `ctx.ta.rsi` - `ctx.ta.macd` - `ctx.ta.stoch` - `ctx.ta.bb` - `ctx.ta.atr` - `ctx.ta.supertrend` - `ctx.ta.crossover` - `ctx.ta.crossunder` - **Time & Sessions** (12) - `ctx.time.isAsianKZ` - `ctx.time.isLondonKZ` - `ctx.time.isNewYorkKZ` - `ctx.time.inSession` - `ctx.time.hour / minute / dayOfWeek` - `ctx.time.hourIn` - `ctx.time.minuteIn` - `Time Filtering Pattern (Advanced)` - `Kill Zone FVG Detection (Complete Example)` - `IFVG Sweep Detection (Complete Example)` - `ctx.request` - `HTF Power of 3 (Example)` - **Plotting** (14) - `ctx.plot` - `ctx.fill` - `ctx.shape` - `ctx.bubble` - `ctx.box` - `ctx.line` - `ctx.label` - `ctx.bgcolor` - `ctx.barcolor` - `ctx.hline` - `ctx.histogram` - `ctx.table` - `ctx.cell` - `ctx.htfCandles` - **Strategy** (11) - `ctx.strategy.entry` - `ctx.strategy.close` - `ctx.strategy.position` - `ctx.strategy.flattenAll` - `ctx.strategy.account` - `ctx.strategy.history` - `ctx.strategy.cancel` - `ctx.strategy.modifyStop` - `ctx.strategy.modifyTP` - `ctx.strategy.setTrail` - `ctx.strategy.riskLimits` - **Live Trading** (9) - `Live Strategy Overview` - `Bracket Orders` - `Supported Order Types` - `Emergency Flatten` - `Input Overrides & Script Settings` - `Live Execution Safety` - `ctx.state` - `Live-only inputs (liveOnly)` - `OCO (One-Cancels-Other) Lifecycle` - **Math** (1) - `ctx.math` - **Order Flow** (7) - `ctx.footprint` - `ctx.footprint.bar` - `ctx.footprint.delta` - `ctx.footprint.cumulativeDelta` - `ctx.footprint.poc` - `ctx.footprint.bidVolume / askVolume / totalVolume` - `ctx.footprint.pocVolume` - **Alerts** (3) - `ctx.alerts.last` - `ctx.alerts.lastReason` - `ctx.alerts.recent` --- ## Inputs ### ctx.input **Signature:** `ctx.input(label, defaultValue, opts?)` Declares a user-configurable input. Appears in the Script Settings modal. Returns the current value (user-modified or default). **Parameters:** - `label` *(string)* — Display label in settings UI - `defaultValue` *(number | string | boolean)* — Default value. Type is inferred: number→slider, boolean→checkbox, "#hex"→color, string→text - `opts?` *(InputOptions)* — Optional: { min, max, step, options } **Returns:** The current value of the input **Example:** ```js const period = ctx.input('Period', 14, { min: 1, max: 200 }); const color = ctx.input('Color', '#2962ff'); ``` ### ctx.input.time **Signature:** `ctx.input.time(label, opts?) → number | null` Click-to-place anchor — collects a single TIME value (epoch ms) from the chart. The Script Settings modal shows a "Pick on chart" button; the next chart click captures the bar timestamp and stores it. Returns null until picked. Anchors persist as timestamps so they remain stable as new bars print and as you scroll deep history. **Parameters:** - `label` *(string)* — Display label in settings UI - `opts?` *({ default?: number })* — Optional default timestamp (epoch ms) **Returns:** number (epoch ms) | null if not yet picked **Example:** ```js // Anchored VWAP — user clicks the chart to set the anchor bar const anchor = ctx.input.time('Anchor'); if (!anchor) return; // waiting for user to pick const start = bars.findIndex(b => +new Date(b.timestamp) >= anchor); let pv = 0, v = 0; const out = new Array(bars.length).fill(null); for (let i = start; i < bars.length; i++) { pv += ctx.price.hlc3[i] * ctx.price.volume[i]; v += ctx.price.volume[i]; out[i] = pv / v; } ctx.plot(out, 'Anchored VWAP', { color: '#fa0' }); ``` ### ctx.input.price **Signature:** `ctx.input.price(label, opts?) → number | null` Click-to-place anchor — collects a single PRICE value from the chart. Modal shows "Pick on chart"; the next chart click captures the price at the click Y coordinate (snapped to candle OHLC if magnet mode is on). Returns null until picked. **Parameters:** - `label` *(string)* — Display label in settings UI - `opts?` *({ default?: number })* — Optional default price **Returns:** number | null if not yet picked **Example:** ```js // Custom horizontal level — user clicks any price on the chart const level = ctx.input.price('Level'); if (level == null) return; ctx.hline(level, { color: '#0fa', label: `Level ${level.toFixed(2)}` }); ``` ### ctx.input.point **Signature:** `ctx.input.point(label, opts?) → PointValue | null` Click-to-place anchor — collects a single (TIME, PRICE) point. Returns { timestamp, price, barIndex } where barIndex is resolved at execution time (never persisted, so it stays correct as new bars print). Returns null until picked. **Parameters:** - `label` *(string)* — Display label in settings UI - `opts?` *({ default?: PointValue })* — Optional default { timestamp, price } **Returns:** { timestamp: number, price: number, barIndex: number } | null **Example:** ```js // Single-anchor label placement const anchor = ctx.input.point('Marker'); if (!anchor) return; ctx.label(anchor.barIndex, anchor.price, '★ marker', { color: '#fa0', textColor: '#000' }); ``` ### ctx.input.points **Signature:** `ctx.input.points(label, { count, default? }) → PointValue[] | null` Click-to-place anchors — collects N (TIME, PRICE) points sequenced one click at a time ("Pick 1 of 3", "Pick 2 of 3", ...). Use for ranges (count=2), fib extensions (count=3), elliott waves (count=5), polylines, etc. Returns null until ALL points are picked. Each click sets a deterministic input value — the script re-runs as a pure function of (bars, inputs), so click placement never causes repaint. **Parameters:** - `label` *(string)* — Display label in settings UI - `opts.count` *(number)* — Number of points to collect (>= 1) - `opts.default?` *(PointValue[])* — Optional default array **Returns:** Array<{ timestamp, price, barIndex }> of length count, or null until fully picked **Example:** ```js // SESSION VOLUME PROFILE — click two bars to define the range function calculate(bars, ctx) { const range = ctx.input.points('Profile Range', { count: 2 }); if (!range) return; // waiting for user to pick both anchors const [a, b] = range; const lo = Math.min(a.barIndex, b.barIndex); const hi = Math.max(a.barIndex, b.barIndex); // Aggregate footprint levels across the range const buckets = new Map(); let totalTop = -Infinity, totalBot = Infinity; for (let i = lo; i <= hi; i++) { const fp = ctx.footprint.bar(i); if (!fp) continue; for (const lvl of fp.levels) { buckets.set(lvl.price, (buckets.get(lvl.price) ?? 0) + lvl.totalVolume); if (lvl.price > totalTop) totalTop = lvl.price; if (lvl.price < totalBot) totalBot = lvl.price; } } if (buckets.size === 0) return; // Find POC (price with highest volume) let poc = 0, pocVol = 0; for (const [p, v] of buckets) if (v > pocVol) { pocVol = v; poc = p; } // Draw range outline + POC line — both use absolute (bar, price) anchors ctx.box(lo, totalTop, hi, totalBot, { borderColor: '#888', fillColor: 'rgba(255,255,255,0.04)', borderStyle: 'dashed', }); ctx.line(lo, poc, hi, poc, { color: '#ff0', lineWidth: 2, extend: 'right' }); ctx.label(hi, poc, ` POC ${poc.toFixed(2)} `, { color: '#ff0', textColor: '#000' }); } ``` ### ctx.log **Signature:** `ctx.log(...args)` Logs a message to the script debug output. **Parameters:** - `args` *(any[])* — Values to log **Returns:** void **Example:** ```js ctx.log('RSI:', rsi[last], 'Position:', ctx.strategy.position.side); ``` --- ## Price ### ctx.price **Signature:** `ctx.price.close / .open / .high / .low / .volume / .hl2 / .hlc3 / .ohlc4 / .last` Pre-extracted price arrays. Each array is a number[] with one value per bar. ctx.price.last is a single number — the real-time tick price (last trade) from the market data feed, updated every tick. Use .last for live strategies that need the actual current price rather than the prior bar close. **Returns:** number[] (arrays) / number (ctx.price.last) **Example:** ```js const sma = ctx.ta.sma(ctx.price.close, 20); const typical = ctx.price.hlc3; // Real-time price for live strategies const livePrice = ctx.price.last; const buyStop = livePrice + 5; // 5 pts above current price ``` ### ctx.price.last **Signature:** `ctx.price.last` The real-time last trade price from the market data feed. Unlike ctx.price.close[last] which is the close of the most recent completed bar (could be up to 59 seconds old on a 1m chart), ctx.price.last is the actual current tick price. In backtest mode, falls back to the last bar close. Essential for OCO scalpers and any strategy that places orders relative to the current price. **Returns:** number **Example:** ```js const price = ctx.price.last; // actual live tick price const tickSize = ctx.syminfo.tickSize; // Place OCO stops 5 points above/below current price const buyPrice = price + 5; const sellPrice = price - 5; ctx.strategy.entry('oco_long', 'long', { type: 'stop', price: buyPrice, qty: 1 }); ctx.strategy.entry('oco_short', 'short', { type: 'stop', price: sellPrice, qty: 1 }); ``` --- ## Technical Analysis ### ctx.ta.sma **Signature:** `ctx.ta.sma(source, period)` Simple Moving Average. Padded with nulls at the start. **Parameters:** - `source` *(number[])* — Price array (e.g., ctx.price.close) - `period` *(number)* — Lookback period **Returns:** (number | null)[] **Example:** ```js const sma20 = ctx.ta.sma(ctx.price.close, 20); ``` ### ctx.ta.ema **Signature:** `ctx.ta.ema(source, period)` Exponential Moving Average. More weight on recent prices. **Parameters:** - `source` *(number[])* — Price array - `period` *(number)* — Lookback period **Returns:** (number | null)[] **Example:** ```js const ema9 = ctx.ta.ema(ctx.price.close, 9); ``` ### ctx.ta.wma **Signature:** `ctx.ta.wma(source, period)` Weighted Moving Average. **Parameters:** - `source` *(number[])* — Price array - `period` *(number)* — Lookback period **Returns:** (number | null)[] **Example:** ```js const wma = ctx.ta.wma(ctx.price.close, 14); ``` ### ctx.ta.vwma **Signature:** `ctx.ta.vwma(source, period)` Volume Weighted Moving Average. **Parameters:** - `source` *(number[])* — Price array - `period` *(number)* — Lookback period **Returns:** (number | null)[] **Example:** ```js const vwma = ctx.ta.vwma(ctx.price.close, 20); ``` ### ctx.ta.hull **Signature:** `ctx.ta.hull(source, period)` Hull Moving Average. Reduced lag compared to SMA/EMA. **Parameters:** - `source` *(number[])* — Price array - `period` *(number)* — Lookback period **Returns:** (number | null)[] **Example:** ```js const hull = ctx.ta.hull(ctx.price.close, 16); ``` ### ctx.ta.rsi **Signature:** `ctx.ta.rsi(source, period)` Relative Strength Index. Range 0–100. **Parameters:** - `source` *(number[])* — Price array - `period` *(number)* — Lookback period (default 14) **Returns:** (number | null)[] **Example:** ```js const rsi = ctx.ta.rsi(ctx.price.close, 14); ctx.plot(rsi, 'RSI', { pane: 'separate' }); ``` ### ctx.ta.macd **Signature:** `ctx.ta.macd(source, fastPeriod, slowPeriod, signalPeriod)` Moving Average Convergence Divergence. Returns { macd, signal, histogram }. **Parameters:** - `source` *(number[])* — Price array - `fastPeriod` *(number)* — Fast EMA period (12) - `slowPeriod` *(number)* — Slow EMA period (26) - `signalPeriod` *(number)* — Signal line period (9) **Returns:** { macd, signal, histogram } — each (number | null)[] **Example:** ```js const m = ctx.ta.macd(ctx.price.close, 12, 26, 9); ctx.plot(m.macd, 'MACD', { pane: 'separate', color: '#2196f3' }); ``` ### ctx.ta.stoch **Signature:** `ctx.ta.stoch(high, low, close, kPeriod, kSmooth, dSmooth)` Stochastic Oscillator. Returns { k, d }. **Parameters:** - `high` *(number[])* — High prices - `low` *(number[])* — Low prices - `close` *(number[])* — Close prices - `kPeriod` *(number)* — %K period - `kSmooth` *(number)* — %K smoothing - `dSmooth` *(number)* — %D smoothing **Returns:** { k, d } **Example:** ```js const s = ctx.ta.stoch(ctx.price.high, ctx.price.low, ctx.price.close, 14, 3, 3); ``` ### ctx.ta.bb **Signature:** `ctx.ta.bb(source, period, stdDev)` Bollinger Bands. Returns { upper, middle, lower }. **Parameters:** - `source` *(number[])* — Price array - `period` *(number)* — SMA period (20) - `stdDev` *(number)* — Standard deviation multiplier (2) **Returns:** { upper, middle, lower } **Example:** ```js const bb = ctx.ta.bb(ctx.price.close, 20, 2); ctx.plot(bb.upper, 'Upper'); ctx.plot(bb.lower, 'Lower'); ``` ### ctx.ta.atr **Signature:** `ctx.ta.atr(high, low, close, period)` Average True Range. **Parameters:** - `high` *(number[])* — High prices - `low` *(number[])* — Low prices - `close` *(number[])* — Close prices - `period` *(number)* — ATR period (14) **Returns:** (number | null)[] **Example:** ```js const atr = ctx.ta.atr(ctx.price.high, ctx.price.low, ctx.price.close, 14); ``` ### ctx.ta.supertrend **Signature:** `ctx.ta.supertrend(high, low, close, period, multiplier)` SuperTrend indicator. Returns { trend, direction }. direction: 1=up, -1=down. **Parameters:** - `high` *(number[])* — High prices - `low` *(number[])* — Low prices - `close` *(number[])* — Close prices - `period` *(number)* — ATR period - `multiplier` *(number)* — ATR multiplier **Returns:** { trend: (number|null)[], direction: (number|null)[] } **Example:** ```js const st = ctx.ta.supertrend(ctx.price.high, ctx.price.low, ctx.price.close, 10, 3); ``` ### ctx.ta.crossover **Signature:** `ctx.ta.crossover(source1, source2)` Returns boolean[] where true means source1 crossed above source2 at that bar. **Parameters:** - `source1` *((number|null)[])* — First series - `source2` *((number|null)[])* — Second series **Returns:** boolean[] **Example:** ```js const cross = ctx.ta.crossover(fast, slow); if (cross[bars.length - 1]) { /* buy signal */ } ``` ### ctx.ta.crossunder **Signature:** `ctx.ta.crossunder(source1, source2)` Returns boolean[] where true means source1 crossed below source2 at that bar. **Parameters:** - `source1` *((number|null)[])* — First series - `source2` *((number|null)[])* — Second series **Returns:** boolean[] **Example:** ```js const crossDn = ctx.ta.crossunder(fast, slow); ``` --- ## Time & Sessions ### ctx.time.isAsianKZ **Signature:** `ctx.time.isAsianKZ(barIndex, startET?, endET?)` Returns true if bar is within the Asian Kill Zone. Default 8pm–12am ET. Pass custom hours to override. **Parameters:** - `barIndex` *(number)* — Index of the bar - `startET?` *(number)* — Custom start hour in ET (0-23) - `endET?` *(number)* — Custom end hour in ET (0-23) **Returns:** boolean **Example:** ```js const start = ctx.input('Asian Start', 20); ctx.time.isAsianKZ(i, start, 0) ``` ### ctx.time.isLondonKZ **Signature:** `ctx.time.isLondonKZ(barIndex, startET?, endET?)` Returns true if bar is within the London Kill Zone. Default 2am–5am ET. Pass custom hours to override. **Parameters:** - `barIndex` *(number)* — Index of the bar - `startET?` *(number)* — Custom start hour in ET (0-23) - `endET?` *(number)* — Custom end hour in ET (0-23) **Returns:** boolean **Example:** ```js const start = ctx.input('London Start', 2); ctx.time.isLondonKZ(i, start, 5) ``` ### ctx.time.isNewYorkKZ **Signature:** `ctx.time.isNewYorkKZ(barIndex, startET?, endET?)` Returns true if bar is within the New York Kill Zone. Default 7am–10am ET. Pass custom hours to override. **Parameters:** - `barIndex` *(number)* — Index of the bar - `startET?` *(number)* — Custom start hour in ET (0-23) - `endET?` *(number)* — Custom end hour in ET (0-23) **Returns:** boolean **Example:** ```js const start = ctx.input('NY Start', 7); ctx.time.isNewYorkKZ(i, start, 10) ``` ### ctx.time.inSession **Signature:** `ctx.time.inSession(barIndex, startHour, endHour, timezone?)` Check if bar falls within a custom time window. Handles midnight wrapping. **Parameters:** - `barIndex` *(number)* — Index of the bar - `startHour` *(number)* — Start hour (0-23) - `endHour` *(number)* — End hour (0-23) - `timezone?` *(string)* — IANA timezone (default: America/New_York) **Returns:** boolean **Example:** ```js ctx.time.inSession(i, 9, 16, 'America/Chicago') ``` ### ctx.time.hour / minute / dayOfWeek **Signature:** `ctx.time.hour(i) / ctx.time.minute(i) / ctx.time.dayOfWeek(i)` Get UTC hour (0-23), minute (0-59), or day of week (0=Sun, 6=Sat) for a bar. **Parameters:** - `barIndex` *(number)* — Index of the bar **Returns:** number **Example:** ```js const hour = ctx.time.hour(i); ``` ### ctx.time.hourIn **Signature:** `ctx.time.hourIn(barIndex, timezone)` Get the hour (0-23) of a bar in a specific IANA timezone. Unlike ctx.time.hour() which returns UTC, this converts to any timezone. **Parameters:** - `barIndex` *(number)* — Index of the bar - `timezone` *(string)* — IANA timezone string, e.g. 'America/New_York', 'America/Chicago', 'UTC' **Returns:** number (0-23) **Example:** ```js const nyHour = ctx.time.hourIn(i, 'America/New_York'); ``` ### ctx.time.minuteIn **Signature:** `ctx.time.minuteIn(barIndex, timezone)` Get the minute (0-59) of a bar in a specific IANA timezone. Use with hourIn for precise time-of-day filtering. **Parameters:** - `barIndex` *(number)* — Index of the bar - `timezone` *(string)* — IANA timezone string, e.g. 'America/Los_Angeles', 'America/New_York', 'UTC' **Returns:** number (0-59) **Example:** ```js // Only run logic between 6:30 AM and 12:00 PM Pacific const h = ctx.time.hourIn(i, 'America/Los_Angeles'); const m = ctx.time.minuteIn(i, 'America/Los_Angeles'); const minuteOfDay = h * 60 + m; if (minuteOfDay >= 390 && minuteOfDay < 720) { // 6:30 AM - 12:00 PM PST } ``` ### Time Filtering Pattern (Advanced) **Signature:** `Combined session + kill zone + day filtering` Advanced pattern for filtering indicator signals by time window, kill zone, and day of week. Use ctx.input() to let users configure session hours, active days, and max lookback. Commonly used in ICT-style indicators to only show FVGs, order blocks, or signals during specific trading sessions. **Parameters:** - `sessionStartHour` *(number)* — Session start hour (0-23 ET), e.g. 18 for 6pm - `sessionEndHour` *(number)* — Session end hour (0-23 ET), e.g. 17 for 5pm - `killZone` *(string)* — Optional kill zone filter: "asian", "london", "newyork", or "none" - `activeDays` *(string)* — Comma-separated active days: "1,2,3,4,5" for Mon-Fri (0=Sun) - `maxLookbackDays` *(number)* — Maximum number of trading days to look back **Returns:** Boolean per bar indicating if it passes all time filters **Example:** ```js // Advanced time filtering — restrict signals to specific sessions and days const sessionStart = ctx.input('Session Start (ET)', 18, { min: 0, max: 23 }); const sessionEnd = ctx.input('Session End (ET)', 17, { min: 0, max: 23 }); const useKillZone = ctx.input('Kill Zone Filter', 'none', { options: ['none', 'asian', 'london', 'newyork'] }); const activeDays = ctx.input('Active Days (0=Sun)', '1,2,3,4,5'); const maxDays = ctx.input('Max Lookback Days', 5, { min: 1, max: 20 }); // Parse active days into a Set for fast lookup const daySet = new Set(activeDays.split(',').map(d => parseInt(d.trim()))); // Helper: check if bar is within session range (handles midnight wrap) function inSessionRange(barIndex) { const hour = ctx.time.hour(barIndex); if (sessionStart <= sessionEnd) { return hour >= sessionStart && hour < sessionEnd; } // Wraps midnight (e.g., 18:00 - 17:00 = almost 24h, or 20:00 - 05:00) return hour >= sessionStart || hour < sessionEnd; } // Count lookback days using session boundaries let dayCount = 0; let lookbackLimit = 0; for (let i = ctx.barCount - 1; i >= 0; i--) { const h = ctx.time.hour(i); const prevH = i > 0 ? ctx.time.hour(i - 1) : h; // Detect new session when hour crosses session start if (h >= sessionStart && prevH < sessionStart) dayCount++; if (dayCount > maxDays) { lookbackLimit = i; break; } } // Apply all filters per bar for (let i = lookbackLimit; i < ctx.barCount; i++) { const dow = ctx.time.dayOfWeek(i); if (!daySet.has(dow)) continue; // Skip inactive days if (!inSessionRange(i)) continue; // Skip out-of-session bars // Optional kill zone filter if (useKillZone === 'asian' && !ctx.time.isAsianKZ(i)) continue; if (useKillZone === 'london' && !ctx.time.isLondonKZ(i)) continue; if (useKillZone === 'newyork' && !ctx.time.isNewYorkKZ(i)) continue; // --- Your signal logic here --- // e.g., detect FVGs, order blocks, or crossovers only within filtered bars } ``` ### Kill Zone FVG Detection (Complete Example) **Signature:** `FVGs within kill zones with session tracking, strictness, and unfilled filtering` COMPLETE REFERENCE: Detects Fair Value Gaps strictly within a kill zone time window. Tracks session boundaries to count FVGs per session and limit them. Filters by direction (bullish/bearish/both), minimum gap size in ticks, and whether the FVG has been filled. Uses ctx.time.inSession() for strict time checking on all 3 bars of the FVG pattern. Renders with ctx.box() using extend:right. This is the canonical pattern for any indicator that needs to detect price patterns ONLY within specific time windows. **Parameters:** - `kzStart` *(number)* — Kill zone start hour (ET), e.g. 10 for 10am - `kzEnd` *(number)* — Kill zone end hour (ET), e.g. 11 for 11am - `minGapTicks` *(number)* — Minimum gap size in ticks to qualify as FVG - `maxFVGs` *(number)* — Max FVGs per session (0=unlimited) - `unfilledOnly` *(boolean)* — If true, only show FVGs not yet filled by later price action - `direction` *(number)* — 0=both, 1=bullish only, 2=bearish only **Returns:** Rendered FVG boxes on chart within kill zone times only **Example:** ```js // ICT Silver Bullet (FVG) — Complete kill zone time-filtered indicator function calculate(bars, ctx) { const kzStart = ctx.input('KZ Start (ET)', 10, { min: 0, max: 23, step: 1 }); const kzEnd = ctx.input('KZ End (ET)', 11, { min: 0, max: 23, step: 1 }); const minGapTicks = ctx.input('Min Gap (ticks)', 4, { min: 0, max: 100, step: 1 }); const maxFVGs = ctx.input('Max FVGs Per Session', 20, { min: 0, max: 200, step: 1 }); const unfilledOnly = ctx.input('Unfilled Only', true); const direction = ctx.input('Direction (0=both 1=bull 2=bear)', 0, { min: 0, max: 2, step: 1 }); const bullColor = ctx.input('Bullish Color', '#26a69a'); const bearColor = ctx.input('Bearish Color', '#ef5350'); const showLabels = ctx.input('Show Labels', true); const tickSize = 0.25; const fvgs = []; // Track session boundaries — reset FVG count when entering new kill zone let inKZ = false; let sessionFVGCount = 0; for (let i = 2; i < bars.length; i++) { const currentInKZ = ctx.time.inSession(i, kzStart, kzEnd); // Detect new session start — reset counter if (currentInKZ && !inKZ) { sessionFVGCount = 0; } inKZ = currentInKZ; if (!currentInKZ) continue; // STRICT: All 3 bars of FVG pattern must be within the kill zone if (!ctx.time.inSession(i - 1, kzStart, kzEnd)) continue; if (!ctx.time.inSession(i - 2, kzStart, kzEnd)) continue; // Session cap reached — skip if (maxFVGs > 0 && sessionFVGCount >= maxFVGs) continue; // Bullish FVG: bar[i].low > bar[i-2].high (gap up) if (direction !== 2 && bars[i].low > bars[i - 2].high) { const gapSize = bars[i].low - bars[i - 2].high; if (gapSize / tickSize < minGapTicks) continue; fvgs.push({ type: 'bull', start: i - 2, end: i, top: bars[i].low, bottom: bars[i - 2].high }); sessionFVGCount++; continue; } // Bearish FVG: bar[i].high < bar[i-2].low (gap down) if (direction !== 1 && bars[i].high < bars[i - 2].low) { const gapSize = bars[i - 2].low - bars[i].high; if (gapSize / tickSize < minGapTicks) continue; fvgs.push({ type: 'bear', start: i - 2, end: i, top: bars[i - 2].low, bottom: bars[i].high }); sessionFVGCount++; } } // Filter to only unfilled FVGs let visible = fvgs; if (unfilledOnly) { visible = fvgs.filter(fvg => { for (let j = fvg.end + 1; j < bars.length; j++) { if (fvg.type === 'bull' && bars[j].low <= fvg.bottom) return false; if (fvg.type === 'bear' && bars[j].high >= fvg.top) return false; } return true; }); } // Render FVG boxes with extend:right for (const fvg of visible) { const isBull = fvg.type === 'bull'; const color = isBull ? bullColor : bearColor; const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); const gapTicks = ((fvg.top - fvg.bottom) / tickSize).toFixed(0); const label = showLabels ? ('FVG ' + (isBull ? '▲' : '▼') + ' ' + gapTicks + 't') : ''; ctx.box(fvg.start, fvg.top, fvg.end, fvg.bottom, { fillColor: 'rgba(' + r + ',' + g + ',' + b + ',0.15)', borderColor: color, extend: 'right', text: label }); } } ``` ### IFVG Sweep Detection (Complete Example) **Signature:** `Session level sweeps → FVG → Inversion = IFVG with labels, lines, and arrows` COMPLETE REFERENCE: Advanced ICT indicator that builds session highs/lows (Asian, London, NY, Previous Day), detects sweeps of those levels, finds FVGs after sweeps, then checks if the FVG gets inverted (price closes through it). Demonstrates: session level building with inSessionRange helper, lookback day counting with ctx.time.isNewSession(), ctx.line() with text labels for session levels, ctx.shape() with text for sweep arrows, ctx.box() with text for IFVG zones, unfilled filtering, per-session counters, wick-only sweep detection, and kill zone filtering. This is the canonical pattern for complex multi-session indicators with text annotations. **Parameters:** - `lookbackDays` *(number)* — Number of trading days to look back - `sweepAsian/London/NY/PrevDay` *(boolean)* — Toggle which session levels to monitor for sweeps - `sessionStart/End hours` *(number)* — Configurable session hours in ET for each session - `sweepWickOnly` *(boolean)* — If true, sweep must be a wick (close back above/below level) - `minSweepTicks` *(number)* — Minimum price beyond level to count as a sweep - `minGapTicks` *(number)* — Minimum FVG size in ticks - `maxBarsForFVG/Inversion` *(number)* — Max bars to look for FVG after sweep, and inversion after FVG - `showLabels/Levels/SweepArrows` *(boolean)* — Toggle visual annotations **Returns:** Rendered IFVG boxes, session level lines, and sweep arrows with text labels **Example:** ```js // ICT IFVG Sweep Strategy — session sweeps → FVG → inversion function calculate(bars, ctx) { const lookbackDays = ctx.input('Lookback Days', 1, { min: 1, max: 30, step: 1 }); const sweepAsian = ctx.input('Sweep Asian H/L', true); const sweepLondon = ctx.input('Sweep London H/L', true); const sweepNY = ctx.input('Sweep NY H/L', true); const sweepPrevDay = ctx.input('Sweep Prev Day H/L', true); const asianStart = ctx.input('Asian Start (ET)', 20, { min: 0, max: 23, step: 1 }); const asianEnd = ctx.input('Asian End (ET)', 0, { min: 0, max: 23, step: 1 }); const londonStart = ctx.input('London Start (ET)', 2, { min: 0, max: 23, step: 1 }); const londonEnd = ctx.input('London End (ET)', 5, { min: 0, max: 23, step: 1 }); const nyStart = ctx.input('NY Start (ET)', 9, { min: 0, max: 23, step: 1 }); const nyEnd = ctx.input('NY End (ET)', 12, { min: 0, max: 23, step: 1 }); const useKZFilter = ctx.input('Only During Kill Zones', true); const sweepWickOnly = ctx.input('Wick Sweeps Only', true); const minSweepTicks = ctx.input('Min Sweep Past Level (ticks)', 2, { min: 0, max: 50, step: 1 }); const minGapTicks = ctx.input('Min FVG Size (ticks)', 3, { min: 1, max: 100, step: 1 }); const maxBarsForFVG = ctx.input('Max Bars For FVG', 10, { min: 1, max: 50, step: 1 }); const maxBarsForInversion = ctx.input('Max Bars For Inversion', 15, { min: 1, max: 50, step: 1 }); const maxIFVGs = ctx.input('Max IFVGs Per Session', 5, { min: 1, max: 50, step: 1 }); const unfilledOnly = ctx.input('Unfilled Only', true); const bullColor = ctx.input('Bullish IFVG Color', '#26a69a'); const bearColor = ctx.input('Bearish IFVG Color', '#ef5350'); const levelColor = ctx.input('Session Level Color', '#ffeb3b'); const showLevels = ctx.input('Show Session Levels', true); const showSweepArrows = ctx.input('Show Sweep Arrows', true); const showLabels = ctx.input('Show IFVG Labels', true); const tickSize = 0.25; const minSweepPrice = minSweepTicks * tickSize; const minGapPrice = minGapTicks * tickSize; // Session check helper — handles midnight wrap function inSessionRange(barIndex, startHour, endHour) { const h = ctx.time.hourIn(barIndex, 'America/New_York'); if (startHour < endHour) return h >= startHour && h < endHour; return h >= startHour || h < endHour; } // Lookback cutoff let dayCount = 0, cutoffBar = 0; for (let i = bars.length - 1; i >= 1; i--) { if (ctx.time.isNewSession(i)) { dayCount++; if (dayCount > lookbackDays) { cutoffBar = i; break; } } } // Build session levels (high/low for each session) const sessions = []; let currentAsian = null, currentLondon = null, currentNY = null, currentDay = null; for (let i = 0; i < bars.length; i++) { const inAsian = inSessionRange(i, asianStart, asianEnd); const inLondon = inSessionRange(i, londonStart, londonEnd); const inNY = inSessionRange(i, nyStart, nyEnd); const isNewDay = ctx.time.isNewSession(i); // Previous Day tracking if (isNewDay) { if (currentDay && currentDay.high !== null && sweepPrevDay) { sessions.push({ type: 'PD', high: currentDay.high, low: currentDay.low, endBar: i - 1, highBar: currentDay.highBar, lowBar: currentDay.lowBar }); } currentDay = { high: bars[i].high, low: bars[i].low, highBar: i, lowBar: i }; } else if (currentDay) { if (bars[i].high > currentDay.high) { currentDay.high = bars[i].high; currentDay.highBar = i; } if (bars[i].low < currentDay.low) { currentDay.low = bars[i].low; currentDay.lowBar = i; } } // Asian session tracking if (inAsian) { if (!currentAsian) currentAsian = { high: bars[i].high, low: bars[i].low, highBar: i, lowBar: i }; else { if (bars[i].high > currentAsian.high) { currentAsian.high = bars[i].high; currentAsian.highBar = i; } if (bars[i].low < currentAsian.low) { currentAsian.low = bars[i].low; currentAsian.lowBar = i; } } } else if (currentAsian) { if (sweepAsian) sessions.push({ type: 'Asia', high: currentAsian.high, low: currentAsian.low, endBar: i - 1, highBar: currentAsian.highBar, lowBar: currentAsian.lowBar }); currentAsian = null; } // London session tracking if (inLondon) { if (!currentLondon) currentLondon = { high: bars[i].high, low: bars[i].low, highBar: i, lowBar: i }; else { if (bars[i].high > currentLondon.high) { currentLondon.high = bars[i].high; currentLondon.highBar = i; } if (bars[i].low < currentLondon.low) { currentLondon.low = bars[i].low; currentLondon.lowBar = i; } } } else if (currentLondon) { if (sweepLondon) sessions.push({ type: 'London', high: currentLondon.high, low: currentLondon.low, endBar: i - 1, highBar: currentLondon.highBar, lowBar: currentLondon.lowBar }); currentLondon = null; } // NY session tracking if (inNY) { if (!currentNY) currentNY = { high: bars[i].high, low: bars[i].low, highBar: i, lowBar: i }; else { if (bars[i].high > currentNY.high) { currentNY.high = bars[i].high; currentNY.highBar = i; } if (bars[i].low < currentNY.low) { currentNY.low = bars[i].low; currentNY.lowBar = i; } } } else if (currentNY) { if (sweepNY) sessions.push({ type: 'NY', high: currentNY.high, low: currentNY.low, endBar: i - 1, highBar: currentNY.highBar, lowBar: currentNY.lowBar }); currentNY = null; } } const recentSessions = sessions.filter(s => s.endBar >= cutoffBar); const sweptSessions = new Set(); const ifvgs = []; const usedSweeps = new Set(); let sessionIFVGCount = 0; for (let i = 2; i < bars.length; i++) { if (i < cutoffBar) continue; if (ctx.time.isNewSession(i)) sessionIFVGCount = 0; if (maxIFVGs > 0 && sessionIFVGCount >= maxIFVGs) continue; if (useKZFilter) { const inKZ = inSessionRange(i, 20, 0) || inSessionRange(i, 2, 5) || inSessionRange(i, 9, 12); if (!inKZ) continue; } for (const sess of recentSessions) { if (i <= sess.endBar) continue; const sweepKey = sess.type + '-' + sess.endBar; // BULLISH IFVG: sweep low → bearish FVG → closes above = inverted const sweptLow = bars[i].low < sess.low - minSweepPrice; const validLowSweep = sweepWickOnly ? (sweptLow && bars[i].close > sess.low) : sweptLow; if (validLowSweep && !usedSweeps.has(sweepKey + '-bull')) { sweptSessions.add(sweepKey + '-low'); for (let j = i; j < Math.min(i + maxBarsForFVG, bars.length - 2); j++) { if (bars[j + 2].high < bars[j].low) { const fvgTop = bars[j].low, fvgBottom = bars[j + 2].high; if (fvgTop - fvgBottom < minGapPrice) continue; for (let k = j + 3; k < Math.min(j + 3 + maxBarsForInversion, bars.length); k++) { if (bars[k].close > fvgTop) { ifvgs.push({ type: 'bull', sweepType: sess.type, sweepBar: i, fvgStart: j, fvgEnd: j + 2, inversionBar: k, top: fvgTop, bottom: fvgBottom }); sessionIFVGCount++; usedSweeps.add(sweepKey + '-bull'); if (showSweepArrows) ctx.shape(i, 'arrow_up', { color: bullColor, location: 'belowBar', text: sess.type + ' Sweep' }); break; } if (bars[k].close < fvgBottom) break; } if (usedSweeps.has(sweepKey + '-bull')) break; } } } // BEARISH IFVG: sweep high → bullish FVG → closes below = inverted const sweptHigh = bars[i].high > sess.high + minSweepPrice; const validHighSweep = sweepWickOnly ? (sweptHigh && bars[i].close < sess.high) : sweptHigh; if (validHighSweep && !usedSweeps.has(sweepKey + '-bear')) { sweptSessions.add(sweepKey + '-high'); for (let j = i; j < Math.min(i + maxBarsForFVG, bars.length - 2); j++) { if (bars[j + 2].low > bars[j].high) { const fvgTop = bars[j + 2].low, fvgBottom = bars[j].high; if (fvgTop - fvgBottom < minGapPrice) continue; for (let k = j + 3; k < Math.min(j + 3 + maxBarsForInversion, bars.length); k++) { if (bars[k].close < fvgBottom) { ifvgs.push({ type: 'bear', sweepType: sess.type, sweepBar: i, fvgStart: j, fvgEnd: j + 2, inversionBar: k, top: fvgTop, bottom: fvgBottom }); sessionIFVGCount++; usedSweeps.add(sweepKey + '-bear'); if (showSweepArrows) ctx.shape(i, 'arrow_down', { color: bearColor, location: 'aboveBar', text: sess.type + ' Sweep' }); break; } if (bars[k].close > fvgTop) break; } if (usedSweeps.has(sweepKey + '-bear')) break; } } } } } // Draw session levels with text labels if (showLevels) { for (const sess of recentSessions) { const sweepKey = sess.type + '-' + sess.endBar; const endDraw = bars.length - 1; const highSwept = sweptSessions.has(sweepKey + '-high'); ctx.line(sess.highBar, sess.high, endDraw, sess.high, { color: highSwept ? bearColor : levelColor, lineWidth: 1, lineStyle: highSwept ? 'solid' : 'dashed', text: sess.type + ' High' + (highSwept ? ' ✓' : '') }); const lowSwept = sweptSessions.has(sweepKey + '-low'); ctx.line(sess.lowBar, sess.low, endDraw, sess.low, { color: lowSwept ? bullColor : levelColor, lineWidth: 1, lineStyle: lowSwept ? 'solid' : 'dashed', text: sess.type + ' Low' + (lowSwept ? ' ✓' : '') }); } } // Filter unfilled IFVGs let visible = ifvgs; if (unfilledOnly) { visible = ifvgs.filter(fvg => { for (let j = fvg.inversionBar + 1; j < bars.length; j++) { if (fvg.type === 'bull' && bars[j].close < fvg.bottom) return false; if (fvg.type === 'bear' && bars[j].close > fvg.top) return false; } return true; }); } // Render IFVG boxes with text labels for (const fvg of visible) { const isBull = fvg.type === 'bull'; const color = isBull ? bullColor : bearColor; const r = parseInt(color.slice(1, 3), 16); const g = parseInt(color.slice(3, 5), 16); const b = parseInt(color.slice(5, 7), 16); const gapTicks = ((fvg.top - fvg.bottom) / tickSize).toFixed(0); const label = showLabels ? ('IFVG ' + (isBull ? '▲' : '▼') + ' ' + gapTicks + 't | ' + fvg.sweepType) : ''; ctx.box(fvg.fvgStart, fvg.top, fvg.inversionBar, fvg.bottom, { fillColor: 'rgba(' + r + ',' + g + ',' + b + ',0.2)', borderColor: color, extend: 'right', text: label }); } } ``` ### ctx.request **Signature:** `ctx.request(timeframe: string) → HTFData` Request higher-timeframe data. Returns non-repainting OHLCV arrays (per-LTF bar, forward-filled from confirmed HTF bars) plus a live forming bar. The confirmed arrays (htf.close, etc.) ONLY contain data from closed HTF bars — they forward-fill the previous confirmed value during forming periods. This means any TA run on htf.close is automatically non-repainting. The forming bar (htf.forming) updates every tick for visual display but is kept separate. **Parameters:** - `timeframe` *(string)* — HTF to request, e.g. '1H', '4H', '1D'. Must be higher than chart's current timeframe. **Returns:** HTFData { open[], high[], low[], close[], volume[], barStart[], barEnd[], bars: HTFConfirmedBar[], forming: HTFFormingBar | null }. bars[] = array of all confirmed HTF bars { open, high, low, close, volume, startIdx, endIdx, timestamp }. forming = live bar { open, high, low, close, volume, startBar, endBar, timestamp }. **Example:** ```js const htf = ctx.request('1H'); // Confirmed HTF close (staircase, non-repainting) ctx.plot(htf.close, 'HTF Close', { color: '#ff9800', lineWidth: 2 }); // HTF EMA from confirmed data only const htfEma = ctx.ta.ema(htf.close, 20); ctx.plot(htfEma, 'HTF EMA 20', { color: '#2196f3', lineWidth: 2 }); // Iterate confirmed bars for (const bar of htf.bars) { // bar.open, bar.high, bar.low, bar.close, bar.volume // bar.startIdx, bar.endIdx, bar.timestamp } ``` ### HTF Power of 3 (Example) **Signature:** `Full example — Projected HTF candles to the right of price` Complete example showing how to build a LuxAlgo-style HTF Power of 3 indicator. Projects the last 1-5 HTF candles to the right of the chart with hour timestamps above each candle, dashed connectors on the most recent candle mapping where O/H/L/C occurred on the LTF chart, price labels, and running delta volume. Hour labels use the user's local timezone. **Returns:** void **Example:** ```js // HTF Power of 3 — SXVNT SDK // Projects the last N HTF candles to the right of price action. // Most recent candle gets dashed connectors showing where O/H/L/C // occurred on the LTF chart. Each candle shows its hour label. function calculate(bars, ctx) { // ═══ Inputs ═══ const htfTimeframe = ctx.input('HTF', '1H'); const numCandles = ctx.input('Candles', 5, { min: 1, max: 5, step: 1 }); const rightOffset = ctx.input('Right Offset', 8, { min: 5, max: 50, step: 1 }); const candleWidth = ctx.input('Candle Width', 6, { min: 3, max: 12, step: 1 }); const candleGap = ctx.input('Candle Gap', 3, { min: 1, max: 8, step: 1 }); const bullColor = ctx.input('Bull Color', '#089981'); const bearColor = ctx.input('Bear Color', '#f23645'); const showLabels = ctx.input('Price Labels', true); const showDelta = ctx.input('Show Delta', true); // ═══ Get HTF data (non-repainting) ═══ const htf = ctx.request(htfTimeframe); const last = bars.length - 1; if (last < 0 || htf.bars.length === 0) return; // ═══ Collect candles to draw (confirmed + forming) ═══ // htf.bars = all confirmed HTF bars. htf.forming = live forming bar. const allConfirmed = htf.bars; const f = htf.forming; const formingCount = f ? (f.endBar - f.startBar + 1) : 0; const hasForming = f && formingCount >= 3; // Build display list: last N candles from confirmed + forming const displayCandles = []; const confirmedToShow = hasForming ? numCandles - 1 : numCandles; const startFrom = Math.max(0, allConfirmed.length - confirmedToShow); for (let i = startFrom; i < allConfirmed.length; i++) { const b = allConfirmed[i]; displayCandles.push({ o: b.open, h: b.high, l: b.low, c: b.close, vol: b.volume, sBar: b.startIdx, eBar: b.endIdx, ts: b.timestamp, isForming: false, }); } if (hasForming) { displayCandles.push({ o: f.open, h: f.high, l: f.low, c: f.close, vol: f.volume, sBar: f.startBar, eBar: f.endBar, ts: f.timestamp, isForming: true, }); } if (displayCandles.length === 0) return; // ═══ Draw each candle ═══ for (let ci = 0; ci < displayCandles.length; ci++) { const d = displayCandles[ci]; const isLast = ci === displayCandles.length - 1; // ── Position ── const startX = last + rightOffset + ci * (candleWidth + candleGap); const endX = startX + candleWidth; const midX = Math.round((startX + endX) / 2); const isBull = d.c >= d.o; const color = isBull ? bullColor : bearColor; // ── Wick ── ctx.line(midX, d.h, midX, d.l, { color, lineWidth: 2 }); // ── Body ── ctx.box(startX, Math.max(d.o, d.c), endX, Math.min(d.o, d.c), { fillColor: color, borderColor: color, }); // ── Hour label above candle ── const dt = new Date(d.ts); const hourStr = String(dt.getHours()); ctx.label(midX, d.h, hourStr, { color: 'transparent', textColor: '#787b86', size: 10, position: 'above', style: 'none', }); // ── Delta volume below candle ── if (showDelta && d.sBar >= 0 && d.eBar < bars.length) { let delta = 0; for (let i = d.sBar; i <= d.eBar; i++) { if (bars[i].close > bars[i].open) delta += bars[i].volume; else if (bars[i].close < bars[i].open) delta -= bars[i].volume; } const sign = delta > 0 ? '+' : ''; const abs = Math.abs(delta); let deltaStr; if (abs >= 1e6) deltaStr = sign + (delta / 1e6).toFixed(1) + 'M'; else if (abs >= 1000) deltaStr = sign + (delta / 1000).toFixed(1) + 'K'; else deltaStr = sign + Math.round(delta).toString(); ctx.label(midX, d.l, deltaStr, { textColor: delta >= 0 ? bullColor : bearColor, size: 9, position: 'below', style: 'none', }); } // ── Connectors + price labels on rightmost candle only ── if (isLast) { // Find which LTF bars made the high/low let hIdx = d.sBar, lIdx = d.sBar; if (d.sBar >= 0 && d.eBar < bars.length) { for (let i = d.sBar; i <= d.eBar; i++) { if (bars[i].high >= d.h) hIdx = i; if (bars[i].low <= d.l) lIdx = i; } } // Dashed connectors ctx.line(d.sBar, d.o, startX, d.o, { color: '#787b86', lineWidth: 1, lineStyle: 'dashed', }); ctx.line(hIdx, d.h, midX, d.h, { color: bullColor, lineWidth: 1, lineStyle: 'dashed', }); ctx.line(lIdx, d.l, midX, d.l, { color: bearColor, lineWidth: 1, lineStyle: 'dashed', }); ctx.line(last, d.c, endX, d.c, { color, lineWidth: 1, lineStyle: 'dashed', }); // Price labels if (showLabels) { const labelX = endX + 1; ctx.label(labelX, d.h, 'High ' + d.h.toFixed(2), { textColor: bullColor, size: 10, position: 'at', style: 'none', textAlign: 'left', }); ctx.label(labelX, d.c, 'Close ' + d.c.toFixed(2), { textColor: color, size: 10, position: 'at', style: 'none', textAlign: 'left', }); ctx.label(labelX, d.o, 'Open ' + d.o.toFixed(2), { textColor: '#787b86', size: 10, position: 'at', style: 'none', textAlign: 'left', }); ctx.label(labelX, d.l, 'Low ' + d.l.toFixed(2), { textColor: bearColor, size: 10, position: 'at', style: 'none', textAlign: 'left', }); } // TF label below the group ctx.label(midX, d.l, htfTimeframe, { color: 'transparent', textColor: '#787b86', size: 11, position: 'below', style: 'none', }); } } } ``` --- ## Plotting ### ctx.plot **Signature:** `ctx.plot(values, name?, opts?)` Draws a line series on the chart. Returns a plot ID for use with ctx.fill(). **Parameters:** - `values` *((number|null)[])* — One value per bar - `name?` *(string)* — Display name - `opts?` *(PlotOptions)* — { color, lineWidth, lineStyle, pane, paneName, showLast, offset } **Returns:** string — plot ID for fill() **Example:** ```js const p = ctx.plot(sma, 'SMA 20', { color: '#2962ff', lineWidth: 2 }); ``` ### ctx.fill **Signature:** `ctx.fill(plot1Id, plot2Id, opts?)` Fills the area between two plots. Color switches based on which is higher. **Parameters:** - `plot1Id` *(string)* — ID returned by first ctx.plot() - `plot2Id` *(string)* — ID returned by second ctx.plot() - `opts?` *(FillOptions)* — { colorUp, colorDown, opacity } **Returns:** void **Example:** ```js ctx.fill(p1, p2, { colorUp: 'rgba(38,166,154,0.2)', colorDown: 'rgba(239,83,80,0.2)' }); ``` ### ctx.shape **Signature:** `ctx.shape(barIndex, shapeType, opts?)` Draws a marker/arrow at a specific bar. Types: arrow_up/down, triangle_up/down, circle, diamond, cross, flag. **Parameters:** - `barIndex` *(number)* — Which bar to draw at - `shapeType` *(ShapeType)* — arrow_up, arrow_down, circle, diamond, etc. - `opts?` *(ShapeOptions)* — { position, price, color, size, text } **Returns:** void **Example:** ```js ctx.shape(i, 'arrow_up', { color: '#26a69a', text: 'BUY' }); ``` ### ctx.bubble **Signature:** `ctx.bubble(values, opts?)` Draws variable-size circles per bar — sized proportionally to the input array. Perfect for volume bubbles, delta bubbles, signal-strength visualization, or any 'show me magnitude at each bar' pattern. Convenience wrapper around ctx.shape('circle', ...) that auto-handles the min/max scaling so you don't have to. Bars where the value is null, zero, or non-finite are skipped (no circle drawn). Scaling uses the ABSOLUTE max of the array as the reference, so arrays containing negative values (e.g. order-flow delta = bid - ask) scale sanely too — set opts.negativeColor to color negatives differently from positives. **Parameters:** - `values` *((number | null)[])* — Array of values to visualize (typically ctx.price.volume, footprint delta, signal strength, etc.). One bar per index. - `opts.color?` *(string)* — Bubble color. Default '#22d3ee80' (cyan w/ alpha). Accepts hex / rgba. - `opts.negativeColor?` *(string)* — Optional separate color for negative values. If set, values >= 0 use `color`, values < 0 use `negativeColor`. Default unset (single color for all). - `opts.minSize?` *(number)* — Minimum pixel size for the smallest non-zero value. Default 4. - `opts.maxSize?` *(number)* — Maximum pixel size for the largest absolute value. Default 30. - `opts.position?` *('above' | 'below' | 'at')* — Where to anchor each bubble. Default 'at'. - `opts.price?` *((number | null)[])* — Per-bar Y position when position="at". Defaults to hlc3 (mid-bar). Pass ctx.price.high to plot at highs, ctx.price.low for lows, etc. **Returns:** void **Example:** ```js // Volume bubbles colored green/red by bar direction const up = ctx.price.close.map((c, i) => c >= ctx.price.open[i] ? ctx.price.volume[i] : 0); const dn = ctx.price.close.map((c, i) => c < ctx.price.open[i] ? ctx.price.volume[i] : 0); ctx.bubble(up, { color: 'rgba(34,197,94,0.5)', maxSize: 25 }); ctx.bubble(dn, { color: 'rgba(239,68,68,0.5)', maxSize: 25 }); // Or simpler — single color for all volume bars ctx.bubble(ctx.price.volume, { color: '#22d3ee80' }); // Delta bubbles (positive = buying pressure green, negative = selling red) const delta = ctx.footprint.delta; // array of bid-ask deltas ctx.bubble(delta, { color: 'rgba(34,197,94,0.5)', negativeColor: 'rgba(239,68,68,0.5)', minSize: 3, maxSize: 28, }); ``` ### ctx.box **Signature:** `ctx.box(startBar, topPrice, endBar, bottomPrice, opts?)` Draws a rectangle (great for FVGs, supply/demand zones, order blocks). **Parameters:** - `startBar` *(number)* — Left bar index - `topPrice` *(number)* — Top price - `endBar` *(number)* — Right bar index - `bottomPrice` *(number)* — Bottom price - `opts?` *(BoxOptions)* — { borderColor, fillColor, borderWidth, borderStyle, extend: 'none'|'right'|'left'|'both', text } **Returns:** void **Example:** ```js ctx.box(i-2, high, i, low, { fillColor: 'rgba(41,98,255,0.1)', extend: 'right' }); ``` ### ctx.line **Signature:** `ctx.line(startBar, startPrice, endBar, endPrice, opts?)` Draws a line between two points. Can extend in either direction. **Parameters:** - `startBar` *(number)* — Start bar index - `startPrice` *(number)* — Start price - `endBar` *(number)* — End bar index - `endPrice` *(number)* — End price - `opts?` *(LineOptions)* — { color, lineWidth, lineStyle, extend } **Returns:** void **Example:** ```js ctx.line(i-10, high, i, low, { color: '#ff9800', extend: 'right' }); ``` ### ctx.label **Signature:** `ctx.label(barIndex, price, text, opts?)` Draws a text label at a specific position. **Parameters:** - `barIndex` *(number)* — Bar index - `price` *(number)* — Price level - `text` *(string)* — Label text - `opts?` *(LabelOptions)* — { color, textColor, size, position, style: 'bubble'|'none' } **Returns:** void **Example:** ```js ctx.label(i, high, 'SFP', { color: '#ef5350', style: 'bubble' }); ``` ### ctx.bgcolor **Signature:** `ctx.bgcolor(conditions, opts?)` Colors the background of individual bars. Pass a color string or null per bar. **Parameters:** - `conditions` *((string|null)[])* — Per-bar color or null - `opts?` *({ opacity? })* — Opacity (default 0.15) **Returns:** void **Example:** ```js ctx.bgcolor(bars.map((_, i) => ctx.time.isAsianKZ(i) ? 'purple' : null)); ``` ### ctx.barcolor **Signature:** `ctx.barcolor(colors)` Overrides candle colors. Pass a color string or null per bar. **Parameters:** - `colors` *((string|null)[])* — Per-bar color or null (keeps default) **Returns:** void **Example:** ```js ctx.barcolor(rsi.map(v => v > 70 ? '#ef5350' : v < 30 ? '#26a69a' : null)); ``` ### ctx.hline **Signature:** `ctx.hline(price, opts?)` Draws a horizontal reference line at a price level. **Parameters:** - `price` *(number)* — Price level - `opts?` *(HLineOptions)* — { color, lineWidth, lineStyle, label, pane } **Returns:** void **Example:** ```js ctx.hline(70, { color: '#ef5350', lineStyle: 'dashed', label: 'OB', pane: 'separate' }); ``` ### ctx.histogram **Signature:** `ctx.histogram(values, name?, opts?)` Draws vertical histogram bars from zero line. **Parameters:** - `values` *((number|null)[])* — One value per bar - `name?` *(string)* — Display name - `opts?` *(HistogramOptions)* — { color, lineWidth, pane } **Returns:** string — histogram ID **Example:** ```js ctx.histogram(m.histogram, 'MACD Hist', { pane: 'separate', color: colors }); ``` ### ctx.table **Signature:** `ctx.table(position, columns, rows, opts?)` Creates a screen-anchored table overlay on the chart. Returns a table ID used with ctx.cell() to populate cells. Tables float in a fixed position (e.g. top-right corner) and do not scroll with the chart. Useful for displaying live stats, indicator readouts, or any key-value data. **Parameters:** - `position` *(TablePosition)* — 'top_left' | 'top_center' | 'top_right' | 'middle_left' | 'middle_center' | 'middle_right' | 'bottom_left' | 'bottom_center' | 'bottom_right' - `columns` *(number)* — Number of columns - `rows` *(number)* — Number of rows - `opts?` *(TableOptions)* — { bgcolor, borderColor, borderWidth, textColor, fontSize, cellPadding, paddingTop, paddingBottom } — paddingTop/paddingBottom (in pixels, default 0) push the table away from the top/bottom edges of the chart **Returns:** string — table ID (pass to ctx.cell) **Example:** ```js const t = ctx.table('top_right', 2, 3, { bgcolor: 'rgba(13,17,28,0.92)', borderColor: 'rgba(255,255,255,0.08)', fontSize: 11, paddingTop: 20, // push 20px down from top edge }); ctx.cell(t, 0, 0, 'RSI', { textColor: '#aaa' }); ctx.cell(t, 0, 1, '72.3', { textColor: '#ef5350' }); ctx.cell(t, 1, 0, 'Trend', { textColor: '#aaa' }); ctx.cell(t, 1, 1, 'Bullish', { textColor: '#00e676' }); ctx.cell(t, 2, 0, 'ATR', { textColor: '#aaa' }); ctx.cell(t, 2, 1, '12.50', { textColor: '#ffab40' }); ``` ### ctx.cell **Signature:** `ctx.cell(tableId, row, col, text, opts?)` Sets the content and style of a single cell in a table created by ctx.table(). Row and column indices are zero-based. Each cell can have its own background color, text color, font size, and alignment. **Parameters:** - `tableId` *(string)* — ID returned by ctx.table() - `row` *(number)* — Row index (0-based) - `col` *(number)* — Column index (0-based) - `text` *(string)* — Cell text content - `opts?` *(CellOptions)* — { textColor, bgcolor, fontSize, align } **Returns:** void **Example:** ```js // Header with custom background ctx.cell(t, 0, 0, 'Metric', { textColor: '#888', bgcolor: 'rgba(255,255,255,0.05)' }); ctx.cell(t, 0, 1, 'Value', { textColor: '#888', bgcolor: 'rgba(255,255,255,0.05)' }); // Dynamic value with conditional color const rsiVal = rsi[i]; const rsiColor = rsiVal > 70 ? '#ef5350' : rsiVal < 30 ? '#26a69a' : '#2196f3'; ctx.cell(t, 1, 0, 'RSI(14)', { textColor: '#aaa' }); ctx.cell(t, 1, 1, rsiVal.toFixed(1), { textColor: rsiColor }); ``` ### ctx.htfCandles **Signature:** `ctx.htfCandles(htf: HTFData, opts?: HTFCandleOptions)` Draw higher-timeframe candle bodies and wicks as an overlay on the current chart. Confirmed HTF bars render as solid filled candles. The live forming bar renders with a dashed border and lower opacity, updating in real-time as new LTF bars arrive. **Parameters:** - `htf` *(HTFData)* — The HTFData object returned by ctx.request(). - `opts.bullColor` *(string)* — Color for bullish HTF candles. Default: '#26a69a'. - `opts.bearColor` *(string)* — Color for bearish HTF candles. Default: '#ef5350'. - `opts.opacity` *(number)* — Fill opacity (0-1). Default: 0.15. - `opts.showWicks` *(boolean)* — Show HTF wicks. Default: true. - `opts.showForming` *(boolean)* — Show the live forming HTF bar. Default: true. **Returns:** void **Example:** ```js const htf = ctx.request('1H'); ctx.htfCandles(htf, { bullColor: '#26a69a', bearColor: '#ef5350', opacity: 0.15, showWicks: true, showForming: true, }); ``` --- ## Strategy ### ctx.strategy.entry **Signature:** `ctx.strategy.entry(id, side, opts?)` Places an entry order. Use bracket for SL/TP/trailing. **Parameters:** - `id` *(string)* — Unique order ID - `side` *('long' | 'short')* — Trade direction - `opts?` *(EntryOptions)* — { qty, type, price, bracket: { stopLoss, takeProfit[], trailingStop, breakeven } } **Returns:** void **Example:** ```js ctx.strategy.entry('long1', 'long', { bracket: { stopLoss: { price: low - 10 }, takeProfit: [{ price: close + 20, onFill: 'breakeven' }] } }); ``` ### ctx.strategy.close **Signature:** `ctx.strategy.close(id?, opts?)` Closes the current position (full or partial). **Parameters:** - `id?` *(string)* — Order ID (optional) - `opts?` *(CloseOptions)* — { qty, type, price } **Returns:** void **Example:** ```js if (crossDn[last]) ctx.strategy.close(); ``` ### ctx.strategy.position **Signature:** `ctx.strategy.position` Read-only current position state. **Returns:** { size, side, avgPrice, unrealizedPnl, unrealizedTicks, entryTime, fills[] } **Example:** ```js if (ctx.strategy.position.side === 'flat') { /* enter trade */ } ``` ### ctx.strategy.flattenAll **Signature:** `ctx.strategy.flattenAll()` Emergency close all positions immediately at market. **Returns:** void **Example:** ```js if (ctx.time.hour(last) === 16) ctx.strategy.flattenAll(); ``` ### ctx.strategy.account **Signature:** `ctx.strategy.account` Read-only account state. In backtesting this reflects simulated balance; in live mode it syncs with your Rithmic account. **Returns:** { balance, equity, dayPnl, todayTrades, margin } **Example:** ```js if (ctx.strategy.account.dayPnl < -500) { ctx.strategy.flattenAll(); // stop trading if down $500 } ``` ### ctx.strategy.history **Signature:** `ctx.strategy.history` Read-only trade history and computed performance metrics for the current session. **Returns:** { trades: TradeRecord[], performance: PerformanceMetrics } **Example:** ```js const perf = ctx.strategy.history.performance; ctx.log('Win rate:', perf.winRate, 'PF:', perf.profitFactor); ``` ### ctx.strategy.cancel **Signature:** `ctx.strategy.cancel(id)` Cancels a pending order by its ID. **Parameters:** - `id` *(string)* — The order ID to cancel **Returns:** void **Example:** ```js ctx.strategy.cancel('limit_entry_1'); ``` ### ctx.strategy.modifyStop **Signature:** `ctx.strategy.modifyStop(id, newPrice)` Moves the stop loss of an active position to a new price level. **Parameters:** - `id` *(string)* — Order ID of the position - `newPrice` *(number)* — New stop loss price **Returns:** void **Example:** ```js // Trail stop to breakeven after 10 ticks profit const pos = ctx.strategy.position; if (pos.side === 'long' && pos.unrealizedTicks >= 10) { ctx.strategy.modifyStop('long1', pos.avgPrice); } ``` ### ctx.strategy.modifyTP **Signature:** `ctx.strategy.modifyTP(id, tpIndex, newPrice)` Moves a take profit level to a new price. Use tpIndex to target which TP level (0-based). **Parameters:** - `id` *(string)* — Order ID of the position - `tpIndex` *(number)* — Index of the TP level (0 = first TP) - `newPrice` *(number)* — New take profit price **Returns:** void **Example:** ```js ctx.strategy.modifyTP('long1', 0, ctx.price.high[last] + 20); ``` ### ctx.strategy.setTrail **Signature:** `ctx.strategy.setTrail(id, opts)` Activates or updates a trailing stop on a position. The stop follows price at the specified offset. **Parameters:** - `id` *(string)* — Order ID of the position - `opts` *(TrailOptions)* — { trailOffset, unit: 'ticks' | 'price' | 'percent', stepSize? } **Returns:** void **Example:** ```js ctx.strategy.setTrail('long1', { trailOffset: 8, unit: 'ticks', stepSize: 2 }); ``` ### ctx.strategy.riskLimits **Signature:** `ctx.strategy.riskLimits` Read-only risk limit configuration for the strategy. These are enforced by the engine and will prevent orders that exceed limits. **Returns:** { maxPositionSize, maxDailyLoss, maxTradesPerDay, maxDrawdown } **Example:** ```js const limits = ctx.strategy.riskLimits; ctx.log('Max daily loss:', limits.maxDailyLoss); ``` --- ## Live Trading ### Live Strategy Overview **Signature:** `calculate(bars, ctx) → OrderIntent → Rithmic Order` When you enable "Go Live" on a strategy script, the platform executes your calculate() function on every bar close using real market data. Any ctx.strategy.entry() or ctx.strategy.close() calls produce OrderIntents which are automatically converted to live Rithmic orders (market, limit, stop, or bracket). The engine handles duplicate suppression, error recovery (auto-disables after 3 consecutive errors), and daily P&L tracking. **Returns:** Live orders dispatched to Rithmic **Example:** ```js // Your existing strategy code works as-is in live mode. // Just write calculate(bars, ctx) using ctx.strategy.entry/close // and enable "Go Live" in the Scripts panel. function calculate(bars, ctx) { const ema9 = ctx.ta.ema(ctx.price.close, 9); const ema21 = ctx.ta.ema(ctx.price.close, 21); const cross = ctx.ta.crossover(ema9, ema21); const last = bars.length - 1; if (cross[last]) { ctx.strategy.entry('long1', 'long', { bracket: { stopLoss: { price: ctx.price.low[last] - 2 }, takeProfit: [{ price: ctx.price.close[last] + 10 }] } }); } } ``` ### Bracket Orders **Signature:** `ctx.strategy.entry(id, side, { bracket: { stopLoss, takeProfit[], trailingStop?, breakeven? } })` Bracket orders send an entry with attached stop loss and take profit OCO (one-cancels-other). In live mode, this dispatches a native Rithmic bracket order for atomic SL+TP management. Supports multiple TP levels, trailing stops, and auto-breakeven triggers. **Parameters:** - `stopLoss` *({ price, type?, limitOffset? })* — Required. Stop loss price. Optionally set type to "stop_limit" with a limitOffset. - `takeProfit` *(BracketTPLevel[])* — Array of TP levels. Each has { price, qty?, onFill? }. onFill: "none" | "breakeven" | "trail" controls what happens when this TP fills. - `trailingStop` *({ trailOffset, unit, activationPrice?, stepSize? })* — Trailing stop config. unit: "ticks" | "price" | "percent". stepSize moves stop in discrete increments. - `breakeven` *({ triggerOffset, offset?, unit })* — Auto-breakeven. Moves SL to entry price + offset after price moves triggerOffset in your favor. **Returns:** void — order is dispatched to Rithmic **Example:** ```js ctx.strategy.entry('long1', 'long', { qty: 2, bracket: { stopLoss: { price: ctx.price.low[last] - 4 }, takeProfit: [ { price: ctx.price.close[last] + 8, qty: 1, onFill: 'breakeven' }, { price: ctx.price.close[last] + 16, qty: 1, onFill: 'trail' } ], trailingStop: { trailOffset: 6, unit: 'ticks', stepSize: 2 }, breakeven: { triggerOffset: 10, offset: 1, unit: 'ticks' } } }); ``` ### Supported Order Types **Signature:** `type: 'market' | 'limit' | 'stop'` All three order types work in live mode. Market orders fill immediately at current price. Limit orders rest at your specified price. Stop orders trigger a market order when price reaches your trigger price. Bracket orders can use market (default) or limit entry. **Parameters:** - `market` *(default)* — Fills immediately at best available price. Default if no type is specified. - `limit` *({ type: 'limit', price })* — Rests at the specified price, fills when market reaches it. - `stop` *({ type: 'stop', price })* — Triggers a market order when price reaches the trigger price. **Returns:** void **Example:** ```js // Market entry (default) ctx.strategy.entry('mkt', 'long'); // Limit entry at specific price ctx.strategy.entry('lmt', 'long', { type: 'limit', price: ctx.price.close[last] - 2 }); // Stop entry (breakout) ctx.strategy.entry('stp', 'long', { type: 'stop', price: ctx.price.high[last] + 1 }); ``` ### Emergency Flatten **Signature:** `ctx.strategy.flattenAll()` Immediately cancels all pending orders and closes all open positions at market. In live mode this calls Rithmic cancelAllOrders + flattenAll. Use for end-of-day flattening, risk limit breaches, or emergency stops. The UI also provides a manual Emergency Flatten button. **Returns:** void **Example:** ```js // Flatten at end of day (4pm ET) const last = bars.length - 1; if (ctx.time.hour(last) >= 16) { ctx.strategy.flattenAll(); } // Flatten on daily loss limit if (ctx.strategy.account.dayPnl < -1000) { ctx.strategy.flattenAll(); } ``` ### Input Overrides & Script Settings **Signature:** `settings.inputOverrides / settings.backtestSettings` Each script can store persistent settings via the settings JSONB column. inputOverrides lets you save customized input values that override ctx.input() defaults without changing code — perfect for tuning a published strategy per-account. backtestSettings saves your preferred initial balance, commission, and slippage for the backtest engine. **Parameters:** - `inputOverrides` *(Record)* — Key-value map of input labels to override values. Keys must match ctx.input() labels exactly. - `backtestSettings` *({ initialBalance?, commission?, slippage? })* — Override backtest defaults: initialBalance (default 100000), commission per side (default 2.50), slippage in ticks (default 1). **Returns:** Persisted to user_scripts.settings column **Example:** ```js // Settings are saved per-script in the database: // { // "backtestSettings": { // "initialBalance": 50000, // "commission": 2.50, // "slippage": 1 // }, // "inputOverrides": { // "EMA Period": 12, // "Stop Loss Ticks": 8, // "Use Trailing": true // } // } // In your script, declare inputs normally: const period = ctx.input('EMA Period', 9); const slTicks = ctx.input('Stop Loss Ticks', 10); const useTrail = ctx.input('Use Trailing', false); // Overrides are applied automatically — no code changes needed. ``` ### Live Execution Safety **Signature:** `Auto-disable / Duplicate suppression / Daily reset` The live engine includes multiple safety mechanisms: (1) Auto-disable after 3 consecutive script execution errors. (2) Duplicate intent suppression — the same order ID on the same bar index is only dispatched once. (3) Daily P&L and trade count reset at midnight. (4) Strategy auto-disables when you switch scripts. (5) Position state syncs from Rithmic in real-time so ctx.strategy.position always reflects your actual position. **Returns:** Automatic protection — no code needed **Example:** ```js // These safety features work automatically. // You can also add your own safeguards: const pos = ctx.strategy.position; const acct = ctx.strategy.account; // Don't enter if already in a position if (pos.side !== 'flat') return; // Don't trade after N trades per day if (acct.todayTrades >= 5) return; // Don't trade if daily loss limit reached if (acct.dayPnl < -500) return; ``` ### ctx.state **Signature:** `ctx.state` Persistent state object that survives across bar executions within the same live session. Use it to track phases (e.g., "stops placed", "bracket placed"), store computed values, or implement multi-step logic. Automatically cleared when the position closes or the strategy is re-enabled. In backtest mode, state resets each time the strategy is re-run. Write any property you want — it's a plain object. **Parameters:** - `ctx.state.*` *(any)* — Any property you assign persists until position closes or strategy resets. **Returns:** Record — your custom state object **Example:** ```js function calculate(bars, ctx) { const pos = ctx.strategy.position; // Phase 1: FLAT — place entry orders if (pos.side === 'flat') { if (!ctx.state.stopsPlaced) { ctx.strategy.entry('buy', 'long', { type: 'stop', price: buyPrice, qty: 1 }); ctx.strategy.entry('sell', 'short', { type: 'stop', price: sellPrice, qty: 1 }); ctx.state.stopsPlaced = true; } return; } // Phase 2: IN POSITION — place bracket (SL/TP) if (!ctx.state.bracketPlaced) { ctx.strategy.entry('sl', closeSide, { type: 'stop', price: slPrice, qty: 1 }); ctx.strategy.entry('tp', closeSide, { type: 'limit', price: tpPrice, qty: 1 }); ctx.state.bracketPlaced = true; } } // ctx.state resets automatically when position closes → fresh Phase 1 ``` ### Live-only inputs (liveOnly) **Signature:** `ctx.input(label, false, { liveOnly: true })` SXVNT scripts CANNOT detect whether they are running live or in a backtest — and this is intentional. The TradingView ecosystem is full of strategies that branch on `barstate.isrealtime` to produce flawless backtests that fail live. We refuse to ship that exploit on SXVNT: on this platform, a backtest is always a faithful simulation of live signal logic. The one legitimate cross-mode case — a 'live trading enabled' safety toggle that defaults to OFF for your live account but should not block backtest order generation — is handled declaratively via the `liveOnly: true` input option. When you declare an input as live-only, the backtest engine silently overrides its value to `true`, so the gate only fires in live mode. Your script reads a single value; the engine does the right thing depending on context. Only applies to boolean inputs. Numeric / string / color inputs ignore the flag. **Parameters:** - `label` *(string)* — Display label in the Settings modal (same as a normal ctx.input). - `defaultValue` *(boolean)* — Default value when running LIVE. Overridden to true in backtest. - `opts.liveOnly` *(boolean)* — Set true to declare this input as a live-only safety gate. **Returns:** boolean — user setting in live, always true in backtest **Example:** ```js // ❌ BAD — backtest produces zero trades because the gate fires in // backtest too. SXVNT does NOT expose ctx.isBacktest / // ctx.mode / barstate.isrealtime to scripts, so do not try to // work around this in code. const enableTrading = ctx.input('Enable Live Trading', false); if (!enableTrading) return; // ✅ GOOD — declare the input as liveOnly. In backtest the value is // silently forced to true; the gate only matters live. const enableTrading = ctx.input('Enable Live Trading', false, { liveOnly: true }); if (!enableTrading) return; // works in BOTH live and backtest ``` ### OCO (One-Cancels-Other) Lifecycle **Signature:** `Automatic OCO cancel + bracket placement` The live engine automatically manages OCO order lifecycles. When you place two opposing entry orders (e.g., buy stop + sell stop), the engine detects when one fills and immediately cancels the other. It then re-executes your strategy so Phase 2 (SL/TP bracket) runs within ~300ms of the fill. When the bracket resolves (TP or SL hit), the remaining bracket leg is cancelled and state is reset for fresh entries. Three independent cancel mechanisms ensure reliability: (1) onTrade fill callback — fastest, fires on the trade event. (2) React position state change — fires when position appears/disappears. (3) Bar execution position detection — fires on next bar close. **Returns:** Automatic — no code needed beyond ctx.strategy.entry() **Example:** ```js // COMPLETE OCO SCALPER LIFECYCLE: function calculate(bars, ctx) { const pos = ctx.strategy.position; const price = ctx.price.last; // real-time tick price const tickSize = ctx.syminfo.tickSize; const offset = ctx.input('Offset (pts)', 5); const slTicks = ctx.input('Stop Loss (ticks)', 40); const tpTicks = ctx.input('Take Profit (ticks)', 4); // Phase 1: FLAT — place OCO stops above & below if (pos.side === 'flat') { if (!ctx.state.stopsPlaced) { ctx.strategy.entry('oco_long', 'long', { type: 'stop', price: price + offset, qty: 1 }); ctx.strategy.entry('oco_short', 'short', { type: 'stop', price: price - offset, qty: 1 }); ctx.state.stopsPlaced = true; } return; // wait for fill } // Phase 2: IN POSITION — place SL & TP // Engine auto-cancelled the unfilled OCO leg if (!ctx.state.bracketPlaced) { const isLong = pos.side === 'long'; const closeSide = isLong ? 'short' : 'long'; const entry = pos.avgPrice; const sl = isLong ? entry - slTicks * tickSize : entry + slTicks * tickSize; const tp = isLong ? entry + tpTicks * tickSize : entry - tpTicks * tickSize; ctx.strategy.entry('bracket_sl', closeSide, { type: 'stop', price: sl, qty: pos.size }); ctx.strategy.entry('bracket_tp', closeSide, { type: 'limit', price: tp, qty: pos.size }); ctx.state.bracketPlaced = true; } } // When TP or SL fills → remaining leg auto-cancelled // → ctx.state resets → Phase 1 runs again ``` --- ## Math ### ctx.math **Signature:** `ctx.math.abs/round/ceil/floor/pow/sqrt/log/max/min/sum/avg/clamp/lerp` Math utility functions. sum() and avg() take arrays. clamp(v, min, max). lerp(a, b, t). **Returns:** number **Example:** ```js const rounded = ctx.math.round(price, 2); const clamped = ctx.math.clamp(rsi, 0, 100); ``` --- ## Order Flow ### ctx.footprint **Signature:** `ctx.footprint.available / .bar(i) / .delta / .poc / ...` Order flow (footprint) data per bar. Available on all chart types when tick data has loaded. Check ctx.footprint.available before use. Provides bid/ask volume at every price level for volume profile, delta analysis, imbalance detection, and POC tracking. **Returns:** FootprintAPI **Example:** ```js if (!ctx.footprint.available) { ctx.log('Waiting for tick data to load...'); return; } const delta = ctx.footprint.delta; ctx.plot(delta, 'Bar Delta', { pane: 'separate' }); ``` ### ctx.footprint.bar **Signature:** `ctx.footprint.bar(barIndex)` Returns full footprint data for a bar: all price levels with bid/ask volume, POC, and delta. Returns null if no data for that bar. Each level has: price, bidVolume, askVolume, delta, totalVolume. **Parameters:** - `barIndex` *(number)* — Index of the bar (0-based) **Returns:** ScriptFootprintBar | null **Example:** ```js const fp = ctx.footprint.bar(bars.length - 1); if (fp) { ctx.log('POC:', fp.poc, 'Delta:', fp.delta); for (const level of fp.levels) { if (level.askVolume > level.bidVolume * 3) { ctx.label(bars.length - 1, level.price, 'BUY IMBALANCE', { color: '#26a69a' }); } } } ``` ### ctx.footprint.delta **Signature:** `ctx.footprint.delta` Per-bar bid/ask delta array (askVolume - bidVolume). Positive = buying aggression, negative = selling aggression. null for bars without footprint data. **Returns:** (number | null)[] **Example:** ```js const delta = ctx.footprint.delta; ctx.histogram(delta, 'Delta', { pane: 'separate', color: delta.map(d => d !== null && d >= 0 ? '#26a69a' : '#ef5350') }); ``` ### ctx.footprint.cumulativeDelta **Signature:** `ctx.footprint.cumulativeDelta` Running cumulative delta across all bars. Useful for detecting divergence between price trend and order flow. Resets at start of visible data. **Returns:** (number | null)[] **Example:** ```js const cumDelta = ctx.footprint.cumulativeDelta; ctx.plot(cumDelta, 'Cumulative Delta', { pane: 'separate', color: '#2196f3' }); ``` ### ctx.footprint.poc **Signature:** `ctx.footprint.poc` Per-bar Point of Control — the price level with the highest total volume in each bar. null for bars without footprint data. **Returns:** (number | null)[] **Example:** ```js const poc = ctx.footprint.poc; ctx.plot(poc, 'POC', { color: '#ff9800', lineWidth: 2 }); ``` ### ctx.footprint.bidVolume / askVolume / totalVolume **Signature:** `ctx.footprint.bidVolume / .askVolume / .totalVolume` Per-bar aggregate footprint volumes. bidVolume = sell aggressor volume (traded at bid), askVolume = buy aggressor volume (traded at ask), totalVolume = bid + ask. **Returns:** (number | null)[] **Example:** ```js const askVol = ctx.footprint.askVolume; const bidVol = ctx.footprint.bidVolume; ctx.histogram(askVol, 'Ask Volume', { pane: 'separate', color: '#26a69a' }); ``` ### ctx.footprint.pocVolume **Signature:** `ctx.footprint.pocVolume` Per-bar volume at the Point of Control level. Useful for gauging conviction at the highest-volume price. **Returns:** (number | null)[] **Example:** ```js const pocVol = ctx.footprint.pocVolume; ctx.histogram(pocVol, 'POC Volume', { pane: 'separate', color: '#ff9800' }); ``` --- ## Alerts ### ctx.alerts.last **Signature:** `ctx.alerts.last: AlertEvent | null` The most recent inbound alert (from POST /api/v1/alerts), or null if none exist yet. Updated immediately as new alerts arrive — your script also re-executes out-of-band when a new alert lands, so you can react in sub-second time without waiting for bar close. Check `ctx.alerts.lastReason === "alert"` to detect an alert-triggered run vs a normal bar run. **Returns:** AlertEvent | null with fields: { id, severity, title, message, source, data, created_at, created_at_ms } **Example:** ```js // React instantly when an external system (TradingView, CnC, ML model) // fires an alert tagged "whale-print" against this account. if (ctx.alerts.lastReason === 'alert' && ctx.alerts.last?.source === 'whale-print') { const a = ctx.alerts.last; ctx.log('Whale print:', a.data.side, '@', a.data.price); // ... place orders, draw shapes, etc. } ``` ### ctx.alerts.lastReason **Signature:** `ctx.alerts.lastReason: "bar" | "alert"` Why this script execution fired. "bar" = normal bar-close run; "alert" = re-execution triggered by a new inbound alert. When "alert", ctx.alerts.last is the alert that fired it. Use this to gate "do something only when a new alert just arrived" logic so you don't re-act on the same alert on every subsequent bar. **Returns:** "bar" or "alert" **Example:** ```js if (ctx.alerts.lastReason === 'alert') { // First chance to see this alert. Act now. const a = ctx.alerts.last; if (a && a.severity === 'critical') { ctx.strategy.flattenAll(); } } ``` ### ctx.alerts.recent **Signature:** `ctx.alerts.recent(opts?): AlertEvent[]` Returns inbound alerts from the rolling 200-row buffer, newest first. Filter by source, time window, or limit. Cheap — pure in-memory filter, no DB roundtrip. **Parameters:** - `opts.source?` *(string)* — Filter to alerts tagged with this source. - `opts.limit?` *(number)* — Max rows returned (1–200, default 50). - `opts.sinceMs?` *(number)* — Only alerts with created_at_ms >= sinceMs (epoch ms). **Returns:** AlertEvent[] **Example:** ```js // All ML-model alerts from the last 5 minutes const fiveMinAgo = Date.now() - 5 * 60_000; const recent = ctx.alerts.recent({ source: 'my-ml-v3', sinceMs: fiveMinAgo, }); if (recent.length >= 3) { // 3+ confirming signals in 5min → conviction trade ctx.strategy.entry({ side: 'long', qty: 1 }); } ``` --- ## Writing scripts A SXVNT script is plain JavaScript executed once per chart update. The `ctx` object is provided as a global. Scripts run in a sandboxed environment — no `require`, no network calls, no DOM access. ### Quick example: EMA crossover with fill ```js const fast = ctx.ta.ema(ctx.price.close, 9); const slow = ctx.ta.ema(ctx.price.close, 21); const p1 = ctx.plot(fast, 'Fast EMA', { color: '#26a69a' }); const p2 = ctx.plot(slow, 'Slow EMA', { color: '#ef5350' }); ctx.fill(p1, p2, { colorUp: 'rgba(38,166,154,0.15)', colorDown: 'rgba(239,83,80,0.15)' }); // Strategy: enter on crossover const cross = ctx.ta.crossover(fast, slow); const crossDn = ctx.ta.crossunder(fast, slow); const last = ctx.price.close.length - 1; if (cross[last]) ctx.strategy.entry('long', 'long'); if (crossDn[last]) ctx.strategy.close(); ```