diff --git a/src/s-z/VolumeProfile/VolumeProfile.Models.cs b/src/s-z/VolumeProfile/VolumeProfile.Models.cs new file mode 100644 index 000000000..99b058782 --- /dev/null +++ b/src/s-z/VolumeProfile/VolumeProfile.Models.cs @@ -0,0 +1,69 @@ +namespace Skender.Stock.Indicators; + +[Serializable] +public class VpvrResult : ResultBase +{ + private VpvrResult? previousResult; + + public VpvrResult(IQuote quote, VpvrResult? previousResult) + { + this.previousResult = previousResult; + + if (quote is null) + { + throw new ArgumentNullException(nameof(quote)); + } + + Date = quote.Date; + High = quote.High; + Low = quote.Low; + Volume = quote.Volume; + } + + public decimal High { get; private set; } + public decimal Low { get; private set; } + public decimal Volume { get; private set; } + public IEnumerable VolumeProfile { get; internal set; } = Array.Empty(); + public IEnumerable CumulativeVolumeProfile + { + get + { + List vpvrValues = cumulativeVolumeProfile.Select((kvp) => new VpvrValue(kvp.Key, kvp.Value)).ToList(); + vpvrValues.Sort((first, second) => first.Price.CompareTo(second.Price)); + return vpvrValues; + } + } + private Dictionary cumulativeVolumeProfile + { + get + { + Dictionary vpvrTotals = previousResult?.cumulativeVolumeProfile ?? new Dictionary(); + + foreach (VpvrValue item in VolumeProfile) + { + if (vpvrTotals.ContainsKey(item.Price)) + { + vpvrTotals[item.Price] += item.Volume; + } + else + { + vpvrTotals.Add(item.Price, item.Volume); + } + } + + return vpvrTotals; + } + } +} + +public class VpvrValue +{ + public VpvrValue(decimal price, decimal volume) + { + Price = price; + Volume = volume; + } + + public decimal Price { get; set; } + public decimal Volume { get; set; } +} diff --git a/src/s-z/VolumeProfile/VolumeProfile.Utilities.cs b/src/s-z/VolumeProfile/VolumeProfile.Utilities.cs new file mode 100644 index 000000000..b93563aba --- /dev/null +++ b/src/s-z/VolumeProfile/VolumeProfile.Utilities.cs @@ -0,0 +1,17 @@ +namespace Skender.Stock.Indicators; + +public static partial class Indicator +{ + // remove recommended periods + // + // + // public static IEnumerable RemoveWarmupPeriods( + // this IEnumerable results) + // { + // int removePeriods = results + // .ToList() + // .FindIndex(x => x.Volume != null); + // + // return results.Remove(removePeriods); + // } +} diff --git a/src/s-z/VolumeProfile/VolumeProfile.cs b/src/s-z/VolumeProfile/VolumeProfile.cs new file mode 100644 index 000000000..192f20c69 --- /dev/null +++ b/src/s-z/VolumeProfile/VolumeProfile.cs @@ -0,0 +1,62 @@ +namespace Skender.Stock.Indicators; + +public static partial class Indicator +{ + // Volume Profile + /// + /// + public static IEnumerable GetVpvr(this IEnumerable quotes, decimal precision = 0.001M) + { + ValidateVpvr(precision); + + List results = new List(); + VpvrResult? vpvrResult = null; + foreach (IQuote quote in quotes) + { + vpvrResult = new VpvrResult(quote, vpvrResult); + results.Add(vpvrResult); + vpvrResult.getVolumeProfile(precision); + } + + return results; + } + + private static IEnumerable getVolumeProfile(this VpvrResult vpvrResult, decimal precision = 0.001M) + { + ValidateVpvr(precision); + + // if ((vpvrResult.VolumeProfile == null) || (vpvrResult.currentPrecision != precision)) + { + decimal high = Math.Ceiling(vpvrResult.High / precision) * precision; + decimal low = Math.Floor(vpvrResult.Low / precision) * precision; + decimal delta = high - low; + + List results = new List(); + if (delta > 0) + { + decimal intervalCount = delta / precision; // should be even number?? + decimal volumeSliceSize = vpvrResult.Volume / intervalCount; + + for (decimal price = low; price <= high; price += precision) + { + results.Add(new VpvrValue(price, volumeSliceSize)); + } + + results.Sort((first, second) => first.Price.CompareTo(second.Price)); + } + + vpvrResult.VolumeProfile = results; + } + + return vpvrResult.VolumeProfile; + } + + // parameter validation + private static void ValidateVpvr(decimal precision) + { + if (precision <= 0) + { + throw new ArgumentOutOfRangeException(nameof(precision), precision, "Precision must be greater than 0."); + } + } +} diff --git a/src/s-z/VolumeProfile/info.xml b/src/s-z/VolumeProfile/info.xml new file mode 100644 index 000000000..ca41a07e5 --- /dev/null +++ b/src/s-z/VolumeProfile/info.xml @@ -0,0 +1,23 @@ + + + + + Volume Profile breaks down historical volume data by price. The length of the bars represents the volume traded at each price, and indicates prices where significant support/resistance was encountered. + Institutions do not create new orders based on the real-time price, at maximum precision, at a specific moment. Expecting to make X% profit, they post orders for Y volume, at Z price, then wait. + The volume above/below that price is immediately consumed, as a taker, creating a void in the available volume around that price. + As other investors adjust their orders to undercut, or "me too," that same price, the volume available concentrates around that price even further, expanding the void. + Those orders, and the voids between them, appear as hills and valleys in the volume profile, representing normal volatility. + Over time, the hills around those prices turn into tall mountains, with steep cliffs. + A gap between any two such cliffs indicates that wicking is likely to occur between them. + To profit, place limit orders at the prices with the longest volume profile bars. (Not financial advice!) :) + + See + documentation + for more information. + + + Historical price quotes. + Width of each volume slice. + Time series of volume-per-price values. + Invalid parameter value provided. + \ No newline at end of file