Library · Pine Script v6 · Library

SMC Profile Engine

by skipp-dev· code credited to preuss_steffen

SMC Profile Engine is a Pine Script library designed to build custom volume profiles on TradingView.

View on GitHub

Repaints

GitHub stars1on the repository
Pine versionv6current
Lines of code374
Settings0inputs
LicenceNone statedfile: MPL-2.0
Last update2 days ago26 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new library, select all of the template text and paste. Press Save and give it a name.
  4. 4Publish or import itA library draws nothing. Publish it from the editor, then import its functions in your own scripts.

Source Code

374 lines of Pine Script v6 from SMC++/smc_profile_engine.pine, published under MPL-2.0 (file header). Keep the author's credit and licence lines when you reuse it.

smc_profile_engine.pine
// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © preuss_steffen

//@version=6
// @description Volume profile engine: Bucket, ProfileConfig, Profile types and methods.
library("smc_profile_engine", overlay = true)

import preuss_steffen/smc_utils/4 as u
import preuss_steffen/smc_draw/3 as d

// ── Types ──

export type Bucket
    varip int idx
    varip float value
    varip float top
    varip float btm
    varip float center
    varip float fraction
    d.SmcLine plot_bucket_line = na

export method update(Bucket this, float top, float bottom, float value, float fraction) =>
    rounded_top = math.round_to_mintick(top)
    rounded_bottom = math.round_to_mintick(bottom)
    rounded_fraction = math.round(fraction, 2)
    this.top := rounded_top
    this.btm := rounded_bottom
    this.center := math.avg(rounded_top, rounded_bottom)
    this.value := value
    this.fraction := rounded_fraction
    this

export method update(Bucket this, float value, float fraction) =>
    this.value := value
    this.fraction := math.round(fraction, 2)
    this

export type ProfileConfig
    bool show_poc = true
    bool show_profile = false
    bool show_va = false
    bool show_background = false
    bool show_labels = false
    bool show_price_levels = false
    bool extend = false

    string poc_label_text = 'POC'
    d.LineArgs args_poc_line = na
    d.LineArgs args_vah_line = na
    d.LineArgs args_val_line = na
    d.LabelArgs args_poc_label = na
    d.LineArgs args_profile_line = na
    d.BoxArgs args_profile_bg = na
    bool initialized = false

