// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/ // © TradingView //@version=5 indicator("CVD - Cumulative Volume Delta Candles", "CVD Candles", format = format.volume) // CVD - Cumulative Volume Delta Candles // v6, 2022.11.19 // This code was written using the recommendations from the Pine Script™ User Manual's Style Guide: // https://www.tradingview.com/pine-script-docs/en/v5/writing/Style_guide.html import PineCoders/Time/2 as PCtime import PineCoders/lower_tf/3 as PCltf import TradingView/ta/4 as TVta // ———————————————————— Constants and Inputs { // ————— Constants int MS_IN_MIN = 60 * 1000 int MS_IN_HOUR = MS_IN_MIN * 60 int MS_IN_DAY = MS_IN_HOUR * 24 // Default colors color GRAY = #808080ff color LIME = #00FF00ff color MAROON = #800000ff color ORANGE = #FF8000ff color PINK = #FF0080ff color TEAL = #008080ff color BG_DIV = color.new(ORANGE, 90) color BG_RESETS = color.new(GRAY, 90) // Reset conditions string RST1 = "None" string RST2 = "On a stepped higher timeframe" string RST3 = "On a fixed higher timeframe..." string RST4 = "At a fixed time..." string RST5 = "At the beginning of the regular session" string RST6 = "At the first visible chart bar" string RST7 = "On trend changes..." // Trends string TR01 = "Supertrend" string TR02 = "Aroon" string TR03 = "Parabolic SAR" // Volume Delta calculation mode string VD01 = "Volume Delta" string VD02 = "Volume Delta Percent" // Realtime calculation modes string RT1 = "Intrabars (same as on historical bars)" string RT2 = "Chart updates" // Intrabar precisions string LTF1 = "Covering most chart bars (least precise)" string LTF2 = "Covering some chart bars (less precise)" string LTF3 = "Covering less chart bars (more precise)" string LTF4 = "Very precise (2min intrabars)" string LTF5 = "Most precise (1min intrabars)" string LTF6 = "~12 intrabars per chart bar" string LTF7 = "~24 intrabars per chart bar" string LTF8 = "~50 intrabars per chart bar" string LTF9 = "~100 intrabars per chart bar" string LTF10 = "~250 intrabars per chart bar" // Tooltips string TT_RST = "This is where you specify how you want the cumulative volume delta to reset. If you select one of the last three choices, you must also specify the relevant additional information below." string TT_RST_HTF = "This value only matters when '" + RST3 +"' is selected." string TT_RST_TIME = "Hour: 0-23\nMinute: 0-59\nThese values only matter when '" + RST4 +"' is selected. A reset will occur when the time is greater or equal to the bar's open time, and less than its close time." string TT_RST_TREND = "These values only matter when '" + RST7 +"' is selected.\n For Supertrend, the first value is the length of ATR, the second is the factor. For Aroon, the first value is the lookback length." string TT_TOTVOL = "Total volume can only be displayed when '" + VD01 +"' is selected as the Volume Delta Calculation mode.\n\n The 'Bodies' value is the transparency of the total volume candle bodies. Zero is opaque, 100 is transparent." string TT_LINE = "This plots a line at the `close` values of the CVD candles. You can use it instead of the CVD candles." string TT_LTF = "Your selection here controls how many intrabars will be analyzed for each chart bar. The more intrabars you analyze, the less chart bars will be covered by the indicator's calculations because a maximum of 100K intrabars can be analyzed.\n\n With the first four choices the quantity of intrabars is determined from the type of chart bar coverage you want. The last four choices allow you to select approximately how many intrabars you want analyzed per chart bar." string TT_MA = "This plots the running average of CVD from its last reset. The 'Length' period only applies when CVD does not reset, i.e., the reset is 'None'." // ————— Inputs string resetInput = input.string(RST2, "CVD Resets", inline = "00", options = [RST1, RST2, RST5, RST6, RST3, RST4, RST7], tooltip = TT_RST) string fixedTfInput = input.timeframe("D", "  Fixed higher timeframe:", tooltip = TT_RST_HTF) int hourInput = input.int(9, "  Fixed time: Hour", inline = "01", minval = 0, maxval = 23) int minuteInput = input.int(30, "Minute", inline = "01", minval = 0, maxval = 59, tooltip = TT_RST_TIME) string trendInput = input.string(TR01, "  Trend: ", inline = "02", options = [TR02, TR03, TR01]) int trendPeriodInput = input.int(14, " Length", inline = "02", minval = 2) float trendValue2Input = input.float(3.0, "", inline = "02", minval = 0.25, step = 0.25, tooltip = TT_RST_TREND) string ltfModeInput = input.string(LTF3, "Intrabar precision", inline = "03", options = [LTF1, LTF2, LTF3, LTF4, LTF5, LTF6, LTF7, LTF8, LTF9, LTF10], tooltip = TT_LTF) string vdCalcModeInput = input.string(VD01, "Volume Delta Calculation", inline = "04", options = [VD01, VD02]) string GRP1 = "Visuals" bool showCandlesInput = input.bool(true, "CVD candles", inline = "11", group = GRP1) color upColorInput = input.color(LIME, " 🡑", inline = "11", group = GRP1) color dnColorInput = input.color(PINK, "🡓", inline = "11", group = GRP1) bool colorDivBodiesInput = input.bool(true, "Color CVD bodies on divergences ", inline = "12", group = GRP1) color upDivColorInput = input.color(TEAL, "🡑", inline = "12", group = GRP1) color dnDivColorInput = input.color(MAROON, "🡓", inline = "12", group = GRP1) bool showTotVolInput = input.bool(false, "Total volume candle borders", inline = "13", group = GRP1) color upTotVolColorInput = input.color(TEAL, "🡑", inline = "13", group = GRP1) color dnTotVolColorInput = input.color(MAROON, "🡓", inline = "13", group = GRP1) int totVolBodyTranspInput = input.int(80, "bodies", inline = "13", group = GRP1, minval = 0, maxval = 100, tooltip = TT_TOTVOL) bool showLineInput = input.bool(false, "CVD line", inline = "14", group = GRP1) color lineUpColorInput = input.color(LIME, " 🡑", inline = "14", group = GRP1) color lineDnColorInput = input.color(PINK, "🡓", inline = "14", group = GRP1, tooltip = TT_LINE) bool showMaInput = input.bool(false, "CVD MA", inline = "15", group = GRP1) color maUpColorInput = input.color(TEAL, "  🡑", inline = "15", group = GRP1) color maDnColorInput = input.color(MAROON, "🡓", inline = "15", group = GRP1) int maPeriodInput = input.int(20, " Length", inline = "15", group = GRP1, minval = 2, tooltip = TT_MA) bool bgDivInput = input.bool(false, "Color background on divergences ", inline = "16", group = GRP1) color bgDivColorInput = input.color(BG_DIV, "", inline = "16", group = GRP1) bool bgResetInput = input.bool(true, "Color background on resets ", inline = "17", group = GRP1) color bgResetColorInput = input.color(BG_RESETS, "", inline = "17", group = GRP1) bool showZeroLineInput = input.bool(true, "Zero line", inline = "18", group = GRP1) bool showInfoBoxInput = input.bool(true, "Show information box ", group = GRP1) string infoBoxSizeInput = input.string("small", "Size ", inline = "19", group = GRP1, options = ["tiny", "small", "normal", "large", "huge", "auto"]) string infoBoxYPosInput = input.string("bottom", "↕", inline = "19", group = GRP1, options = ["top", "middle", "bottom"]) string infoBoxXPosInput = input.string("left", "↔", inline = "19", group = GRP1, options = ["left", "center", "right"]) color infoBoxColorInput = input.color(color.gray, "", inline = "19", group = GRP1) color infoBoxTxtColorInput = input.color(color.white, "T", inline = "19", group = GRP1) // } // ———————————————————— Functions { // @function Determines if the volume for an intrabar is up or down. // @returns ([float, float]) A tuple of two values, one of which contains the bar's volume. `upVol` is the volume of up bars. `dnVol` is the volume of down bars. // Note that when this function is called with `request.security_lower_tf()` a tuple of float[] arrays will be returned by `request.security_lower_tf()`. upDnIntrabarVolumes() => float upVol = 0.0 float dnVol = 0.0 switch // Bar polarity can be determined. close > open => upVol += volume close < open => dnVol -= volume // If not, use price movement since last bar. close > nz(close[1]) => upVol += volume close < nz(close[1]) => dnVol -= volume // If not, use previously known polarity. nz(upVol[1]) > 0 => upVol += volume nz(dnVol[1]) < 0 => dnVol -= volume [upVol, dnVol] // @function Selects a HTF from the chart's TF. // @returns (simple string) A timeframe string. htfStep() => int tfInMs = timeframe.in_seconds() * 1000 string result = switch tfInMs < MS_IN_MIN => "60" tfInMs < MS_IN_HOUR * 3 => "D" tfInMs <= MS_IN_HOUR * 12 => "W" tfInMs < MS_IN_DAY * 7 => "M" => "12M" // @function Determines when a bar opens at a given time. // @param hours (series int) "Hour" part of the time we are looking for. // @param minutes (series int) "Minute" part of the time we are looking for. // @returns (series bool) `true` when the bar opens at `hours`:`minutes`, false otherwise. timeReset(int hours, int minutes) => int openTime = timestamp(year, month, dayofmonth, hours, minutes, 0) bool timeInBar = time <= openTime and time_close > openTime bool result = timeframe.isintraday and not timeInBar[1] and timeInBar // } // ———————————————————— Calculations { // Lower timeframe (LTF) used to mine intrabars. var string ltfString = PCltf.ltf(ltfModeInput, LTF1, LTF2, LTF3, LTF4, LTF5, LTF6, LTF7, LTF8, LTF9, LTF10) // Get two arrays, one each for up and dn volumes. [upVolumes, dnVolumes] = request.security_lower_tf(syminfo.tickerid, ltfString, upDnIntrabarVolumes()) // ————— Select between historical and realtime DV calcs. In rt, use user selection. float totalUpVolume = nz(array.sum(upVolumes)) float totalDnVolume = nz(array.sum(dnVolumes)) float totalVolume = totalUpVolume - totalDnVolume float maxUpVolume = nz(array.max(upVolumes)) float maxDnVolume = nz(array.min(dnVolumes)) float delta = totalUpVolume + totalDnVolume float deltaPct = delta / totalVolume // Track cumulative volume. var float cvd = 0.0 [reset, trendIsUp, resetDescription] = switch resetInput RST1 => [false, na, "No resets"] RST2 => [timeframe.change(htfStep()), na, "Resets every " + htfStep()] RST3 => [timeframe.change(fixedTfInput), na, "Resets every " + fixedTfInput] RST4 => [timeReset(hourInput, minuteInput), na, str.format("Resets at {0,number,00}:{1,number,00}", hourInput, minuteInput)] RST5 => [session.isfirstbar_regular, na, "Resets at the beginning of the session"] RST6 => [time == chart.left_visible_bar_time, na, "Resets at the beginning of visible bars"] RST7 => switch trendInput TR01 => [_, direction] = ta.supertrend(trendValue2Input, trendPeriodInput) [ta.change(direction, 1) != 0, direction == -1, "Resets on Supertrend changes"] TR02 => [up, dn] = TVta.aroon(trendPeriodInput) [ta.cross(up, dn), ta.crossover(up, dn), "Resets on Aroon changes"] TR03 => float psar = ta.sar(0.02, 0.02, 0.2) [ta.cross(psar, close), ta.crossunder(psar, close), "Resets on PSAR changes"] => [na, na, na] if reset cvd := 0 // Build OHLC values for CVD candles. bool useVdPct = vdCalcModeInput == VD02 float barDelta = useVdPct ? deltaPct : delta float cvdO = cvd float cvdC = cvdO + barDelta float cvdH = useVdPct ? math.max(cvdO, cvdC) : cvdO + maxUpVolume float cvdL = useVdPct ? math.min(cvdO, cvdC) : cvdO + maxDnVolume cvd += barDelta // MA of CVD var float ma = cvd var cvdValues = array.new() if resetInput == RST1 ma := ta.sma(cvdC, maPeriodInput) else if reset array.clear(cvdValues) array.push(cvdValues, cvd) else array.push(cvdValues, cvd) ma := array.avg(cvdValues) // Total volume level relative to CVD. float totalVolumeLevel = cvdO + (totalVolume * math.sign(barDelta)) // ———— Intrabar stats [intrabars, chartBarsCovered, avgIntrabars] = PCltf.ltfStats(upVolumes) int chartBars = bar_index + 1 // Detect errors. if resetInput == RST3 and timeframe.in_seconds(fixedTfInput) <= timeframe.in_seconds() runtime.error("The higher timeframe for resets must be greater than the chart's timeframe.") else if resetInput == RST4 and not timeframe.isintraday runtime.error("Resets at a fixed time work on intraday charts only.") else if ta.cum(totalVolume) == 0 and barstate.islast runtime.error("No volume is provided by the data vendor.") else if ta.cum(intrabars) == 0 and barstate.islast runtime.error("No intrabar information exists at the '" + ltfString + "' timeframe.") else if timeframe.in_seconds() < 60 * 2 runtime.error("The indicator cannot work on chart timeframes < 2min.") // Detect divergences between volume delta and the bar's polarity. bool divergence = delta != 0 and math.sign(delta) != math.sign(close - open) // } // ———————————————————— Visuals { color candleColor = delta > 0 ? colorDivBodiesInput and divergence ? upDivColorInput : upColorInput : colorDivBodiesInput and divergence ? dnDivColorInput : dnColorInput color totVolCandleColor = delta > 0 ? upTotVolColorInput : dnTotVolColorInput // Display key values in indicator values and Data Window. displayLocation = display.data_window plot(delta, "Volume delta for the bar", candleColor, display = displayLocation) plot(totalUpVolume, "Up volume for the bar", upColorInput, display = displayLocation) plot(totalDnVolume, "Dn volume for the bar", dnColorInput, display = displayLocation) plot(totalVolume, "Total volume", display = displayLocation) plot(na, "═════════════════", display = displayLocation) plot(cvdO, "CVD before this bar", display = displayLocation) plot(cvdC, "CVD after this bar", display = displayLocation) plot(maxUpVolume, "Max intrabar up volume", upColorInput, display = displayLocation) plot(maxDnVolume, "Max intrabar dn volume", dnColorInput, display = displayLocation) plot(intrabars, "Intrabars in this bar", display = displayLocation) plot(avgIntrabars, "Average intrabars", display = displayLocation) plot(chartBarsCovered, "Chart bars covered", display = displayLocation) plot(bar_index + 1, "Chart bars", display = displayLocation) plot(na, "═════════════════", display = displayLocation) // Total volume boxes. [totalVolO, totalVolH, totalVolL, totalVolC] = if showTotVolInput and not useVdPct [cvdO, math.max(cvdO, totalVolumeLevel), math.min(cvdO, totalVolumeLevel), totalVolumeLevel] else [na, na, na, na] plotcandle(totalVolO, totalVolH, totalVolL, totalVolC, "CVD", color = color.new(totVolCandleColor, totVolBodyTranspInput), wickcolor = totVolCandleColor, bordercolor = totVolCandleColor) // CVD candles. plotcandle(showCandlesInput ? cvdO : na, cvdH, cvdL, cvdC, "CVD", color = candleColor, wickcolor = candleColor, bordercolor = candleColor) // CVD line. plot(showLineInput ? cvdC : na, "CVD line", cvdC > 0 ? lineUpColorInput : lineDnColorInput) // CVD MA. plot(showMaInput ? ma : na, "CVD MA", reset ? na : ma > 0 ? maUpColorInput : maDnColorInput) // Zero line. hline(showZeroLineInput ? 0 : na, "Zero", GRAY, hline.style_dotted) // Up/Dn arrow used when resets occur on trend changes. plotchar(reset and not na(trendIsUp) ? trendIsUp : na, "Up trend", "▲", location.top, upColorInput) plotchar(reset and not na(trendIsUp) ? not trendIsUp : na, "Dn trend", "▼", location.top, dnColorInput) // Background on resets and divergences. bgcolor(bgResetInput and reset ? bgResetColorInput : bgDivInput and divergence ? bgDivColorInput : na) // Display information box only once on the last historical bar, instead of on all realtime updates, as when `barstate.islast` is used. if showInfoBoxInput and barstate.islastconfirmedhistory var table infoBox = table.new(infoBoxYPosInput + "_" + infoBoxXPosInput, 1, 1) color infoBoxBgColor = infoBoxColorInput string txt = str.format("{0}\nUses intrabars at {1}\nAvg intrabars per chart bar: {2,number,#.##}\nChart bars covered: {3} / {4} ({5,number,percent})", resetDescription, PCtime.formattedNoOfPeriods(timeframe.in_seconds(ltfString) * 1000), avgIntrabars, chartBarsCovered, bar_index + 1, chartBarsCovered / (bar_index + 1)) if avgIntrabars < 5 txt += "\nThis quantity of intrabars is dangerously small.\nResults will not be as reliable with so few." infoBoxBgColor := color.red table.cell(infoBox, 0, 0, txt, text_color = infoBoxTxtColorInput, text_size = infoBoxSizeInput, bgcolor = infoBoxBgColor) // }