From bf38e8301c32796ebf4ba3c958f0206d5b17b7e5 Mon Sep 17 00:00:00 2001 From: JGronholz Date: Mon, 22 Aug 2022 16:14:44 -0500 Subject: [PATCH 1/2] Volume Profile --- src/s-z/VolumeProfile/VolumeProfile.Models.cs | 69 +++++++++++++++++++ .../VolumeProfile/VolumeProfile.Utilities.cs | 17 +++++ src/s-z/VolumeProfile/VolumeProfile.cs | 62 +++++++++++++++++ src/s-z/VolumeProfile/info.xml | 23 +++++++ 4 files changed, 171 insertions(+) create mode 100644 src/s-z/VolumeProfile/VolumeProfile.Models.cs create mode 100644 src/s-z/VolumeProfile/VolumeProfile.Utilities.cs create mode 100644 src/s-z/VolumeProfile/VolumeProfile.cs create mode 100644 src/s-z/VolumeProfile/info.xml diff --git a/src/s-z/VolumeProfile/VolumeProfile.Models.cs b/src/s-z/VolumeProfile/VolumeProfile.Models.cs new file mode 100644 index 000000000..24c6116cf --- /dev/null +++ b/src/s-z/VolumeProfile/VolumeProfile.Models.cs @@ -0,0 +1,69 @@ +namespace Skender.Stock.Indicators; + +[Serializable] +public class VpvrResult : ResultBase +{ + 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; + } + + private VpvrResult previousResult; + + public decimal High { get; private set; } + public decimal Low { get; private set; } + public decimal Volume { get; private set; } + public IEnumerable VolumeProfile { get; internal set; } + 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..1317eeeda --- /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..2c60f638a --- /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 From 0630de0cc03fd9836d41b769971cac46c6615d14 Mon Sep 17 00:00:00 2001 From: JGronholz Date: Thu, 27 Apr 2023 20:10:11 -0500 Subject: [PATCH 2/2] Fixed errors during azure build --- src/s-z/VolumeProfile/VolumeProfile.Models.cs | 8 ++++---- src/s-z/VolumeProfile/VolumeProfile.Utilities.cs | 12 ++++++------ src/s-z/VolumeProfile/VolumeProfile.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/s-z/VolumeProfile/VolumeProfile.Models.cs b/src/s-z/VolumeProfile/VolumeProfile.Models.cs index 24c6116cf..99b058782 100644 --- a/src/s-z/VolumeProfile/VolumeProfile.Models.cs +++ b/src/s-z/VolumeProfile/VolumeProfile.Models.cs @@ -3,7 +3,9 @@ namespace Skender.Stock.Indicators; [Serializable] public class VpvrResult : ResultBase { - public VpvrResult(IQuote quote, VpvrResult previousResult) + private VpvrResult? previousResult; + + public VpvrResult(IQuote quote, VpvrResult? previousResult) { this.previousResult = previousResult; @@ -18,12 +20,10 @@ public VpvrResult(IQuote quote, VpvrResult previousResult) Volume = quote.Volume; } - private VpvrResult previousResult; - public decimal High { get; private set; } public decimal Low { get; private set; } public decimal Volume { get; private set; } - public IEnumerable VolumeProfile { get; internal set; } + public IEnumerable VolumeProfile { get; internal set; } = Array.Empty(); public IEnumerable CumulativeVolumeProfile { get diff --git a/src/s-z/VolumeProfile/VolumeProfile.Utilities.cs b/src/s-z/VolumeProfile/VolumeProfile.Utilities.cs index 1317eeeda..b93563aba 100644 --- a/src/s-z/VolumeProfile/VolumeProfile.Utilities.cs +++ b/src/s-z/VolumeProfile/VolumeProfile.Utilities.cs @@ -3,15 +3,15 @@ namespace Skender.Stock.Indicators; public static partial class Indicator { // remove recommended periods - /// - /// - //public static IEnumerable RemoveWarmupPeriods( + // + // + // 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 index 2c60f638a..192f20c69 100644 --- a/src/s-z/VolumeProfile/VolumeProfile.cs +++ b/src/s-z/VolumeProfile/VolumeProfile.cs @@ -10,7 +10,7 @@ public static IEnumerable GetVpvr(this IEnumerable quotes, d ValidateVpvr(precision); List results = new List(); - VpvrResult vpvrResult = null; + VpvrResult? vpvrResult = null; foreach (IQuote quote in quotes) { vpvrResult = new VpvrResult(quote, vpvrResult);