export method init(ProfileConfig this) =>
    if na(this) ? false : not this.initialized
        if na(this.args_poc_line)
            this.args_poc_line := d.LineArgs.new(color.orange, line.style_dotted, 1, xloc = xloc.bar_index)
        if na(this.args_vah_line)
            this.args_vah_line := d.LineArgs.new(color.yellow, line.style_dotted, 1, xloc = xloc.bar_index)
        if na(this.args_val_line)
            this.args_val_line := d.LineArgs.new(color.yellow, line.style_dotted, 1, xloc = xloc.bar_index)
        if na(this.args_poc_label)
            this.args_poc_label := d.LabelArgs.new(color.orange, #00000000, xloc = xloc.bar_index, style = label.style_label_left)
        if na(this.args_profile_line)
            this.args_profile_line := d.LineArgs.new(color.rgb(120, 123, 134, 40), line.style_dotted, 1, xloc = xloc.bar_index)
        if na(this.args_profile_bg)
            this.args_profile_bg := d.BoxArgs.new(#00000000, bg_color = color.rgb(120, 123, 134, 92), xloc = xloc.bar_index)
        this.initialized := true
    this

export type Profile
    int id
    int resolution
    float vah_threshold_pc
    float val_threshold_pc
    varip float[] data_opens
    varip float[] data_highs
    varip float[] data_lows
    varip float[] data_closes
    varip float[] data_values
    varip float h = na
    varip float l = na
    varip float total = 0
    varip float total_up = 0
    varip float total_down = 0
    Bucket[] buckets
    varip int poc_bucket_index = na
    varip int vah_bucket_index = na
    varip int val_bucket_index = na
    varip float poc = na
    varip float vah = na
    varip float val = na
    d.SmcLine plot_poc = na
    d.SmcLine plot_vah = na
    d.SmcLine plot_val = na
    d.SmcLabel plot_poc_label = na
    d.SmcBox plot_profile_bg = na
    bool hidden = false

export method apply_style(Profile this, ProfileConfig args) =>
    if not na(this) and not na(args)
        if not na(this.plot_poc)
            this.plot_poc.apply_style(args.args_poc_line)
        if not na(this.plot_vah)
            this.plot_vah.apply_style(args.args_vah_line)
        if not na(this.plot_val)
            this.plot_val.apply_style(args.args_val_line)
        if not na(this.plot_poc_label)
            this.plot_poc_label.apply_style(args.args_poc_label)
        if not na(this.plot_profile_bg)
            this.plot_profile_bg.apply_style(args.args_profile_bg, d.BoxTextArgs.new())
    this

export method delete(Profile this) =>
    if not na(this)
        if not na(this.buckets)
            for bucket in this.buckets
                if not na(bucket.plot_bucket_line)
                    bucket.plot_bucket_line.delete()
                    bucket.plot_bucket_line := na
            array.clear(this.buckets)
            this.buckets := na
        if not na(this.plot_poc)
            this.plot_poc.delete()
            this.plot_poc := na
        if not na(this.plot_vah)
            this.plot_vah.delete()
            this.plot_vah := na
        if not na(this.plot_val)
            this.plot_val.delete()
            this.plot_val := na
        if not na(this.plot_poc_label)
            this.plot_poc_label.delete()
            this.plot_poc_label := na
        if not na(this.plot_profile_bg)
            this.plot_profile_bg.delete()
            this.plot_profile_bg := na
        if not na(this.data_opens)
            array.clear(this.data_opens)
        if not na(this.data_highs)
            array.clear(this.data_highs)
        if not na(this.data_lows)
            array.clear(this.data_lows)
        if not na(this.data_closes)
            array.clear(this.data_closes)
        if not na(this.data_values)
            array.clear(this.data_values)
        this.hidden := false
    this

export method hide(Profile this) =>
    if na(this) ? false : not this.hidden
        if not na(this.buckets)
            for bucket in this.buckets
                if not na(bucket.plot_bucket_line)
                    bucket.plot_bucket_line.hide()
        if not na(this.plot_poc)
            this.plot_poc.hide()
        if not na(this.plot_vah)
            this.plot_vah.hide()
        if not na(this.plot_val)
            this.plot_val.hide()
        if not na(this.plot_poc_label)
            this.plot_poc_label.hide()
        if not na(this.plot_profile_bg)
            this.plot_profile_bg.hide()
        this.hidden := true
    this

export method init(Profile this, bool update_buckets = false) =>
    if not na(this)
        create_buckets = na(this.buckets)
        if create_buckets or update_buckets
            float bucket_size = this.resolution > 0 ? (this.h - this.l) / this.resolution : 0
            if create_buckets
                this.buckets := array.new<Bucket>(this.resolution)
            for i = 0 to this.resolution - 1
                float bucket_bottom = this.l + i * bucket_size
                float bucket_top = bucket_bottom + bucket_size
                if create_buckets
                    this.buckets.set(i, Bucket.new(i, 0, bucket_top, bucket_bottom, math.avg(bucket_top, bucket_bottom), 0))
                else
                    this.buckets.get(i).update(bucket_top, bucket_bottom, 0, 0)
    this

export method calculate(Profile this, simple int use_open_close_data_for_ranges_shorter_than_bars = 4) =>
    int range_count = array.size(this.data_values)
    if not na(this.buckets)
        this.poc_bucket_index := na
        this.val_bucket_index := na
        this.vah_bucket_index := na
        this.poc := na
        this.val := na
        this.vah := na
        this.total_up := 0
        this.total_down := 0
        float poc_volume = 0
        float sum_volume_up = 0
        float sum_volume_down = 0
        int range_count_opens = na(this.data_opens) ? 0 : array.size(this.data_opens)
        int range_count_closes = na(this.data_closes) ? 0 : array.size(this.data_closes)

        if range_count <= 0 or this.total <= 0
            for i = 0 to this.resolution - 1
                this.buckets.get(i).update(0, 0)
        else
            for i = 0 to this.resolution - 1
                Bucket bucket = this.buckets.get(i)
                float bucket_value = 0
                for j = 0 to range_count - 1
                    float range_value = array.get(this.data_values, j)
                    float range_high = array.get(this.data_highs, j)
                    float range_low = array.get(this.data_lows, j)
                    float range_size = range_high - range_low
                    float overlap = math.max(0, math.min(range_high, bucket.top) - math.max(range_low, bucket.btm))
                    if (i == 0 or 0 < use_open_close_data_for_ranges_shorter_than_bars) and j < range_count_opens and j < range_count_closes
                        float range_open = array.get(this.data_opens, j)
                        float range_close = array.get(this.data_closes, j)
                        if i == 0
                            if range_open < range_close
                                sum_volume_up += range_value
                            else
                                sum_volume_down += range_value
                        if j < use_open_close_data_for_ranges_shorter_than_bars
                            overlap += math.max(0, math.min(math.max(range_open, range_close), bucket.top) - math.max(math.min(range_open, range_close), bucket.btm)) / 2
                            range_size += math.abs(range_open - range_close) / 2
                    // v4 (Review 12.10.2026 #9): Kerzen mit Hoch = Tief fielen durch (overlap immer 0). Jetzt dem Bucket zugeordnet,
                    // in dem der Preis liegt (unten inklusive, oben exklusiv; der oberste Bucket oben inklusive).
                    float range_value_fraction = range_size > 0 ? overlap / range_size : (range_low >= bucket.btm and (range_low < bucket.top or i == this.resolution - 1) ? 1 : 0)
                    bucket_value += range_value_fraction * range_value
                bucket.update(bucket_value, bucket_value / this.total)
                if bucket_value > 0 and (na(this.poc_bucket_index) or bucket_value >= poc_volume)
                    poc_volume := bucket_value
                    this.poc_bucket_index := i
                    this.poc := bucket.center
            if not na(this.poc_bucket_index)
                int left_va = this.poc_bucket_index
                int right_va = this.poc_bucket_index
                float va_sum = this.buckets.get(this.poc_bucket_index).value
                float va_target = this.total * math.max(this.vah_threshold_pc - this.val_threshold_pc, 0) / 100.0

                while va_sum < va_target and (left_va > 0 or right_va < this.resolution - 1)
                    float next_left = left_va > 0 ? this.buckets.get(left_va - 1).value : -1
                    float next_right = right_va < this.resolution - 1 ? this.buckets.get(right_va + 1).value : -1

                    if next_left > next_right
                        left_va -= 1
                        va_sum += next_left
                    else
                        right_va += 1
                        va_sum += next_right

                this.val_bucket_index := left_va
                this.vah_bucket_index := right_va
                this.val := this.buckets.get(left_va).center
                this.vah := this.buckets.get(right_va).center
            this.total_up := sum_volume_up
            this.total_down := sum_volume_down
    this

export method update(Profile this, float[] opens = na, float[] highs = na, float[] lows = na, float[] closes = na, float[] values = na) =>
    if not na(this)
        recalculation_required = false
        layout_required = false
        if not na(opens)
            this.data_opens := opens
            recalculation_required := true
        if not na(highs)
            this.data_highs := highs
            high_max = highs.max()
            layout_required := this.h != high_max
            this.h := high_max
            recalculation_required := true
        if not na(lows)
            this.data_lows := lows
            low_min = lows.min()
            layout_required := layout_required or this.l != low_min
            this.l := low_min
            recalculation_required := true
        if not na(closes)
            this.data_closes := closes
            recalculation_required := true
        if not na(values)
            this.data_values := values
            this.total := values.sum()
            recalculation_required := true
        if layout_required
            this.init(update_buckets = true)
        if recalculation_required or na(this.poc)
            this.calculate()
    this

export method draw(Profile this, ProfileConfig config, int left = na, int right = na, bool extend_only = true, simple bool force_overlay = false, string poc_text_override = na) =>
    if na(this) ? false : na(this.buckets) ? false : this.buckets.size() > 0
        config.init()
        this.init()
        if config.show_background
            if na(this.plot_profile_bg)
                this.plot_profile_bg := d.create_box(left, this.h, right, this.l, config.args_profile_bg.xloc)
            else
                this.plot_profile_bg.left_top.update(left, this.h, config.args_profile_bg.xloc)
                this.plot_profile_bg.right_bottom.update(right, this.l, config.args_profile_bg.xloc)
            this.plot_profile_bg.draw(config.args_profile_bg, na, extend_only, force_overlay)
        if config.show_poc and not na(this.poc)
            if na(this.plot_poc)
                this.plot_poc := d.create_line(left, this.poc, right, this.poc, config.args_poc_line.xloc)
            else
                this.plot_poc.start.update(left, this.poc, config.args_poc_line.xloc)
                this.plot_poc.end.update(right, this.poc, config.args_poc_line.xloc)
            this.plot_poc.draw(config.args_poc_line, extend_only, force_overlay)
            if config.show_labels
                tooltip = str.format('{0} ({1})', na(poc_text_override) ? config.poc_label_text : poc_text_override, this.poc)
                txt = config.show_price_levels ? tooltip : na(poc_text_override) ? config.poc_label_text : poc_text_override
                if na(this.plot_poc_label)
                    this.plot_poc_label := d.create_label(d.create_point(right, this.poc, config.args_poc_label.xloc), txt, tooltip)
                else
                    this.plot_poc_label.point.update(right, this.poc, config.args_poc_label.xloc)
                    this.plot_poc_label.txt := txt
                    this.plot_poc_label.tooltip := tooltip
                this.plot_poc_label.draw(config.args_poc_label, force_overlay)
        if config.show_va and not na(this.vah) and not na(this.val)
            if na(this.plot_vah)
                this.plot_vah := d.create_line(left, this.vah, right, this.vah, config.args_vah_line.xloc)
            else
                this.plot_vah.start.update(left, this.vah, config.args_vah_line.xloc)
                this.plot_vah.end.update(right, this.vah, config.args_vah_line.xloc)
            this.plot_vah.draw(config.args_vah_line, extend_only, force_overlay)
            if na(this.plot_val)
                this.plot_val := d.create_line(left, this.val, right, this.val, config.args_val_line.xloc)
            else
                this.plot_val.start.update(left, this.val, config.args_val_line.xloc)
                this.plot_val.end.update(right, this.val, config.args_val_line.xloc)
            this.plot_val.draw(config.args_val_line, extend_only, force_overlay)
        this.hidden := false
    this

// ── Helper functions ──

export normalize_profile_resolution(int resolution) =>
    math.max(1, resolution)

export normalize_profile_vah_pc(float vah_pc, float val_pc) =>
    math.max(0.0, math.min(100.0, math.max(vah_pc, val_pc)))

export normalize_profile_val_pc(float vah_pc, float val_pc) =>
    math.max(0.0, math.min(100.0, math.min(vah_pc, val_pc)))

export profile_data_ready(float[] highs, float[] lows, float[] values) =>
    not na(highs) and not na(lows) and not na(values) and array.size(highs) > 0 and array.size(lows) > 0 and array.size(values) > 0

export is_impulse_candle_now(float candle_body, float impulse_candle_size) =>
    candle_body >= impulse_candle_size

export is_indecision_candle_now(float high_now, float low_now, float prior_high, float prior_low, float candle_top_now, float candle_btm_now, float prior_candle_top, float prior_candle_btm, float prior_candle_body, float candle_body, float candle_open, float candle_close) =>
    u.in_range(high_now, low_now, prior_high, prior_low) and prior_candle_body > candle_body or u.in_range(candle_top_now, candle_btm_now, prior_candle_top, prior_candle_btm) or candle_open == candle_close

export profile_features_enabled(bool capture_profile_now, bool align_edge_to_value_area_now, bool align_break_price_to_poc_now) =>
    capture_profile_now or align_edge_to_value_area_now or align_break_price_to_poc_now

export create_profile(float[] opens, float[] highs, float[] lows, float[] closes, float[] values, int resolution = 25, float vah_pc = 80, float val_pc = 20, int idx = bar_index, bool init_calculated = true, simple int use_open_close_data_for_ranges_shorter_than_bars = 4) =>
    int profile_resolution = normalize_profile_resolution(resolution)
    float profile_vah_pc = normalize_profile_vah_pc(vah_pc, val_pc)
    float profile_val_pc = normalize_profile_val_pc(vah_pc, val_pc)
    bool profile_has_range_data = profile_data_ready(highs, lows, values)
    float profile_high = profile_has_range_data ? highs.max() : na
    float profile_low = profile_has_range_data ? lows.min() : na
    float profile_total = profile_has_range_data ? values.sum() : 0
    profile = Profile.new(idx, profile_resolution, profile_vah_pc, profile_val_pc, opens, highs, lows, closes, values, h = profile_high, l = profile_low, total = profile_total)
    profile.init(update_buckets = true)
    if init_calculated
        profile.calculate(use_open_close_data_for_ranges_shorter_than_bars)
    profile

Overview

SMC Profile Engine provides structured data types including Bucket, ProfileConfig, and Profile to manage volume calculations across price levels. The library facilitates tracking Point of Control, Value Area High, and Value Area Low levels, alongside bucket data updates and visual line or box drawings. It allows traders and developers to configure display styles and dynamically calculate volume distribution over specified candle series directly on the price chart.

Details

Published by

skipp-dev on GitHub

Code credit

preuss_steffen, as written in the script's header

Type

Library, Pine Script v6

Draws

Library (no plots)

Category

Volume, Support & resistance, SMC / ICT / price action

Markets

Any symbol

Timeframes

Any timeframe

Risk Notes

  • Signals can change on the live bar or after it closes (repainting)
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

SFA Price Gap Inefficiency v1

by genesisgrubio39
1 stars

The library detects price gap inefficiencies using structural flow analytics. It identifies new bullish and bearish gaps with their top and bottom levels, tracks mitigation status, and checks flow alignment between price and CVD trends. It applies regime detec

Library v6 SMC / ICT / price actionVolume
Updated 10 months ago · None stated GitHub repository of SFA Price Gap Inefficiency v1

FVGDetectorLibrary

by alvinng214
2 stars

This Pine Script library detects and plots Smart Money Concept Fair Value Gaps (FVGs) for both demand and supply zones directly on the price chart. It exports a detection function that accepts configurable filter thresholds ranging from standard three-bar imba

Library v5 SMC / ICT / price actionSupport & resistance
Updated 2 months ago · None stated GitHub repository of FVGDetectorLibrary

Moon Dev GEX Sweep & CVD

by moondevonyt
92 stars

The indicator detects liquidity sweeps across key price levels and calculates a cumulative volume delta proxy. It uses pivot high and low lookbacks to identify swing points, marking sweeps when price breaks beyond a level and quickly closes back inside. Featur

Indicator v5 Support & resistanceSMC / ICT / price action
Updated 8 weeks ago · Custom licence GitHub repository of Moon Dev GEX Sweep & CVD

Calculates higher-timeframe volume profiles to identify Value Area High (VAH), Value Area Low (VAL), and Point of Control (POC) levels directly on the price chart. It flags potential reversal setups and Change in State of Delivery (CISD) patterns relative to v

Indicator v6 VolumeSupport & resistance
Updated 2 months ago · None stated GitHub repository of CandelaCharts - Value Area Reversals

Copilot

by milocaetano
48 stars

Copilot is an order flow range-reversal indicator designed to overlay trading setups directly on the price chart. It evaluates four criteria: market context (sideways vs trending regime based on ATR height), structure (double top or double bottom retests), abs

Indicator v5 SMC / ICT / price actionSupport & resistance
Updated yesterday · MIT GitHub repository of Copilot

Supply and Demand Visible Range identifies supply, demand, and equilibrium zones across the visible chart range. It accumulates high, low, and volume data starting from the leftmost visible bar, optionally requesting lower timeframe intrabar data for finer res

Indicator v5 Support & resistanceVolume Repaints
Updated 2 months ago · AGPL-3.0 GitHub repository of Supply and Demand Visible Range [LuxAlgo]

More libraries

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers