using System; using System.Collections.Generic; using NT8.Core.Common.Models; using NT8.Core.Logging; namespace NT8.Core.Intelligence { /// /// Detects trend regime and trend quality from recent bar data and AVWAP context. /// public class TrendRegimeDetector { private readonly ILogger _logger; private readonly object _lock = new object(); private readonly Dictionary _currentRegimes; private readonly Dictionary _currentStrength; /// /// Creates a new trend regime detector. /// /// Logger instance. public TrendRegimeDetector(ILogger logger) { if (logger == null) throw new ArgumentNullException("logger"); _logger = logger; _currentRegimes = new Dictionary(); _currentStrength = new Dictionary(); } /// /// Detects trend regime for a symbol based on bars and AVWAP value. /// /// Instrument symbol. /// Recent bars in chronological order. /// Current AVWAP value. /// Detected trend regime. public TrendRegime DetectTrend(string symbol, List bars, double avwap) { if (string.IsNullOrEmpty(symbol)) throw new ArgumentNullException("symbol"); if (bars == null) throw new ArgumentNullException("bars"); if (bars.Count < 5) throw new ArgumentException("At least 5 bars are required", "bars"); try { var strength = CalculateTrendStrength(bars, avwap); var regime = ClassifyTrendRegime(strength); lock (_lock) { _currentRegimes[symbol] = regime; _currentStrength[symbol] = strength; } _logger.LogDebug("Trend regime detected for {0}: Regime={1}, Strength={2:F4}", symbol, regime, strength); return regime; } catch (Exception ex) { _logger.LogError("DetectTrend failed for {0}: {1}", symbol, ex.Message); throw; } } /// /// Calculates trend strength in range [-1.0, +1.0]. /// Positive values indicate uptrend and negative values indicate downtrend. /// /// Recent bars in chronological order. /// Current AVWAP value. /// Trend strength score. public double CalculateTrendStrength(List bars, double avwap) { if (bars == null) throw new ArgumentNullException("bars"); if (bars.Count < 5) throw new ArgumentException("At least 5 bars are required", "bars"); try { var last = bars[bars.Count - 1]; var lookback = bars.Count >= 10 ? 10 : bars.Count; var past = bars[bars.Count - lookback]; var slopePerBar = (last.Close - past.Close) / lookback; var range = EstimateAverageRange(bars, lookback); var normalizedSlope = range > 0.0 ? slopePerBar / range : 0.0; var aboveAvwapCount = 0; var belowAvwapCount = 0; var startIndex = bars.Count - lookback; for (var i = startIndex; i < bars.Count; i++) { if (bars[i].Close > avwap) aboveAvwapCount++; else if (bars[i].Close < avwap) belowAvwapCount++; } var avwapBias = 0.0; if (lookback > 0) avwapBias = (double)(aboveAvwapCount - belowAvwapCount) / lookback; var structureBias = CalculateStructureBias(bars, lookback); var strength = (normalizedSlope * 0.45) + (avwapBias * 0.35) + (structureBias * 0.20); strength = Clamp(strength, -1.0, 1.0); return strength; } catch (Exception ex) { _logger.LogError("CalculateTrendStrength failed: {0}", ex.Message); throw; } } /// /// Determines whether bars are ranging based on normalized trend strength threshold. /// /// Recent bars. /// Absolute strength threshold that defines range state. /// True when market appears to be ranging. public bool IsRanging(List bars, double threshold) { if (bars == null) throw new ArgumentNullException("bars"); if (bars.Count < 5) throw new ArgumentException("At least 5 bars are required", "bars"); if (threshold <= 0.0) throw new ArgumentException("threshold must be greater than zero", "threshold"); try { var avwap = bars[bars.Count - 1].Close; var strength = CalculateTrendStrength(bars, avwap); return Math.Abs(strength) < threshold; } catch (Exception ex) { _logger.LogError("IsRanging failed: {0}", ex.Message); throw; } } /// /// Assesses trend quality using structure consistency and volatility noise. /// /// Recent bars. /// Trend quality classification. public TrendQuality AssessTrendQuality(List bars) { if (bars == null) throw new ArgumentNullException("bars"); if (bars.Count < 5) throw new ArgumentException("At least 5 bars are required", "bars"); try { var lookback = bars.Count >= 10 ? 10 : bars.Count; var structure = Math.Abs(CalculateStructureBias(bars, lookback)); var noise = CalculateNoiseRatio(bars, lookback); var qualityScore = (structure * 0.65) + ((1.0 - noise) * 0.35); qualityScore = Clamp(qualityScore, 0.0, 1.0); if (qualityScore >= 0.80) return TrendQuality.Excellent; if (qualityScore >= 0.60) return TrendQuality.Good; if (qualityScore >= 0.40) return TrendQuality.Fair; return TrendQuality.Poor; } catch (Exception ex) { _logger.LogError("AssessTrendQuality failed: {0}", ex.Message); throw; } } /// /// Gets last detected trend regime for a symbol. /// /// Instrument symbol. /// Current trend regime or Range when unknown. public TrendRegime GetCurrentTrendRegime(string symbol) { if (string.IsNullOrEmpty(symbol)) throw new ArgumentNullException("symbol"); lock (_lock) { TrendRegime regime; if (_currentRegimes.TryGetValue(symbol, out regime)) return regime; return TrendRegime.Range; } } /// /// Gets last detected trend strength for a symbol. /// /// Instrument symbol. /// Trend strength or zero when unknown. public double GetCurrentTrendStrength(string symbol) { if (string.IsNullOrEmpty(symbol)) throw new ArgumentNullException("symbol"); lock (_lock) { double strength; if (_currentStrength.TryGetValue(symbol, out strength)) return strength; return 0.0; } } private static TrendRegime ClassifyTrendRegime(double strength) { if (strength >= 0.80) return TrendRegime.StrongUp; if (strength >= 0.30) return TrendRegime.WeakUp; if (strength <= -0.80) return TrendRegime.StrongDown; if (strength <= -0.30) return TrendRegime.WeakDown; return TrendRegime.Range; } private static double EstimateAverageRange(List bars, int lookback) { if (lookback <= 0) return 0.0; var sum = 0.0; var start = bars.Count - lookback; for (var i = start; i < bars.Count; i++) { sum += (bars[i].High - bars[i].Low); } return sum / lookback; } private static double CalculateStructureBias(List bars, int lookback) { var start = bars.Count - lookback; var upStructure = 0; var downStructure = 0; for (var i = start + 1; i < bars.Count; i++) { var prev = bars[i - 1]; var cur = bars[i]; var higherHigh = cur.High > prev.High; var higherLow = cur.Low > prev.Low; var lowerHigh = cur.High < prev.High; var lowerLow = cur.Low < prev.Low; if (higherHigh && higherLow) upStructure++; else if (lowerHigh && lowerLow) downStructure++; } var transitions = lookback - 1; if (transitions <= 0) return 0.0; return (double)(upStructure - downStructure) / transitions; } private static double CalculateNoiseRatio(List bars, int lookback) { var start = bars.Count - lookback; var directionalMove = Math.Abs(bars[bars.Count - 1].Close - bars[start].Close); var path = 0.0; for (var i = start + 1; i < bars.Count; i++) { path += Math.Abs(bars[i].Close - bars[i - 1].Close); } if (path <= 0.0) return 1.0; var efficiency = directionalMove / path; efficiency = Clamp(efficiency, 0.0, 1.0); return 1.0 - efficiency; } private static double Clamp(double value, double min, double max) { if (value < min) return min; if (value > max) return max; return value; } } }