Hey all. Is there a way to calculate relative volume at a specific time of day that I'm not seeing? If there isn't an easy way, I'm wondering if anyone would be willing to help me code a Relative Volume indicator for intraday use? The intent would be for it to be a ratio of the cumulative intraday volume divided by the average of that figure up to the same bar over a given lookback period in days. This would aid in identifying if today marks an unusually high or low amount of trading volume compared to previous trading sessions.
Rename
I'm still very much a novice when it comes to C# (although I'm trying to learn), but I learned Pinescript from TradingView in the past and this is what I coded in that to get what I'm looking for (albeit not perfect since it doesn't handle situations like holidays well).
I think DailyValueAsOf(Volume) could be used instead of calculating cumulative volume and perhaps OffsetInd and BarOfDay would be helpful as well? I'm not sure.
I welcome all thoughts and help.
CODE:
// Input parameters lookbackPeriod = input.int(20, title="Lookback Period") // Calculate # of bars in day bars = timeframe.isintraday ? math.round(23400 / timeframe.in_seconds(timeframe.period)) : 1 lastBar = lookbackPeriod * bars // Calculate today's cumulative volume var float cumVolume = na if (dayofmonth != dayofmonth[1]) cumVolume := volume else cumVolume := nz(cumVolume[1]) + volume // Initialize array for storing cumulative volumes for lookback period var cumVolArray = array.new_float(lookbackPeriod, 0) // Update cumulative volume array for idx = bars to lastBar by bars array.pop(cumVolArray) array.unshift(cumVolArray, cumVolume[idx]) // Calculate relative volume avgCumVol = array.avg(cumVolArray) relativeVolume = cumVolume / avgCumVol // Plot relative volume plot(relativeVolume, title="Relative Volume", color=color.orange, linewidth=3) hline(1, "Neutral Line", color.white, hline.style_dotted, 1)
I think DailyValueAsOf(Volume) could be used instead of calculating cumulative volume and perhaps OffsetInd and BarOfDay would be helpful as well? I'm not sure.
I welcome all thoughts and help.
QUOTE:Good call, that would work. You just have to be careful about synchronizing the elements by time each day. Shouldn't be too hard.
DailyValueAsOf(Volume) could be used instead of calculating cumulative volume
Is this a duplicate topic?
Searched and found this thread where Glitch posted code for it:
Relative Intraday Volume indicator
Searched and found this thread where Glitch posted code for it:
Relative Intraday Volume indicator
I think it's very similar conceptually, but if I'm understanding that C# code correctly, this specific implementation is only accounting for the first bar of the trading day, not a continuous process throughout the day, which I'm unsure of how to do in C#.
Would it be instead of checking for IsFirstBarOfDay, check for if it is same BarOfDay as current and then use DailyValueAsOf at that point if so?
Would it be instead of checking for IsFirstBarOfDay, check for if it is same BarOfDay as current and then use DailyValueAsOf at that point if so?
QUOTE:
this specific implementation is only accounting for the first bar of the trading day, not a continuous process throughout the day, which I'm unsure of how to do in C#.
I hope I'm not stating the obvious here ...
CODE:That will give you the 50-day moving-average Volume if you're on the Daily scale. If you want to sample multiple scales (Daily and 15-minute scales), that's another topic. Just search the forum for that answer.
IndicatorBase avergeVolume = SMA.Series(bars.Volume, 50); //avg Vol over past 50 days
It might be faster (and simpler) to do all the calculations employing a single time scale (e.g. 15-minutes), then make final adjustments to their values to get the partial ratio you want.
@st - It's not that.
Imagine an intraday chart of 10 minutes bars. We want the average volume at each time of day (0940, 0950, etc) at that time of day over that last n days. It's pretty straight forward, but the trick is aligning the data at each time of day, accounting for missing bars and short days. Straightforward, but tricky. Probably would take me an hour to do >> Concierge Service for custom programming jobs and consulting.
Imagine an intraday chart of 10 minutes bars. We want the average volume at each time of day (0940, 0950, etc) at that time of day over that last n days. It's pretty straight forward, but the trick is aligning the data at each time of day, accounting for missing bars and short days. Straightforward, but tricky. Probably would take me an hour to do >> Concierge Service for custom programming jobs and consulting.
QUOTE:
We want the average volume at each time of day (0940, 0950, etc) at that time of day over that last n days.
So you want to "overlay" the 10-minute "slotted" 50-day average over the current day's 10-minute slots. Thanks for the clarification.
I'm asking myself if the first slot (and maybe the last slot) for each day is the only slot that really matters? I guess that would depend on how wide the slot it.
If I understood you correctly, it's not the average of the slots. It's the average cumulative volume for last n sessions at each interval. The result is directly comparable to the DailyValueAsOf(Volume) indicator.
My understanding of the indicator is the following:
Let SV = Sum of a day's (intraday) volume, to the current bar, for the given time scale.
Let N = number of days
Let PV = Sum of SV for N prior days - current day excluded
Let PAVG = PV / N
Let CSV = current day's SV
Let Result = CSV / PAVG
Is the above correct? If so, I might write the indicator.
Let SV = Sum of a day's (intraday) volume, to the current bar, for the given time scale.
Let N = number of days
Let PV = Sum of SV for N prior days - current day excluded
Let PAVG = PV / N
Let CSV = current day's SV
Let Result = CSV / PAVG
Is the above correct? If so, I might write the indicator.
QUOTE:
Straightforward, but tricky.
Well that at least makes me feel better about my struggles haha. It felt straight-forward but I was having a hard time with the C#, granted I am a noob still with that.
QUOTE:
It's the average cumulative volume for last n sessions at each interval. The result is directly comparable to the DailyValueAsOf(Volume) indicator.
This. If we're 5min into a trading session, I want the cumulative volume up to that point vs the average of the past n days up to that 5min mark. If we're an hour in, same thing but average cumulative volume for the first hour of trading. The ratio should be relatively smooth (depending on the time scale and how many bars into the session we are) over/under 1.0, which would indicate that today's cumulative volume is in line with previous sessions. Over would indicate today has relatively more volume, under would indicate today has lower volume. And if it were not cumulative, it would be more choppy and less informative of the trading session as a whole.
QUOTE:
Let SV = Sum of a day's (intraday) volume, to the current bar, for the given time scale.
Let N = number of days
Let PV = Sum of SV for N prior days - current day excluded
Let PAVG = PV / N
Let CSV = current day's SV
Let Result = CSV / PAVG
@paul1986 - I think that looks great! That'd be awesome!
QUOTE:
It's pretty straight forward, but the trick is aligning the data at each time of day, accounting for missing bars and short days.
Math.NET has methods that will fold a single dimensional array into a multidimensional array, but that won't account for missing bars and short days.
.NET has an Array.ConvertAll<Tinput,Toutput> method that might be used.
https://learn.microsoft.com/en-us/dotnet/api/system.array.convertall?view=net-8.0
But that method would require crafting a delegate that would either handle or skip the special cases. I'm curious to see how Paul approaches it.
@paul1986 - Re: Is the above correct? If so, I might write the indicator.
That's it. The only minor change to those requirements would be:
Let SV = Sum of a day's (intraday) volume, to the currentbar bar's time-of-day, for the given time scale.
To avoid repeating summations, use the DailyValueAsOf indicator.
That's it. The only minor change to those requirements would be:
Let SV = Sum of a day's (intraday) volume, to the current
To avoid repeating summations, use the DailyValueAsOf indicator.
Here ya go. This implemenation uses GetHistory() so you don't have to have a wide intraday data range to compute the values. The logic uses a non-sparse list of the DailyValueAsOf(volume) values which makes is very easy to calculate the final values. See the code and code comments for details. It seems to be very speedy.
Edit (2024/08/12): Code modified since publishing on 2024/08/11. Changes: now supports ONLY minute data, fixed issue with not recognizing non-trading day, and removed dependency on an external library.
Edit (2024/08/12): Code modified since publishing on 2024/08/11. Changes: now supports ONLY minute data, fixed issue with not recognizing non-trading day, and removed dependency on an external library.
CODE:
using System; using System.Collections.Generic; using WealthLab.Core; using WealthLab.Data; using WealthLab.Indicators; using WealthLab.PowerPack; namespace WLUtility.Indicators { /// <summary> /// Wealth-Lab 8 indicator that calculates the cumulative intra-day volume divided by the average volume of /// the same periods of the prior N days. It loads data via GetHistory so that results are readily available /// within the strategy's data range. Hence, you don't need to use a "wide" intra-day data range in the strategy /// just to get the data needed for the calculations. /// This indicator is useful for identifying unusual volume during certain times of day. /// This indicator requires the Wealth-Lab PowerPack extension. /// Works for N minute data scale frequency only. /// Should only be used for scales for which one day of time divided by the scale's time span /// is an integer - e.g. 1 minute, 2 minutes, 3 minutes, 5 minutes, 10 minutes, 15 minutes, 30 minutes, /// 60 minutes and any others. /// </summary> public class RelativeVolume : IndicatorBase { public RelativeVolume() { // needed for WL8 } public RelativeVolume(BarHistory barHistory, int period = 20) { Parameters[0].Value = barHistory; Parameters[1].Value = period; Populate(); } public override PlotStyle DefaultPlotStyle => PlotStyle.Line; public override WLColor DefaultColor => WLColor.Green; public override bool IsCalculationLengthy => false; private BarHistory Source => Parameters[0].AsBarHistory; private int Period => Parameters[1].AsInt; public override string Name => "RelativeVolume"; public override string Abbreviation => "RelativeVolume"; public override string HelpDescription => "Cumulative intra-day volume divided by the average volume of the same periods of the prior N days"; public override string PaneTag => "RelativeVolume"; protected override void GenerateParameters() { AddParameter("Source", ParameterType.BarHistory, null); AddParameter("Period", ParameterType.Int32, 20); } public override void Populate() { // make things speedier var source = Source; FirstValidIndex = 0; // if the source scale's frequency must be minute if (source.Scale.Frequency != Frequency.Minute) { // not supported so not much to do here for (var i = 0; i < source.Count; i++) { Add(double.NaN, source.DateTimes[i]); } return; } var period = Period; // Use GetHistory because the settings for a back test or live trading could be quite different from the // required days needed for the calculations. Don't short-change the results by using the source data only. // The user may want to go back a lot of days. var fromDate = source.SubtractTradingDays(0, period).Date; var cb = new DataRequestOptions { FilterPrePost = false, OfflineModeOverride = true }; // fetch the bar history var history = WLHost.Instance.GetHistory(source.Symbol, source.Scale, fromDate, source.DateTimes[source.Count - 1], int.MaxValue, cb); // We need to use intraday volume data. Wealth-Lab PowerPack is required because it contains // the DailyValueAsOf indicator. var intraDayVolumeAsOf = new DailyValueAsOf(history, PriceComponent.Volume); // Build a volume/datetime list that is not sparse of the date/times - it will // have a date/time with volume for every period of time in the scale from the start // of the history to the end of the history. This makes it very easy to traverse // by time units. var nonSparse = new List<(double volume, DateTime dateTime)>(intraDayVolumeAsOf.Count * 2); // how long is the scale in time? // this implementation only works for scales that are a multiple of minutes var scaleTimeSpan = TimeSpan.FromMinutes(source.Scale.Interval); // how many units of scale time are in a day? var scaleTimeSpanUnitsPerDay = (int) (new TimeSpan(24, 0, 0) / scaleTimeSpan); // build the nonSparse list for (var intraDayVolumeIdx = 0; intraDayVolumeIdx < intraDayVolumeAsOf.Count; intraDayVolumeIdx++) { var volume = intraDayVolumeAsOf[intraDayVolumeIdx]; var date = intraDayVolumeAsOf.DateTimes[intraDayVolumeIdx]; var saveDate = date; // store the intra-day volume for the current datetime nonSparse.Add((volume, date)); // move to the next datetime date += scaleTimeSpan; // if we are at the end of the list then we are done // otherwise, if the next datetime is not the next datetime in the intraDayVolumeAsOf list if (intraDayVolumeIdx >= intraDayVolumeAsOf.Count - 1 || date >= intraDayVolumeAsOf.DateTimes[intraDayVolumeIdx + 1]) { continue; } // need to stop at the next datetime var nextDate = intraDayVolumeAsOf.DateTimes[intraDayVolumeIdx + 1]; // keep adding the same volume until the next date in intraDayVolumeAsOf while (date < nextDate) { if (date.Date != saveDate.Date) { // changed days - reset the volume volume = 0.0; } // don't add intra-day volume for non-trading days if (source.Market.IsTradingDay(date)) { nonSparse.Add((volume, date)); } date += scaleTimeSpan; } } var nonSparseIdx = 0; // for each source bar... for (var sourceIdx = 0; sourceIdx < source.Count; sourceIdx++) { var srcDate = source.DateTimes[sourceIdx]; while (nonSparse[nonSparseIdx].dateTime < srcDate && nonSparseIdx < nonSparse.Count) { nonSparseIdx++; } if (nonSparseIdx >= nonSparse.Count) { break; } // sum the volume for the period by walking back in the nonSparse time series // nonSparseIdx is the index of the volume (within nonSparse) that corresponds to the current source bar var sum = 0.0; var j = nonSparseIdx - scaleTimeSpanUnitsPerDay; var count = 0; while (count < period && j >= 0) { sum += nonSparse[j].volume; j -= scaleTimeSpanUnitsPerDay; count++; } var avg = count == 0 ? 0 : sum / count; Add(avg == 0 ? 0 : nonSparse[nonSparseIdx].volume / avg, srcDate); // for testing //Add(sum, srcDate); } } public static RelativeVolume Series(BarHistory barHistory, int period = 20) { var key = CacheKey("RelativeVolume", period); if (barHistory.Cache.TryGetValue(key, out var rv)) { return (RelativeVolume) rv; } var result = new RelativeVolume(barHistory, period); barHistory.Cache[key] = result; return result; } } }
Thanks for putting that together, Paul! Excuse me ignorance though, but I'm getting a couple errors when I copy that over that I don't know how to fix.
Line 7:
Line 102:
Line 7:
CODE:Error: The type or namespace name 'Extensions' does not exist in the namespace 'WLUtility' (are you missing an assembly reference?)
using WLUtility.Extensions();
Line 102:
CODE:Error: 'HistoryScale' does not contain a definition 'ToTimeSpan' and no accessible extension method 'ToTimeSpan' accepting a first argument of type 'HistoryScale' could be found (are you missing a using directive or an assembly reference?)
var scaleTimeSpan = source.Scale.ToTimeSpan();
Oops. I didn't realize the code was using some of my extensions. I'll fix it a little later today.
@Believer - I updated the code. See Post #13 for updated code and the edit note.
Awesome stuff! Looks great so far. Thanks, Paul!
Your Response
Post
Edit Post
Login is required