using System; using System.Collections.Generic; using NT8.Core.Common.Models; namespace NT8.Core.Indicators { /// /// Represents value area range around volume point of control. /// public class ValueArea { /// /// Volume point of control (highest volume price level). /// public double VPOC { get; set; } /// /// Value area high boundary. /// public double ValueAreaHigh { get; set; } /// /// Value area low boundary. /// public double ValueAreaLow { get; set; } /// /// Total profile volume. /// public double TotalVolume { get; set; } /// /// Value area volume. /// public double ValueAreaVolume { get; set; } /// /// Creates a value area model. /// /// VPOC level. /// Value area high. /// Value area low. /// Total volume. /// Value area volume. public ValueArea(double vpoc, double valueAreaHigh, double valueAreaLow, double totalVolume, double valueAreaVolume) { VPOC = vpoc; ValueAreaHigh = valueAreaHigh; ValueAreaLow = valueAreaLow; TotalVolume = totalVolume; ValueAreaVolume = valueAreaVolume; } } /// /// Analyzes volume profile by price level and derives VPOC/value areas. /// public class VolumeProfileAnalyzer { private readonly object _lock = new object(); /// /// Gets VPOC from provided bars. /// /// Bars in profile window. /// VPOC price level. public double GetVPOC(List bars) { if (bars == null) throw new ArgumentNullException("bars"); lock (_lock) { var profile = BuildProfile(bars, 0.25); if (profile.Count == 0) return 0.0; var maxVolume = double.MinValue; var vpoc = 0.0; foreach (var kv in profile) { if (kv.Value > maxVolume) { maxVolume = kv.Value; vpoc = kv.Key; } } return vpoc; } } /// /// Returns high volume nodes where volume exceeds 1.5x average level volume. /// /// Bars in profile window. /// High volume node price levels. public List GetHighVolumeNodes(List bars) { if (bars == null) throw new ArgumentNullException("bars"); lock (_lock) { var profile = BuildProfile(bars, 0.25); var result = new List(); if (profile.Count == 0) return result; var avg = CalculateAverageVolume(profile); var threshold = avg * 1.5; foreach (var kv in profile) { if (kv.Value >= threshold) result.Add(kv.Key); } result.Sort(); return result; } } /// /// Returns low volume nodes where volume is below 0.5x average level volume. /// /// Bars in profile window. /// Low volume node price levels. public List GetLowVolumeNodes(List bars) { if (bars == null) throw new ArgumentNullException("bars"); lock (_lock) { var profile = BuildProfile(bars, 0.25); var result = new List(); if (profile.Count == 0) return result; var avg = CalculateAverageVolume(profile); var threshold = avg * 0.5; foreach (var kv in profile) { if (kv.Value <= threshold) result.Add(kv.Key); } result.Sort(); return result; } } /// /// Calculates 70% value area around VPOC. /// /// Bars in profile window. /// Calculated value area. public ValueArea CalculateValueArea(List bars) { if (bars == null) throw new ArgumentNullException("bars"); lock (_lock) { var profile = BuildProfile(bars, 0.25); if (profile.Count == 0) return new ValueArea(0.0, 0.0, 0.0, 0.0, 0.0); var levels = new List(profile.Keys); levels.Sort(); var vpoc = GetVPOC(bars); var totalVolume = 0.0; for (var i = 0; i < levels.Count; i++) totalVolume += profile[levels[i]]; var target = totalVolume * 0.70; var included = new HashSet(); included.Add(vpoc); var includedVolume = profile.ContainsKey(vpoc) ? profile[vpoc] : 0.0; var vpocIndex = levels.IndexOf(vpoc); var left = vpocIndex - 1; var right = vpocIndex + 1; while (includedVolume < target && (left >= 0 || right < levels.Count)) { var leftVolume = left >= 0 ? profile[levels[left]] : -1.0; var rightVolume = right < levels.Count ? profile[levels[right]] : -1.0; if (rightVolume > leftVolume) { included.Add(levels[right]); includedVolume += profile[levels[right]]; right++; } else if (left >= 0) { included.Add(levels[left]); includedVolume += profile[levels[left]]; left--; } else { included.Add(levels[right]); includedVolume += profile[levels[right]]; right++; } } var vah = vpoc; var val = vpoc; foreach (var level in included) { if (level > vah) vah = level; if (level < val) val = level; } return new ValueArea(vpoc, vah, val, totalVolume, includedVolume); } } private static Dictionary BuildProfile(List bars, double tickSize) { var profile = new Dictionary(); for (var i = 0; i < bars.Count; i++) { var bar = bars[i]; if (bar == null) continue; var low = RoundToTick(bar.Low, tickSize); var high = RoundToTick(bar.High, tickSize); if (high < low) { var temp = high; high = low; low = temp; } var levelsCount = ((high - low) / tickSize) + 1.0; if (levelsCount <= 0.0) continue; var volumePerLevel = bar.Volume / levelsCount; var level = low; while (level <= high + 0.0000001) { if (!profile.ContainsKey(level)) profile.Add(level, 0.0); profile[level] = profile[level] + volumePerLevel; level = RoundToTick(level + tickSize, tickSize); } } return profile; } private static double CalculateAverageVolume(Dictionary profile) { if (profile == null || profile.Count == 0) return 0.0; var sum = 0.0; var count = 0; foreach (var kv in profile) { sum += kv.Value; count++; } return count > 0 ? sum / count : 0.0; } private static double RoundToTick(double value, double tickSize) { if (tickSize <= 0.0) return value; var ticks = Math.Round(value / tickSize); return ticks * tickSize; } } }