Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Volume Profile indicator #880

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
69 changes: 69 additions & 0 deletions src/s-z/VolumeProfile/VolumeProfile.Models.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace Skender.Stock.Indicators;

[Serializable]
public class VpvrResult : ResultBase
{
private VpvrResult? previousResult;

Check warning on line 6 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / analyze (csharp)

previousResult participates in a potential reference cycle (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5362)

Check warning on line 6 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / analyze (csharp)

previousResult participates in a potential reference cycle (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5362)

Check failure on line 6 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / unit tests

previousResult participates in a potential reference cycle (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5362)

Check failure on line 6 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / unit tests

previousResult participates in a potential reference cycle (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca5362)

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<VpvrValue> VolumeProfile { get; internal set; } = Array.Empty<VpvrValue>();
public IEnumerable<VpvrValue> CumulativeVolumeProfile
{
get
{
List<VpvrValue> vpvrValues = cumulativeVolumeProfile.Select((kvp) => new VpvrValue(kvp.Key, kvp.Value)).ToList();
vpvrValues.Sort((first, second) => first.Price.CompareTo(second.Price));
return vpvrValues;
}
}

Check warning on line 35 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / analyze (csharp)

Check warning on line 35 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / analyze (csharp)

Check failure on line 35 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / unit tests

Check failure on line 35 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / unit tests

private Dictionary<decimal, decimal> cumulativeVolumeProfile

Check warning on line 36 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / analyze (csharp)

Element 'cumulativeVolumeProfile' should begin with an uppercase letter (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md)

Check warning on line 36 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / analyze (csharp)

Element 'cumulativeVolumeProfile' should begin with an uppercase letter (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md)

Check failure on line 36 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / unit tests

Element 'cumulativeVolumeProfile' should begin with an uppercase letter (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md)

Check failure on line 36 in src/s-z/VolumeProfile/VolumeProfile.Models.cs

View workflow job for this annotation

GitHub Actions / unit tests

Element 'cumulativeVolumeProfile' should begin with an uppercase letter (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md)
{
get
{
Dictionary<decimal, decimal> vpvrTotals = previousResult?.cumulativeVolumeProfile ?? new Dictionary<decimal, decimal>();

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; }
}
17 changes: 17 additions & 0 deletions src/s-z/VolumeProfile/VolumeProfile.Utilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Skender.Stock.Indicators;

public static partial class Indicator
{
// remove recommended periods
// <include file='../../_common/Results/info.xml' path='info/type[@name="Prune"]/*' />
//
// public static IEnumerable<VpvrResult> RemoveWarmupPeriods(
// this IEnumerable<VpvrResult> results)
// {
// int removePeriods = results
// .ToList()
// .FindIndex(x => x.Volume != null);
//
// return results.Remove(removePeriods);
// }
}
62 changes: 62 additions & 0 deletions src/s-z/VolumeProfile/VolumeProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace Skender.Stock.Indicators;

public static partial class Indicator
{
// Volume Profile
/// <include file='./info.xml' path='indicator/*' />
///
public static IEnumerable<VpvrResult> GetVpvr(this IEnumerable<IQuote> quotes, decimal precision = 0.001M)
{
ValidateVpvr(precision);

List<VpvrResult> results = new List<VpvrResult>();
VpvrResult? vpvrResult = null;
foreach (IQuote quote in quotes)

Check warning on line 14 in src/s-z/VolumeProfile/VolumeProfile.cs

View workflow job for this annotation

GitHub Actions / analyze (csharp)

In externally visible method 'IEnumerable<VpvrResult> Indicator.GetVpvr(IEnumerable<IQuote> quotes, decimal precision = 0.001)', validate parameter 'quotes' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)

Check warning on line 14 in src/s-z/VolumeProfile/VolumeProfile.cs

View workflow job for this annotation

GitHub Actions / analyze (csharp)

In externally visible method 'IEnumerable<VpvrResult> Indicator.GetVpvr(IEnumerable<IQuote> quotes, decimal precision = 0.001)', validate parameter 'quotes' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)

Check failure on line 14 in src/s-z/VolumeProfile/VolumeProfile.cs

View workflow job for this annotation

GitHub Actions / unit tests

In externally visible method 'IEnumerable<VpvrResult> Indicator.GetVpvr(IEnumerable<IQuote> quotes, decimal precision = 0.001)', validate parameter 'quotes' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)

Check failure on line 14 in src/s-z/VolumeProfile/VolumeProfile.cs

View workflow job for this annotation

GitHub Actions / unit tests

In externally visible method 'IEnumerable<VpvrResult> Indicator.GetVpvr(IEnumerable<IQuote> quotes, decimal precision = 0.001)', validate parameter 'quotes' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)
{
vpvrResult = new VpvrResult(quote, vpvrResult);
results.Add(vpvrResult);
vpvrResult.getVolumeProfile(precision);
}

return results;
}

private static IEnumerable<VpvrValue> getVolumeProfile(this VpvrResult vpvrResult, decimal precision = 0.001M)

Check warning on line 24 in src/s-z/VolumeProfile/VolumeProfile.cs

View workflow job for this annotation

GitHub Actions / analyze (csharp)

Element 'getVolumeProfile' should begin with an uppercase letter (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md)

Check warning on line 24 in src/s-z/VolumeProfile/VolumeProfile.cs

View workflow job for this annotation

GitHub Actions / analyze (csharp)

Element 'getVolumeProfile' should begin with an uppercase letter (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md)

Check failure on line 24 in src/s-z/VolumeProfile/VolumeProfile.cs

View workflow job for this annotation

GitHub Actions / unit tests

Element 'getVolumeProfile' should begin with an uppercase letter (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md)

Check failure on line 24 in src/s-z/VolumeProfile/VolumeProfile.cs

View workflow job for this annotation

GitHub Actions / unit tests

Element 'getVolumeProfile' should begin with an uppercase letter (https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md)
{
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<VpvrValue> results = new List<VpvrValue>();
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.");
}
}
}
23 changes: 23 additions & 0 deletions src/s-z/VolumeProfile/info.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>

<indicator>
<summary>
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!) :)
<para>
See
<see href="https://daveskender.github.io/Stock.Indicators/indicators/VolumeProfile/#content">documentation</see>
for more information.
</para>
</summary>
<param name="quotes">Historical price quotes.</param>
<param name="precision">Width of each volume slice.</param>
<returns>Time series of volume-per-price values.</returns>
<exception cref="ArgumentOutOfRangeException">Invalid parameter value provided.</exception>
</indicator>