Skip to content

SMC

by MeMyselfAndI · 2 in use · published 3 months ago

Loading SMC on BTC-USDT 1h…

Source

sha256 3f2af8d75066d716

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' ? 8 : name === 'Normal' ? 12 : 10
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)
179const LABEL_SPACING_ATR = 0.24
180
181// VP labels are anchored directly at a price; unlike Pine's label_up/down
182// styles they do not automatically clear a horizontal line. Offset labels by
183// volatility so the spacing remains readable across instruments and zooms.
184const displacedLabelPrice = (price, index, above, multiplier) => {
185 if (!finite(price)) return price
186 let range = index >= 0 && index < barCount && finite(atr[index]) ? atr[index] : null
187 if (!finite(range) || range <= 0) {
188 const b = index >= 0 && index < barCount ? bars[index] : null
189 range = b && finite(b.high) && finite(b.low) ? Math.abs(b.high - b.low) : null
190 }
191 if (!finite(range) || range <= 0) range = Math.max(Math.abs(price) * 0.002, 0.000001)
192 const padding = range * LABEL_SPACING_ATR * multiplier
193 return above ? price + padding : price - padding
194}
195
196// Each call sharing a symbol/resolution is one foreign context, so the FVG
197// fields below consume one of VP Script's four context slots, not five.
198const fvgOpen = showFvg ? request.security('', fvgTimeframe, (f) => f.open) : null
199const fvgHigh = showFvg ? request.security('', fvgTimeframe, (f) => f.high) : null
200const fvgLow = showFvg ? request.security('', fvgTimeframe, (f) => f.low) : null
201const fvgClose = showFvg ? request.security('', fvgTimeframe, (f) => f.close) : null
202const fvgTime = showFvg ? request.security('', fvgTimeframe, (f) => f.bars.map((b) => b.time)) : null
203
204const dailyHigh = showDaily ? request.security('', '1D', (f) => f.high) : null
205const dailyLow = showDaily ? request.security('', '1D', (f) => f.low) : null
206const weeklyHigh = showWeekly ? request.security('', '1W', (f) => f.high) : null
207const weeklyLow = showWeekly ? request.security('', '1W', (f) => f.low) : null
208const monthlyHigh = showMonthly ? request.security('', '1M', (f) => f.high) : null
209const monthlyLow = showMonthly ? request.security('', '1M', (f) => f.low) : null
210
211let trailingTop = null
212let trailingBottom = null
213let trailingTopIndex = -1
214let trailingBottomIndex = -1
215let trailingBaseIndex = -1
216
217let trueRangeTotal = 0
218let fvgDeltaTotal = 0
219let fvgDeltaCount = 0
220let lastForeignTime = null
221const completedForeignBars = []
222
223const highestBetween = (from, to) => {
224 let value = -Infinity
225 for (let i = from; i <= to; i++) if (rawHighs[i] > value) value = rawHighs[i]
226 return value
227}
228const lowestBetween = (from, to) => {
229 let value = Infinity
230 for (let i = from; i <= to; i++) if (rawLows[i] < value) value = rawLows[i]
231 return value
232}
233
234const updatePivotFamily = (i, size, family, equalOnly, internal) => {
235 if (i < size) return family.leg
236
237 const candidate = i - size
238 if (!finite(rawHighs[candidate]) || !finite(rawLows[candidate])) return family.leg
239 let recentHigh = -Infinity
240 let recentLow = Infinity
241 for (let j = candidate + 1; j <= i; j++) {
242 if (finite(rawHighs[j]) && rawHighs[j] > recentHigh) recentHigh = rawHighs[j]
243 if (finite(rawLows[j]) && rawLows[j] < recentLow) recentLow = rawLows[j]
244 }
245 if (!finite(recentHigh) || !finite(recentLow)) return family.leg
246
247 let nextLeg = family.leg
248 if (rawHighs[candidate] > recentHigh) nextLeg = 0
249 else if (rawLows[candidate] < recentLow) nextLeg = 1
250 if (nextLeg === family.leg) return nextLeg
251
252 const isLow = nextLeg === 1
253 const pivot = isLow
254 ? (equalOnly ? equalLow : internal ? internalLow : swingLow)
255 : (equalOnly ? equalHigh : internal ? internalHigh : swingHigh)
256 const level = isLow ? rawLows[candidate] : rawHighs[candidate]
257
258 if (equalOnly && finite(pivot.current) && finite(atr[i]) &&
259 Math.abs(pivot.current - level) < equalThreshold * atr[i]) {
260 const tag = isLow ? 'EQL' : 'EQH'
261 const color = isLow ? swingBull : swingBear
262 const middle = Math.round((pivot.index + candidate) / 2)
263 rememberLine(tag, {
264 from: { index: pivot.index, price: pivot.current },
265 to: { index: candidate, price: level },
266 options: { color, width: 1, lineStyle: 'dotted' },
267 })
268 rememberLabel(tag, {
269 index: middle, price: displacedLabelPrice(level, middle, !isLow, 1.6), text: tag,
270 options: { color, background: '#00000000', size: labelSize(structureLabelSize), align: 'center' },
271 })
272 alertSeries[isLow ? 'equalLows' : 'equalHighs'][i] = 1
273 }
274
275 pivot.last = pivot.current
276 pivot.current = level
277 pivot.crossed = false
278 pivot.index = candidate
279
280 if (!equalOnly && !internal) {
281 trailingBaseIndex = candidate
282 if (isLow) {
283 trailingBottom = level
284 trailingBottomIndex = candidate
285 } else {
286 trailingTop = level
287 trailingTopIndex = candidate
288 }
289 if (showSwings) {
290 const tag = isLow
291 ? (finite(pivot.last) && level < pivot.last ? 'LL' : 'HL')
292 : (finite(pivot.last) && level > pivot.last ? 'HH' : 'LH')
293 rememberLabel(isLow ? 'swing-low' : 'swing-high', {
294 index: candidate, price: displacedLabelPrice(level, candidate, !isLow, 1), text: tag,
295 options: {
296 color: isLow ? swingBull : swingBear,
297 background: '#00000000', size: 9, align: 'center',
298 },
299 })
300 }
301 }
302 return nextLeg
303}
304
305const storeOrderBlock = (pivot, internal, bias, currentIndex) => {
306 if (pivot.index < 0 || pivot.index >= currentIndex) return
307 let chosen = -1
308 if (bias === BEARISH) {
309 let best = -Infinity
310 for (let i = pivot.index; i < currentIndex; i++) {
311 if (!finite(parsedHighs[i]) || !finite(parsedLows[i])) continue
312 if (parsedHighs[i] > best) { best = parsedHighs[i]; chosen = i }
313 }
314 } else {
315 let best = Infinity
316 for (let i = pivot.index; i < currentIndex; i++) {
317 if (!finite(parsedHighs[i]) || !finite(parsedLows[i])) continue
318 if (parsedLows[i] < best) { best = parsedLows[i]; chosen = i }
319 }
320 }
321 if (chosen < 0 || !finite(parsedHighs[chosen]) || !finite(parsedLows[chosen])) return
322 const target = internal ? internalOrderBlocks : swingOrderBlocks
323 target.unshift({ high: parsedHighs[chosen], low: parsedLows[chosen], index: chosen, bias })
324 if (target.length > 100) target.length = 100
325}
326
327const drawStructureEvent = (pivot, tag, color, internal, bullish, i) => {
328 const channel = (internal ? 'internal-' : 'swing-') + (bullish ? 'bull' : 'bear')
329 rememberLine(channel, {
330 from: { index: pivot.index, price: pivot.current },
331 to: { index: i, price: pivot.current },
332 options: { color, width: 1, lineStyle: internal ? 'dashed' : 'solid' },
333 })
334 rememberLabel(channel, {
335 index: Math.round((pivot.index + i) / 2),
336 price: displacedLabelPrice(pivot.current, Math.round((pivot.index + i) / 2), bullish, 1),
337 text: tag,
338 options: {
339 color, background: '#00000000',
340 size: labelSize(structureLabelSize), align: 'center',
341 },
342 })
343}
344
345const processStructure = (i, internal) => {
346 const highPivot = internal ? internalHigh : swingHigh
347 const lowPivot = internal ? internalLow : swingLow
348 const previousClose = i > 0 ? bars[i - 1].close : null
349 const upperWick = bars[i].high - Math.max(bars[i].close, bars[i].open)
350 const lowerWick = Math.min(bars[i].close, bars[i].open) - bars[i].low
351 const bullishBar = !internalConfluence || upperWick > lowerWick
352 const bearishBar = !internalConfluence || upperWick < lowerWick
353
354 const highPrevious = highPivot.seriesPrev
355 const bullCross = finite(highPivot.current) && finite(highPrevious) && finite(previousClose) &&
356 bars[i].close > highPivot.current && previousClose <= highPrevious && !highPivot.crossed &&
357 (!internal || (highPivot.current !== swingHigh.current && bullishBar))
358
359 if (bullCross) {
360 const trendBefore = internal ? internalTrend : swingTrend
361 const tag = trendBefore === BEARISH ? 'CHoCH' : 'BOS'
362 highPivot.crossed = true
363 if (internal) internalTrend = BULLISH
364 else swingTrend = BULLISH
365 alertSeries[(internal ? 'internal' : 'swing') + 'Bullish' + tag][i] = 1
366
367 const display = internal
368 ? showInternals && accepts(internalBullMode, tag)
369 : showStructure && accepts(swingBullMode, tag)
370 if (display) drawStructureEvent(highPivot, tag, internal ? internalBull : swingBull, internal, true, i)
371 if ((internal && showInternalOb) || (!internal && showSwingOb)) {
372 storeOrderBlock(highPivot, internal, BULLISH, i)
373 }
374 }
375
376 const lowPrevious = lowPivot.seriesPrev
377 const bearCross = finite(lowPivot.current) && finite(lowPrevious) && finite(previousClose) &&
378 bars[i].close < lowPivot.current && previousClose >= lowPrevious && !lowPivot.crossed &&
379 (!internal || (lowPivot.current !== swingLow.current && bearishBar))
380
381 if (bearCross) {
382 const trendBefore = internal ? internalTrend : swingTrend
383 const tag = trendBefore === BULLISH ? 'CHoCH' : 'BOS'
384 lowPivot.crossed = true
385 if (internal) internalTrend = BEARISH
386 else swingTrend = BEARISH
387 alertSeries[(internal ? 'internal' : 'swing') + 'Bearish' + tag][i] = 1
388
389 const display = internal
390 ? showInternals && accepts(internalBearMode, tag)
391 : showStructure && accepts(swingBearMode, tag)
392 if (display) drawStructureEvent(lowPivot, tag, internal ? internalBear : swingBear, internal, false, i)
393 if ((internal && showInternalOb) || (!internal && showSwingOb)) {
394 storeOrderBlock(lowPivot, internal, BEARISH, i)
395 }
396 }
397}
398
399// ─────────────────────────────────────────────────────────────────────────────
400// One chronological pass
401// ─────────────────────────────────────────────────────────────────────────────
402
403for (let i = 0; i < barCount; i++) {
404 const b = bars[i]
405 const validBar = finite(b.open) && finite(b.high) && finite(b.low) && finite(b.close)
406 rawHighs.push(validBar ? b.high : null)
407 rawLows.push(validBar ? b.low : null)
408
409 // Malformed feed bars must remain gaps. Letting Infinity enter the running
410 // range or order-block search poisons all later state and can produce an
411 // invalid drawing corner.
412 if (!validBar) {
413 parsedHighs.push(null)
414 parsedLows.push(null)
415 trendSeries[i] = null
416 continue
417 }
418
419 const previousClose = i > 0 && finite(bars[i - 1].close) ? bars[i - 1].close : b.close
420 const tr = Math.max(b.high - b.low, Math.abs(b.high - previousClose), Math.abs(b.low - previousClose))
421 trueRangeTotal += tr
422 const cumulativeRange = trueRangeTotal / Math.max(1, i)
423 const volatility = obFilter === 'Atr' ? atr[i] : cumulativeRange
424 const highVolatility = finite(volatility) && (b.high - b.low) >= 2 * volatility
425 parsedHighs.push(highVolatility ? b.low : b.high)
426 parsedLows.push(highVolatility ? b.high : b.low)
427
428 if (!finite(trailingTop) || b.high > trailingTop) {
429 trailingTop = b.high
430 trailingTopIndex = i
431 }
432 if (!finite(trailingBottom) || b.low < trailingBottom) {
433 trailingBottom = b.low
434 trailingBottomIndex = i
435 }
436
437 // Pine colours the current candle with the bias known before this bar's
438 // close produces any new structure event.
439 trendSeries[i] = internalTrend
440 if (colorCandles) candleColors[i] = internalTrend === BULLISH ? swingBull : swingBear
441
442 swingLeg = updatePivotFamily(i, swingsLength, { leg: swingLeg }, false, false)
443 internalLeg = updatePivotFamily(i, 5, { leg: internalLeg }, false, true)
444 if (showEqual) equalLeg = updatePivotFamily(i, equalLength, { leg: equalLeg }, true, false)
445
446 if (showInternals || showInternalOb || colorCandles) processStructure(i, true)
447 if (showStructure || showSwingOb || showStrongWeak) processStructure(i, false)
448
449 // Order-block mitigation. A removal is also the corresponding breakout
450 // alert, matching the source indicator's alert semantics.
451 const bearishSource = obMitigation === 'Close' ? b.close : b.high
452 const bullishSource = obMitigation === 'Close' ? b.close : b.low
453 for (const pair of [[internalOrderBlocks, true], [swingOrderBlocks, false]]) {
454 const blocks = pair[0]
455 const internal = pair[1]
456 for (let j = blocks.length - 1; j >= 0; j--) {
457 const ob = blocks[j]
458 const crossed = (ob.bias === BEARISH && bearishSource > ob.high) ||
459 (ob.bias === BULLISH && bullishSource < ob.low)
460 if (!crossed) continue
461 blocks.splice(j, 1)
462 const key = (internal ? 'internal' : 'swing') +
463 (ob.bias === BULLISH ? 'Bullish' : 'Bearish') + 'OrderBlock'
464 alertSeries[key][i] = 1
465 }
466 }
467
468 // Existing fair-value gaps are removed before a new completed foreign bar
469 // may create one, mirroring Pine's execution order.
470 if (showFvg) {
471 for (let j = fairValueGaps.length - 1; j >= 0; j--) {
472 const gap = fairValueGaps[j]
473 if ((gap.bias === BULLISH && b.low < gap.bottom) ||
474 (gap.bias === BEARISH && b.high > gap.top)) fairValueGaps.splice(j, 1)
475 }
476
477 const ft = fvgTime[i]
478 if (finite(ft) && ft !== lastForeignTime && finite(fvgOpen[i]) && finite(fvgHigh[i]) &&
479 finite(fvgLow[i]) && finite(fvgClose[i])) {
480 completedForeignBars.push({
481 time: ft, open: fvgOpen[i], high: fvgHigh[i], low: fvgLow[i], close: fvgClose[i], index: i,
482 })
483 lastForeignTime = ft
484 if (completedForeignBars.length >= 3) {
485 const current = completedForeignBars[completedForeignBars.length - 1]
486 const previous = completedForeignBars[completedForeignBars.length - 2]
487 const twoBack = completedForeignBars[completedForeignBars.length - 3]
488 const delta = previous.open !== 0 ? (previous.close - previous.open) / (previous.open * 100) : 0
489 fvgDeltaTotal += Math.abs(delta)
490 fvgDeltaCount += 1
491 const threshold = autoFvgThreshold ? (fvgDeltaTotal / fvgDeltaCount) * 2 : 0
492 const width = Math.max(1, current.index - previous.index)
493 const right = current.index + fvgExtend * width
494
495 if (current.low > twoBack.high && previous.close > twoBack.high && delta > threshold) {
496 fairValueGaps.unshift({
497 top: current.low, bottom: twoBack.high, bias: BULLISH,
498 left: previous.index, right,
499 })
500 alertSeries.bullishFairValueGap[i] = 1
501 }
502 if (current.high < twoBack.low && previous.close < twoBack.low && -delta > threshold) {
503 fairValueGaps.unshift({
504 top: twoBack.low, bottom: current.high, bias: BEARISH,
505 left: previous.index, right,
506 })
507 alertSeries.bearishFairValueGap[i] = 1
508 }
509 if (fairValueGaps.length > 100) fairValueGaps.length = 100
510 }
511 }
512 }
513
514 // The previous value of a mutable pivot is the value Pine's crossover sees
515 // through the pivot series' [1] history on the next bar.
516 for (const pivot of [swingHigh, swingLow, internalHigh, internalLow, equalHigh, equalLow]) {
517 pivot.seriesPrev = pivot.current
518 }
519}
520
521// ─────────────────────────────────────────────────────────────────────────────
522// Current objects: order blocks, FVGs, strong/weak levels and zones
523// ─────────────────────────────────────────────────────────────────────────────
524
525const lastIndex = Math.max(0, barCount - 1)
526
527const drawOrderBlocks = (blocks, count, internal) => {
528 const visible = blocks.slice(0, count)
529 for (const ob of visible) {
530 const base = internal
531 ? (ob.bias === BEARISH ? internalBearObInput : internalBullObInput)
532 : (ob.bias === BEARISH ? swingBearObInput : swingBullObInput)
533 const color = withAlpha(
534 style === 'Monochrome' ? (ob.bias === BEARISH ? MONO_BEARISH : MONO_BULLISH) : base,
535 '33',
536 )
537 emitBox(
538 { index: ob.index, price: ob.high },
539 { index: lastIndex, price: ob.low },
540 { color: internal ? '#00000000' : color, fillColor: color },
541 )
542 }
543}
544
545if (barCount > 0 && showInternalOb) drawOrderBlocks(internalOrderBlocks, internalObCount, true)
546if (barCount > 0 && showSwingOb) drawOrderBlocks(swingOrderBlocks, swingObCount, false)
547
548if (barCount > 0 && showFvg) {
549 for (const gap of fairValueGaps) {
550 const middle = (gap.top + gap.bottom) / 2
551 const color = gap.bias === BULLISH ? bullishFvgColor : bearishFvgColor
552 emitBox(
553 { index: gap.left, price: gap.top },
554 { index: gap.right, price: middle },
555 { color, fillColor: color },
556 )
557 emitBox(
558 { index: gap.left, price: middle },
559 { index: gap.right, price: gap.bottom },
560 { color, fillColor: color },
561 )
562 }
563}
564
565if (barCount > 0 && showStrongWeak && finite(trailingTop) && finite(trailingBottom)) {
566 line(
567 { index: trailingTopIndex, price: trailingTop },
568 { index: lastIndex, price: trailingTop },
569 { color: swingBear, width: 1, extend: true },
570 )
571 label(lastIndex, displacedLabelPrice(trailingTop, lastIndex, true, 1), swingTrend === BEARISH ? 'Strong High' : 'Weak High', {
572 color: swingBear, background: '#00000000', size: 8, align: 'right',
573 })
574 line(
575 { index: trailingBottomIndex, price: trailingBottom },
576 { index: lastIndex, price: trailingBottom },
577 { color: swingBull, width: 1, extend: true },
578 )
579 label(lastIndex, displacedLabelPrice(trailingBottom, lastIndex, false, 1), swingTrend === BULLISH ? 'Strong Low' : 'Weak Low', {
580 color: swingBull, background: '#00000000', size: 8, align: 'right',
581 })
582}
583
584if (barCount > 0 && showZones && finite(trailingTop) && finite(trailingBottom)) {
585 const start = Math.max(0, trailingBaseIndex)
586 const equilibrium = (trailingTop + trailingBottom) / 2
587 const premiumBottom = 0.95 * trailingTop + 0.05 * trailingBottom
588 const equilibriumTop = 0.525 * trailingTop + 0.475 * trailingBottom
589 const equilibriumBottom = 0.525 * trailingBottom + 0.475 * trailingTop
590 const discountTop = 0.95 * trailingBottom + 0.05 * trailingTop
591
592 emitBox({ index: start, price: trailingTop }, { index: lastIndex, price: premiumBottom }, {
593 color: '#00000000', fillColor: withAlpha(premiumColor, '33'), text: 'Premium',
594 })
595 emitBox({ index: start, price: equilibriumTop }, { index: lastIndex, price: equilibriumBottom }, {
596 color: '#00000000', fillColor: withAlpha(equilibriumInput, '33'), text: 'Equilibrium',
597 })
598 emitBox({ index: start, price: discountTop }, { index: lastIndex, price: trailingBottom }, {
599 color: '#00000000', fillColor: withAlpha(discountColor, '33'), text: 'Discount',
600 })
601}
602
603// The causal security API exposes the last completed HTF high/low. Locate the
604// period that produced it on the chart, then anchor the line to the actual
605// extreme instead of pretending the level began at the current period open.
606const drawPreviousLevels = (highSeries, lowSeries, prefix, color, styleName) => {
607 if (barCount === 0 || !highSeries || !lowSeries) return
608 let end = barCount - 1
609 while (end >= 0 && (!finite(highSeries[end]) || !finite(lowSeries[end]))) end--
610 if (end < 0) return
611
612 const currentHigh = highSeries[end]
613 const currentLow = lowSeries[end]
614 let currentStart = end
615 while (currentStart > 0 && same(highSeries[currentStart - 1], currentHigh) &&
616 same(lowSeries[currentStart - 1], currentLow)) currentStart--
617
618 let previousStart = currentStart - 1
619 if (previousStart < 0) return
620 const olderHigh = highSeries[previousStart]
621 const olderLow = lowSeries[previousStart]
622 while (previousStart > 0 && same(highSeries[previousStart - 1], olderHigh) &&
623 same(lowSeries[previousStart - 1], olderLow)) previousStart--
624
625 const from = previousStart
626 const to = Math.max(previousStart, currentStart - 1)
627 let highIndex = from
628 let lowIndex = from
629 for (let i = from; i <= to; i++) {
630 if (bars[i].high > bars[highIndex].high) highIndex = i
631 if (bars[i].low < bars[lowIndex].low) lowIndex = i
632 }
633
634 line({ index: highIndex, price: currentHigh }, { index: lastIndex, price: currentHigh }, {
635 color, width: 1, lineStyle: lineStyle(styleName), extend: true,
636 })
637 label(lastIndex, displacedLabelPrice(currentHigh, lastIndex, true, 1), 'P' + prefix + 'H', {
638 color, background: '#00000000', size: 9, align: 'right',
639 })
640 line({ index: lowIndex, price: currentLow }, { index: lastIndex, price: currentLow }, {
641 color, width: 1, lineStyle: lineStyle(styleName), extend: true,
642 })
643 label(lastIndex, displacedLabelPrice(currentLow, lastIndex, false, 1), 'P' + prefix + 'L', {
644 color, background: '#00000000', size: 9, align: 'right',
645 })
646}
647
648if (showDaily) drawPreviousLevels(dailyHigh, dailyLow, 'D', dailyColor, mtfStyle)
649if (showWeekly) drawPreviousLevels(weeklyHigh, weeklyLow, 'W', weeklyColor, mtfStyle)
650if (showMonthly) drawPreviousLevels(monthlyHigh, monthlyLow, 'M', monthlyColor, mtfStyle)
651
652// Reserve room below VP Script's hard 500-line/500-label caps for the fixed
653// levels above. Long histories keep their newest structure drawings.
654const eventLines = mode === 'Present' ? [...presentLines.values()] : historicalLines.slice(-470)
655const eventLabels = mode === 'Present' ? [...presentLabels.values()] : historicalLabels.slice(-470)
656for (const item of eventLines) line(item.from, item.to, item.options)
657for (const item of eventLabels) label(item.index, item.price, item.text, item.options)
658
659if (colorCandles) barcolor(candleColors)
660plot(trendSeries, { title: 'Internal Trend', color: '#00000000', ignoreScale: true })
661
662// ─────────────────────────────────────────────────────────────────────────────
663// Alerts — same sixteen conditions as the source indicator
664// ─────────────────────────────────────────────────────────────────────────────
665
666alertcondition(alertSeries.internalBullishBOS, { title: 'Internal Bullish BOS', message: 'Internal Bullish BOS formed' })
667alertcondition(alertSeries.internalBullishCHoCH, { title: 'Internal Bullish CHoCH', message: 'Internal Bullish CHoCH formed' })
668alertcondition(alertSeries.internalBearishBOS, { title: 'Internal Bearish BOS', message: 'Internal Bearish BOS formed' })
669alertcondition(alertSeries.internalBearishCHoCH, { title: 'Internal Bearish CHoCH', message: 'Internal Bearish CHoCH formed' })
670alertcondition(alertSeries.swingBullishBOS, { title: 'Bullish BOS', message: 'Bullish BOS formed' })
671alertcondition(alertSeries.swingBullishCHoCH, { title: 'Bullish CHoCH', message: 'Bullish CHoCH formed' })
672alertcondition(alertSeries.swingBearishBOS, { title: 'Bearish BOS', message: 'Bearish BOS formed' })
673alertcondition(alertSeries.swingBearishCHoCH, { title: 'Bearish CHoCH', message: 'Bearish CHoCH formed' })
674alertcondition(alertSeries.internalBullishOrderBlock, { title: 'Bullish Internal OB Breakout', message: 'Price broke bullish internal OB' })
675alertcondition(alertSeries.internalBearishOrderBlock, { title: 'Bearish Internal OB Breakout', message: 'Price broke bearish internal OB' })
676alertcondition(alertSeries.swingBullishOrderBlock, { title: 'Bullish Swing OB Breakout', message: 'Price broke bullish swing OB' })
677alertcondition(alertSeries.swingBearishOrderBlock, { title: 'Bearish Swing OB Breakout', message: 'Price broke bearish swing OB' })
678alertcondition(alertSeries.equalHighs, { title: 'Equal Highs', message: 'Equal highs detected' })
679alertcondition(alertSeries.equalLows, { title: 'Equal Lows', message: 'Equal lows detected' })
680alertcondition(alertSeries.bullishFairValueGap, { title: 'Bullish FVG', message: 'Bullish FVG formed' })
681alertcondition(alertSeries.bearishFairValueGap, { title: 'Bearish FVG', message: 'Bearish FVG formed' })
682

Versions

  • v5current3f2af8d73 months ago
  • v49dc30ca43 months ago
  • v36e7dae543 months ago
  • v1ee94b2123 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.