Skip to content

SMC

by MeMyselfAndI · 1 in use · published 3 months ago

Loading SMC on BTC-USDT 1h…

Source

sha256 8123c902a8009c04

1// Smart Money Concepts — VP Script / VerticalView TypeScript port.
2//
3// Adapted from "Smart Money Concepts [LuxAlgo]" (Pine Script v5).
4// Original work © LuxAlgo, licensed CC BY-NC-SA 4.0:
5// https://creativecommons.org/licenses/by-nc-sa/4.0/
6// This adaptation is distributed under the same licence.
7//
8// VP Script runs once over the complete bar array. State that Pine keeps in
9// `var` values is therefore ordinary local state inside the loop below.
10// Drawings are emitted declaratively after the calculation; they are never
11// mutated. The port deliberately uses no Intl or browser globals.
12
13indicator('Smart Money Concepts [LuxAlgo] — VP', {
14 shortTitle: 'SMC — VP',
15 pane: 'overlay',
16})
17
18// ─────────────────────────────────────────────────────────────────────────────
19// Inputs
20// ─────────────────────────────────────────────────────────────────────────────
21
22const mode = input.string('Mode', 'Historical', { options: ['Historical', 'Present'] })
23const style = input.string('Style', 'Colored', { options: ['Colored', 'Monochrome'] })
24const colorCandles = input.bool('Color Candles', false)
25
26const showInternals = input.bool('Show Internal Structure', true)
27const internalBullMode = input.string('Internal Bullish Structure', 'All', { options: ['All', 'BOS', 'CHoCH'] })
28const internalBearMode = input.string('Internal Bearish Structure', 'All', { options: ['All', 'BOS', 'CHoCH'] })
29const internalBullInput = input.color('Internal Bullish Color', '#089981')
30const internalBearInput = input.color('Internal Bearish Color', '#F23645')
31const internalConfluence = input.bool('Internal Confluence Filter', false)
32const structureLabelSize = input.string('Structure Label Size', 'Small', { options: ['Tiny', 'Small', 'Normal'] })
33
34const showStructure = input.bool('Show Swing Structure', true)
35const swingBullMode = input.string('Swing Bullish Structure', 'All', { options: ['All', 'BOS', 'CHoCH'] })
36const swingBearMode = input.string('Swing Bearish Structure', 'All', { options: ['All', 'BOS', 'CHoCH'] })
37const swingBullInput = input.color('Swing Bullish Color', '#089981')
38const swingBearInput = input.color('Swing Bearish Color', '#F23645')
39const showSwings = input.bool('Show Swing Points', false)
40const swingsLength = input.int('Swing Length', 50, { min: 10, max: 500 })
41const showStrongWeak = input.bool('Show Strong/Weak High/Low', true)
42
43const showInternalOb = input.bool('Internal Order Blocks', true)
44const internalObCount = input.int('Internal Order Block Count', 5, { min: 1, max: 20 })
45const showSwingOb = input.bool('Swing Order Blocks', false)
46const swingObCount = input.int('Swing Order Block Count', 5, { min: 1, max: 20 })
47const obFilter = input.string('Order Block Filter', 'Atr', { options: ['Atr', 'Cumulative Mean Range'] })
48const obMitigation = input.string('Order Block Mitigation', 'High/Low', { options: ['Close', 'High/Low'] })
49const internalBullObInput = input.color('Internal Bullish OB', '#3179f5')
50const internalBearObInput = input.color('Internal Bearish OB', '#f77c80')
51const swingBullObInput = input.color('Swing Bullish OB', '#1848cc')
52const swingBearObInput = input.color('Swing Bearish OB', '#b22833')
53
54const showEqual = input.bool('Equal High/Low', true)
55const equalLength = input.int('EQH/EQL Confirmation Bars', 3, { min: 1, max: 100 })
56const equalThreshold = input.float('EQH/EQL Threshold', 0.1, { min: 0, max: 0.5 })
57
58const showFvg = input.bool('Fair Value Gaps', false)
59// VP Script requires an explicit resolution. Unlike Pine's empty timeframe,
60// an empty request is rejected rather than guessed from chart state.
61const fvgTimeframe = input.string('FVG Timeframe', '60', {
62 options: ['1', '5', '15', '30', '60', '240', '1D', '1W'],
63})
64const autoFvgThreshold = input.bool('FVG Auto Threshold', true)
65const bullishFvgInput = input.color('Bullish FVG', '#00ff68')
66const bearishFvgInput = input.color('Bearish FVG', '#ff0008')
67const fvgExtend = input.int('Extend FVG (bars)', 1, { min: 0, max: 100 })
68
69const showDaily = input.bool('Previous Daily High/Low', false)
70const mtfStyle = input.string('MTF Level Style', 'Solid', { options: ['Solid', 'Dashed', 'Dotted'] })
71const dailyColor = input.color('Daily Level Color', '#2157f3')
72const showWeekly = input.bool('Previous Weekly High/Low', false)
73const weeklyColor = input.color('Weekly Level Color', '#2157f3')
74const showMonthly = input.bool('Previous Monthly High/Low', false)
75const monthlyColor = input.color('Monthly Level Color', '#2157f3')
76
77const showZones = input.bool('Premium/Discount Zones', false)
78const premiumInput = input.color('Premium Zone', '#F23645')
79const equilibriumInput = input.color('Equilibrium Zone', '#878b94')
80const discountInput = input.color('Discount Zone', '#089981')
81
82// ─────────────────────────────────────────────────────────────────────────────
83// Constants and small helpers
84// ─────────────────────────────────────────────────────────────────────────────
85
86const BULLISH = 1
87const BEARISH = -1
88const GREEN = '#089981'
89const RED = '#F23645'
90const MONO_BULLISH = '#b2b5be'
91const MONO_BEARISH = '#5d606b'
92
93const swingBull = style === 'Monochrome' ? MONO_BULLISH : swingBullInput
94const swingBear = style === 'Monochrome' ? MONO_BEARISH : swingBearInput
95const internalBull = style === 'Monochrome' ? MONO_BULLISH : internalBullInput
96const internalBear = style === 'Monochrome' ? MONO_BEARISH : internalBearInput
97const withAlpha = (color, suffix) => color[0] === '#' && color.length === 7 ? color + suffix : color
98const bullishFvgColor = withAlpha(style === 'Monochrome' ? MONO_BULLISH : bullishFvgInput, '4d')
99const bearishFvgColor = withAlpha(style === 'Monochrome' ? MONO_BEARISH : bearishFvgInput, '4d')
100const premiumColor = style === 'Monochrome' ? MONO_BEARISH : premiumInput
101const discountColor = style === 'Monochrome' ? MONO_BULLISH : discountInput
102
103const labelSize = (name) => name === 'Tiny' ? 9 : name === 'Normal' ? 13 : 11
104const lineStyle = (name) => name === 'Dashed' ? 'dashed' : name === 'Dotted' ? 'dotted' : 'solid'
105const accepts = (choice, tag) => choice === 'All' || choice === tag
106const finite = (v) => typeof v === 'number' && Number.isFinite(v)
107const same = (a, b) => finite(a) && finite(b) && Math.abs(a - b) <= Math.max(1, Math.abs(a), Math.abs(b)) * 1e-12
108const finitePoint = (point) => finite(point.index) && finite(point.price)
109const emitBox = (from, to, options) => {
110 if (!finitePoint(from) || !finitePoint(to)) return
111 box(from, to, options)
112}
113
114const makePivot = () => ({
115 current: null,
116 last: null,
117 crossed: false,
118 index: -1,
119 seriesPrev: null,
120})
121
122const swingHigh = makePivot()
123const swingLow = makePivot()
124const internalHigh = makePivot()
125const internalLow = makePivot()
126const equalHigh = makePivot()
127const equalLow = makePivot()
128
129let swingTrend = 0
130let internalTrend = 0
131let swingLeg = 0
132let internalLeg = 0
133let equalLeg = 0
134
135const parsedHighs = []
136const parsedLows = []
137const rawHighs = []
138const rawLows = []
139const internalOrderBlocks = []
140const swingOrderBlocks = []
141const fairValueGaps = []
142
143const historicalLines = []
144const historicalLabels = []
145const presentLines = new Map()
146const presentLabels = new Map()
147
148const rememberLine = (key, value) => {
149 if (mode === 'Present') presentLines.set(key, value)
150 else historicalLines.push(value)
151}
152const rememberLabel = (key, value) => {
153 if (mode === 'Present') presentLabels.set(key, value)
154 else historicalLabels.push(value)
155}
156
157const alertSeries = {
158 internalBullishBOS: new Array(barCount).fill(0),
159 internalBullishCHoCH: new Array(barCount).fill(0),
160 internalBearishBOS: new Array(barCount).fill(0),
161 internalBearishCHoCH: new Array(barCount).fill(0),
162 swingBullishBOS: new Array(barCount).fill(0),
163 swingBullishCHoCH: new Array(barCount).fill(0),
164 swingBearishBOS: new Array(barCount).fill(0),
165 swingBearishCHoCH: new Array(barCount).fill(0),
166 internalBullishOrderBlock: new Array(barCount).fill(0),
167 internalBearishOrderBlock: new Array(barCount).fill(0),
168 swingBullishOrderBlock: new Array(barCount).fill(0),
169 swingBearishOrderBlock: new Array(barCount).fill(0),
170 equalHighs: new Array(barCount).fill(0),
171 equalLows: new Array(barCount).fill(0),
172 bullishFairValueGap: new Array(barCount).fill(0),
173 bearishFairValueGap: new Array(barCount).fill(0),
174}
175
176const trendSeries = new Array(barCount).fill(null)
177const candleColors = new Array(barCount).fill(null)
178const atr = ta.atr(200)
179
180// Each call sharing a symbol/resolution is one foreign context, so the FVG
181// fields below consume one of VP Script's four context slots, not five.
182const fvgOpen = showFvg ? request.security('', fvgTimeframe, (f) => f.open) : null
183const fvgHigh = showFvg ? request.security('', fvgTimeframe, (f) => f.high) : null
184const fvgLow = showFvg ? request.security('', fvgTimeframe, (f) => f.low) : null
185const fvgClose = showFvg ? request.security('', fvgTimeframe, (f) => f.close) : null
186const fvgTime = showFvg ? request.security('', fvgTimeframe, (f) => f.bars.map((b) => b.time)) : null
187
188const dailyHigh = showDaily ? request.security('', '1D', (f) => f.high) : null
189const dailyLow = showDaily ? request.security('', '1D', (f) => f.low) : null
190const weeklyHigh = showWeekly ? request.security('', '1W', (f) => f.high) : null
191const weeklyLow = showWeekly ? request.security('', '1W', (f) => f.low) : null
192const monthlyHigh = showMonthly ? request.security('', '1M', (f) => f.high) : null
193const monthlyLow = showMonthly ? request.security('', '1M', (f) => f.low) : null
194
195let trailingTop = null
196let trailingBottom = null
197let trailingTopIndex = -1
198let trailingBottomIndex = -1
199let trailingBaseIndex = -1
200
201let trueRangeTotal = 0
202let fvgDeltaTotal = 0
203let fvgDeltaCount = 0
204let lastForeignTime = null
205const completedForeignBars = []
206
207const highestBetween = (from, to) => {
208 let value = -Infinity
209 for (let i = from; i <= to; i++) if (rawHighs[i] > value) value = rawHighs[i]
210 return value
211}
212const lowestBetween = (from, to) => {
213 let value = Infinity
214 for (let i = from; i <= to; i++) if (rawLows[i] < value) value = rawLows[i]
215 return value
216}
217
218const updatePivotFamily = (i, size, family, equalOnly, internal) => {
219 if (i < size) return family.leg
220
221 const candidate = i - size
222 if (!finite(rawHighs[candidate]) || !finite(rawLows[candidate])) return family.leg
223 let recentHigh = -Infinity
224 let recentLow = Infinity
225 for (let j = candidate + 1; j <= i; j++) {
226 if (finite(rawHighs[j]) && rawHighs[j] > recentHigh) recentHigh = rawHighs[j]
227 if (finite(rawLows[j]) && rawLows[j] < recentLow) recentLow = rawLows[j]
228 }
229 if (!finite(recentHigh) || !finite(recentLow)) return family.leg
230
231 let nextLeg = family.leg
232 if (rawHighs[candidate] > recentHigh) nextLeg = 0
233 else if (rawLows[candidate] < recentLow) nextLeg = 1
234 if (nextLeg === family.leg) return nextLeg
235
236 const isLow = nextLeg === 1
237 const pivot = isLow
238 ? (equalOnly ? equalLow : internal ? internalLow : swingLow)
239 : (equalOnly ? equalHigh : internal ? internalHigh : swingHigh)
240 const level = isLow ? rawLows[candidate] : rawHighs[candidate]
241
242 if (equalOnly && finite(pivot.current) && finite(atr[i]) &&
243 Math.abs(pivot.current - level) < equalThreshold * atr[i]) {
244 const tag = isLow ? 'EQL' : 'EQH'
245 const color = isLow ? swingBull : swingBear
246 const middle = Math.round((pivot.index + candidate) / 2)
247 rememberLine(tag, {
248 from: { index: pivot.index, price: pivot.current },
249 to: { index: candidate, price: level },
250 options: { color, width: 1, lineStyle: 'dotted' },
251 })
252 rememberLabel(tag, {
253 index: middle, price: level, text: tag,
254 options: { color, background: '#00000000', size: labelSize(structureLabelSize), align: 'center' },
255 })
256 alertSeries[isLow ? 'equalLows' : 'equalHighs'][i] = 1
257 }
258
259 pivot.last = pivot.current
260 pivot.current = level
261 pivot.crossed = false
262 pivot.index = candidate
263
264 if (!equalOnly && !internal) {
265 trailingBaseIndex = candidate
266 if (isLow) {
267 trailingBottom = level
268 trailingBottomIndex = candidate
269 } else {
270 trailingTop = level
271 trailingTopIndex = candidate
272 }
273 if (showSwings) {
274 const tag = isLow
275 ? (finite(pivot.last) && level < pivot.last ? 'LL' : 'HL')
276 : (finite(pivot.last) && level > pivot.last ? 'HH' : 'LH')
277 rememberLabel(isLow ? 'swing-low' : 'swing-high', {
278 index: candidate, price: level, text: tag,
279 options: {
280 color: isLow ? swingBull : swingBear,
281 background: '#00000000', size: 10, align: 'center',
282 },
283 })
284 }
285 }
286 return nextLeg
287}
288
289const storeOrderBlock = (pivot, internal, bias, currentIndex) => {
290 if (pivot.index < 0 || pivot.index >= currentIndex) return
291 let chosen = -1
292 if (bias === BEARISH) {
293 let best = -Infinity
294 for (let i = pivot.index; i < currentIndex; i++) {
295 if (!finite(parsedHighs[i]) || !finite(parsedLows[i])) continue
296 if (parsedHighs[i] > best) { best = parsedHighs[i]; chosen = i }
297 }
298 } else {
299 let best = Infinity
300 for (let i = pivot.index; i < currentIndex; i++) {
301 if (!finite(parsedHighs[i]) || !finite(parsedLows[i])) continue
302 if (parsedLows[i] < best) { best = parsedLows[i]; chosen = i }
303 }
304 }
305 if (chosen < 0 || !finite(parsedHighs[chosen]) || !finite(parsedLows[chosen])) return
306 const target = internal ? internalOrderBlocks : swingOrderBlocks
307 target.unshift({ high: parsedHighs[chosen], low: parsedLows[chosen], index: chosen, bias })
308 if (target.length > 100) target.length = 100
309}
310
311const drawStructureEvent = (pivot, tag, color, internal, bullish, i) => {
312 const channel = (internal ? 'internal-' : 'swing-') + (bullish ? 'bull' : 'bear')
313 rememberLine(channel, {
314 from: { index: pivot.index, price: pivot.current },
315 to: { index: i, price: pivot.current },
316 options: { color, width: 1, lineStyle: internal ? 'dashed' : 'solid' },
317 })
318 rememberLabel(channel, {
319 index: Math.round((pivot.index + i) / 2), price: pivot.current, text: tag,
320 options: {
321 color, background: '#00000000',
322 size: labelSize(structureLabelSize), align: 'center',
323 },
324 })
325}
326
327const processStructure = (i, internal) => {
328 const highPivot = internal ? internalHigh : swingHigh
329 const lowPivot = internal ? internalLow : swingLow
330 const previousClose = i > 0 ? bars[i - 1].close : null
331 const upperWick = bars[i].high - Math.max(bars[i].close, bars[i].open)
332 const lowerWick = Math.min(bars[i].close, bars[i].open) - bars[i].low
333 const bullishBar = !internalConfluence || upperWick > lowerWick
334 const bearishBar = !internalConfluence || upperWick < lowerWick
335
336 const highPrevious = highPivot.seriesPrev
337 const bullCross = finite(highPivot.current) && finite(highPrevious) && finite(previousClose) &&
338 bars[i].close > highPivot.current && previousClose <= highPrevious && !highPivot.crossed &&
339 (!internal || (highPivot.current !== swingHigh.current && bullishBar))
340
341 if (bullCross) {
342 const trendBefore = internal ? internalTrend : swingTrend
343 const tag = trendBefore === BEARISH ? 'CHoCH' : 'BOS'
344 highPivot.crossed = true
345 if (internal) internalTrend = BULLISH
346 else swingTrend = BULLISH
347 alertSeries[(internal ? 'internal' : 'swing') + 'Bullish' + tag][i] = 1
348
349 const display = internal
350 ? showInternals && accepts(internalBullMode, tag)
351 : showStructure && accepts(swingBullMode, tag)
352 if (display) drawStructureEvent(highPivot, tag, internal ? internalBull : swingBull, internal, true, i)
353 if ((internal && showInternalOb) || (!internal && showSwingOb)) {
354 storeOrderBlock(highPivot, internal, BULLISH, i)
355 }
356 }
357
358 const lowPrevious = lowPivot.seriesPrev
359 const bearCross = finite(lowPivot.current) && finite(lowPrevious) && finite(previousClose) &&
360 bars[i].close < lowPivot.current && previousClose >= lowPrevious && !lowPivot.crossed &&
361 (!internal || (lowPivot.current !== swingLow.current && bearishBar))
362
363 if (bearCross) {
364 const trendBefore = internal ? internalTrend : swingTrend
365 const tag = trendBefore === BULLISH ? 'CHoCH' : 'BOS'
366 lowPivot.crossed = true
367 if (internal) internalTrend = BEARISH
368 else swingTrend = BEARISH
369 alertSeries[(internal ? 'internal' : 'swing') + 'Bearish' + tag][i] = 1
370
371 const display = internal
372 ? showInternals && accepts(internalBearMode, tag)
373 : showStructure && accepts(swingBearMode, tag)
374 if (display) drawStructureEvent(lowPivot, tag, internal ? internalBear : swingBear, internal, false, i)
375 if ((internal && showInternalOb) || (!internal && showSwingOb)) {
376 storeOrderBlock(lowPivot, internal, BEARISH, i)
377 }
378 }
379}
380
381// ─────────────────────────────────────────────────────────────────────────────
382// One chronological pass
383// ─────────────────────────────────────────────────────────────────────────────
384
385for (let i = 0; i < barCount; i++) {
386 const b = bars[i]
387 const validBar = finite(b.open) && finite(b.high) && finite(b.low) && finite(b.close)
388 rawHighs.push(validBar ? b.high : null)
389 rawLows.push(validBar ? b.low : null)
390
391 // Malformed feed bars must remain gaps. Letting Infinity enter the running
392 // range or order-block search poisons all later state and can produce an
393 // invalid drawing corner.
394 if (!validBar) {
395 parsedHighs.push(null)
396 parsedLows.push(null)
397 trendSeries[i] = null
398 continue
399 }
400
401 const previousClose = i > 0 && finite(bars[i - 1].close) ? bars[i - 1].close : b.close
402 const tr = Math.max(b.high - b.low, Math.abs(b.high - previousClose), Math.abs(b.low - previousClose))
403 trueRangeTotal += tr
404 const cumulativeRange = trueRangeTotal / Math.max(1, i)
405 const volatility = obFilter === 'Atr' ? atr[i] : cumulativeRange
406 const highVolatility = finite(volatility) && (b.high - b.low) >= 2 * volatility
407 parsedHighs.push(highVolatility ? b.low : b.high)
408 parsedLows.push(highVolatility ? b.high : b.low)
409
410 if (!finite(trailingTop) || b.high > trailingTop) {
411 trailingTop = b.high
412 trailingTopIndex = i
413 }
414 if (!finite(trailingBottom) || b.low < trailingBottom) {
415 trailingBottom = b.low
416 trailingBottomIndex = i
417 }
418
419 // Pine colours the current candle with the bias known before this bar's
420 // close produces any new structure event.
421 trendSeries[i] = internalTrend
422 if (colorCandles) candleColors[i] = internalTrend === BULLISH ? swingBull : swingBear
423
424 swingLeg = updatePivotFamily(i, swingsLength, { leg: swingLeg }, false, false)
425 internalLeg = updatePivotFamily(i, 5, { leg: internalLeg }, false, true)
426 if (showEqual) equalLeg = updatePivotFamily(i, equalLength, { leg: equalLeg }, true, false)
427
428 if (showInternals || showInternalOb || colorCandles) processStructure(i, true)
429 if (showStructure || showSwingOb || showStrongWeak) processStructure(i, false)
430
431 // Order-block mitigation. A removal is also the corresponding breakout
432 // alert, matching the source indicator's alert semantics.
433 const bearishSource = obMitigation === 'Close' ? b.close : b.high
434 const bullishSource = obMitigation === 'Close' ? b.close : b.low
435 for (const pair of [[internalOrderBlocks, true], [swingOrderBlocks, false]]) {
436 const blocks = pair[0]
437 const internal = pair[1]
438 for (let j = blocks.length - 1; j >= 0; j--) {
439 const ob = blocks[j]
440 const crossed = (ob.bias === BEARISH && bearishSource > ob.high) ||
441 (ob.bias === BULLISH && bullishSource < ob.low)
442 if (!crossed) continue
443 blocks.splice(j, 1)
444 const key = (internal ? 'internal' : 'swing') +
445 (ob.bias === BULLISH ? 'Bullish' : 'Bearish') + 'OrderBlock'
446 alertSeries[key][i] = 1
447 }
448 }
449
450 // Existing fair-value gaps are removed before a new completed foreign bar
451 // may create one, mirroring Pine's execution order.
452 if (showFvg) {
453 for (let j = fairValueGaps.length - 1; j >= 0; j--) {
454 const gap = fairValueGaps[j]
455 if ((gap.bias === BULLISH && b.low < gap.bottom) ||
456 (gap.bias === BEARISH && b.high > gap.top)) fairValueGaps.splice(j, 1)
457 }
458
459 const ft = fvgTime[i]
460 if (finite(ft) && ft !== lastForeignTime && finite(fvgOpen[i]) && finite(fvgHigh[i]) &&
461 finite(fvgLow[i]) && finite(fvgClose[i])) {
462 completedForeignBars.push({
463 time: ft, open: fvgOpen[i], high: fvgHigh[i], low: fvgLow[i], close: fvgClose[i], index: i,
464 })
465 lastForeignTime = ft
466 if (completedForeignBars.length >= 3) {
467 const current = completedForeignBars[completedForeignBars.length - 1]
468 const previous = completedForeignBars[completedForeignBars.length - 2]
469 const twoBack = completedForeignBars[completedForeignBars.length - 3]
470 const delta = previous.open !== 0 ? (previous.close - previous.open) / (previous.open * 100) : 0
471 fvgDeltaTotal += Math.abs(delta)
472 fvgDeltaCount += 1
473 const threshold = autoFvgThreshold ? (fvgDeltaTotal / fvgDeltaCount) * 2 : 0
474 const width = Math.max(1, current.index - previous.index)
475 const right = current.index + fvgExtend * width
476
477 if (current.low > twoBack.high && previous.close > twoBack.high && delta > threshold) {
478 fairValueGaps.unshift({
479 top: current.low, bottom: twoBack.high, bias: BULLISH,
480 left: previous.index, right,
481 })
482 alertSeries.bullishFairValueGap[i] = 1
483 }
484 if (current.high < twoBack.low && previous.close < twoBack.low && -delta > threshold) {
485 fairValueGaps.unshift({
486 top: twoBack.low, bottom: current.high, bias: BEARISH,
487 left: previous.index, right,
488 })
489 alertSeries.bearishFairValueGap[i] = 1
490 }
491 if (fairValueGaps.length > 100) fairValueGaps.length = 100
492 }
493 }
494 }
495
496 // The previous value of a mutable pivot is the value Pine's crossover sees
497 // through the pivot series' [1] history on the next bar.
498 for (const pivot of [swingHigh, swingLow, internalHigh, internalLow, equalHigh, equalLow]) {
499 pivot.seriesPrev = pivot.current
500 }
501}
502
503// ─────────────────────────────────────────────────────────────────────────────
504// Current objects: order blocks, FVGs, strong/weak levels and zones
505// ─────────────────────────────────────────────────────────────────────────────
506
507const lastIndex = Math.max(0, barCount - 1)
508
509const drawOrderBlocks = (blocks, count, internal) => {
510 const visible = blocks.slice(0, count)
511 for (const ob of visible) {
512 const base = internal
513 ? (ob.bias === BEARISH ? internalBearObInput : internalBullObInput)
514 : (ob.bias === BEARISH ? swingBearObInput : swingBullObInput)
515 const color = withAlpha(
516 style === 'Monochrome' ? (ob.bias === BEARISH ? MONO_BEARISH : MONO_BULLISH) : base,
517 '33',
518 )
519 emitBox(
520 { index: ob.index, price: ob.high },
521 { index: lastIndex, price: ob.low },
522 { color: internal ? '#00000000' : color, fillColor: color },
523 )
524 }
525}
526
527if (barCount > 0 && showInternalOb) drawOrderBlocks(internalOrderBlocks, internalObCount, true)
528if (barCount > 0 && showSwingOb) drawOrderBlocks(swingOrderBlocks, swingObCount, false)
529
530if (barCount > 0 && showFvg) {
531 for (const gap of fairValueGaps) {
532 const middle = (gap.top + gap.bottom) / 2
533 const color = gap.bias === BULLISH ? bullishFvgColor : bearishFvgColor
534 emitBox(
535 { index: gap.left, price: gap.top },
536 { index: gap.right, price: middle },
537 { color, fillColor: color },
538 )
539 emitBox(
540 { index: gap.left, price: middle },
541 { index: gap.right, price: gap.bottom },
542 { color, fillColor: color },
543 )
544 }
545}
546
547if (barCount > 0 && showStrongWeak && finite(trailingTop) && finite(trailingBottom)) {
548 line(
549 { index: trailingTopIndex, price: trailingTop },
550 { index: lastIndex, price: trailingTop },
551 { color: swingBear, width: 1, extend: true },
552 )
553 label(lastIndex, trailingTop, swingTrend === BEARISH ? 'Strong High' : 'Weak High', {
554 color: swingBear, background: '#00000000', size: 9, align: 'right',
555 })
556 line(
557 { index: trailingBottomIndex, price: trailingBottom },
558 { index: lastIndex, price: trailingBottom },
559 { color: swingBull, width: 1, extend: true },
560 )
561 label(lastIndex, trailingBottom, swingTrend === BULLISH ? 'Strong Low' : 'Weak Low', {
562 color: swingBull, background: '#00000000', size: 9, align: 'right',
563 })
564}
565
566if (barCount > 0 && showZones && finite(trailingTop) && finite(trailingBottom)) {
567 const start = Math.max(0, trailingBaseIndex)
568 const equilibrium = (trailingTop + trailingBottom) / 2
569 const premiumBottom = 0.95 * trailingTop + 0.05 * trailingBottom
570 const equilibriumTop = 0.525 * trailingTop + 0.475 * trailingBottom
571 const equilibriumBottom = 0.525 * trailingBottom + 0.475 * trailingTop
572 const discountTop = 0.95 * trailingBottom + 0.05 * trailingTop
573
574 emitBox({ index: start, price: trailingTop }, { index: lastIndex, price: premiumBottom }, {
575 color: '#00000000', fillColor: withAlpha(premiumColor, '33'), text: 'Premium',
576 })
577 emitBox({ index: start, price: equilibriumTop }, { index: lastIndex, price: equilibriumBottom }, {
578 color: '#00000000', fillColor: withAlpha(equilibriumInput, '33'), text: 'Equilibrium',
579 })
580 emitBox({ index: start, price: discountTop }, { index: lastIndex, price: trailingBottom }, {
581 color: '#00000000', fillColor: withAlpha(discountColor, '33'), text: 'Discount',
582 })
583}
584
585// The causal security API exposes the last completed HTF high/low. Locate the
586// period that produced it on the chart, then anchor the line to the actual
587// extreme instead of pretending the level began at the current period open.
588const drawPreviousLevels = (highSeries, lowSeries, prefix, color, styleName) => {
589 if (barCount === 0 || !highSeries || !lowSeries) return
590 let end = barCount - 1
591 while (end >= 0 && (!finite(highSeries[end]) || !finite(lowSeries[end]))) end--
592 if (end < 0) return
593
594 const currentHigh = highSeries[end]
595 const currentLow = lowSeries[end]
596 let currentStart = end
597 while (currentStart > 0 && same(highSeries[currentStart - 1], currentHigh) &&
598 same(lowSeries[currentStart - 1], currentLow)) currentStart--
599
600 let previousStart = currentStart - 1
601 if (previousStart < 0) return
602 const olderHigh = highSeries[previousStart]
603 const olderLow = lowSeries[previousStart]
604 while (previousStart > 0 && same(highSeries[previousStart - 1], olderHigh) &&
605 same(lowSeries[previousStart - 1], olderLow)) previousStart--
606
607 const from = previousStart
608 const to = Math.max(previousStart, currentStart - 1)
609 let highIndex = from
610 let lowIndex = from
611 for (let i = from; i <= to; i++) {
612 if (bars[i].high > bars[highIndex].high) highIndex = i
613 if (bars[i].low < bars[lowIndex].low) lowIndex = i
614 }
615
616 line({ index: highIndex, price: currentHigh }, { index: lastIndex, price: currentHigh }, {
617 color, width: 1, lineStyle: lineStyle(styleName), extend: true,
618 })
619 label(lastIndex, currentHigh, 'P' + prefix + 'H', {
620 color, background: '#00000000', size: 10, align: 'right',
621 })
622 line({ index: lowIndex, price: currentLow }, { index: lastIndex, price: currentLow }, {
623 color, width: 1, lineStyle: lineStyle(styleName), extend: true,
624 })
625 label(lastIndex, currentLow, 'P' + prefix + 'L', {
626 color, background: '#00000000', size: 10, align: 'right',
627 })
628}
629
630if (showDaily) drawPreviousLevels(dailyHigh, dailyLow, 'D', dailyColor, mtfStyle)
631if (showWeekly) drawPreviousLevels(weeklyHigh, weeklyLow, 'W', weeklyColor, mtfStyle)
632if (showMonthly) drawPreviousLevels(monthlyHigh, monthlyLow, 'M', monthlyColor, mtfStyle)
633
634// Reserve room below VP Script's hard 500-line/500-label caps for the fixed
635// levels above. Long histories keep their newest structure drawings.
636const eventLines = mode === 'Present' ? [...presentLines.values()] : historicalLines.slice(-470)
637const eventLabels = mode === 'Present' ? [...presentLabels.values()] : historicalLabels.slice(-470)
638for (const item of eventLines) line(item.from, item.to, item.options)
639for (const item of eventLabels) label(item.index, item.price, item.text, item.options)
640
641if (colorCandles) barcolor(candleColors)
642plot(trendSeries, { title: 'Internal Trend', color: '#00000000', ignoreScale: true })
643
644// ─────────────────────────────────────────────────────────────────────────────
645// Alerts — same sixteen conditions as the source indicator
646// ─────────────────────────────────────────────────────────────────────────────
647
648alertcondition(alertSeries.internalBullishBOS, { title: 'Internal Bullish BOS', message: 'Internal Bullish BOS formed' })
649alertcondition(alertSeries.internalBullishCHoCH, { title: 'Internal Bullish CHoCH', message: 'Internal Bullish CHoCH formed' })
650alertcondition(alertSeries.internalBearishBOS, { title: 'Internal Bearish BOS', message: 'Internal Bearish BOS formed' })
651alertcondition(alertSeries.internalBearishCHoCH, { title: 'Internal Bearish CHoCH', message: 'Internal Bearish CHoCH formed' })
652alertcondition(alertSeries.swingBullishBOS, { title: 'Bullish BOS', message: 'Bullish BOS formed' })
653alertcondition(alertSeries.swingBullishCHoCH, { title: 'Bullish CHoCH', message: 'Bullish CHoCH formed' })
654alertcondition(alertSeries.swingBearishBOS, { title: 'Bearish BOS', message: 'Bearish BOS formed' })
655alertcondition(alertSeries.swingBearishCHoCH, { title: 'Bearish CHoCH', message: 'Bearish CHoCH formed' })
656alertcondition(alertSeries.internalBullishOrderBlock, { title: 'Bullish Internal OB Breakout', message: 'Price broke bullish internal OB' })
657alertcondition(alertSeries.internalBearishOrderBlock, { title: 'Bearish Internal OB Breakout', message: 'Price broke bearish internal OB' })
658alertcondition(alertSeries.swingBullishOrderBlock, { title: 'Bullish Swing OB Breakout', message: 'Price broke bullish swing OB' })
659alertcondition(alertSeries.swingBearishOrderBlock, { title: 'Bearish Swing OB Breakout', message: 'Price broke bearish swing OB' })
660alertcondition(alertSeries.equalHighs, { title: 'Equal Highs', message: 'Equal highs detected' })
661alertcondition(alertSeries.equalLows, { title: 'Equal Lows', message: 'Equal lows detected' })
662alertcondition(alertSeries.bullishFairValueGap, { title: 'Bullish FVG', message: 'Bullish FVG formed' })
663alertcondition(alertSeries.bearishFairValueGap, { title: 'Bearish FVG', message: 'Bearish FVG formed' })
664

Versions

  • v1current8123c9023 months ago

This indicator runs in an isolated sandbox: it cannot reach your account, your data or the network, and if it stops responding it is stopped for you. That does not make it correct — nobody has checked whether it is any good, which is why its source is above.