using System; using System.Collections.Generic; using System.Linq; using FinlyticCore.Dtos.TechnicalAnalysis; namespace FinlyticTechnicals.Patterns.Candlesticks; /// /// Detects Bullish Hammer (long lower wick at support) and Bearish Shooting Star (long upper wick at resistance). /// public class HammerShootingStarDetector : IPatternDetector { public PatternType HandledType => PatternType.Hammer; public PatternCategory Category => PatternCategory.Candlestick; public PatternResultDto? Evaluate(TechnicalContext context) { var candles = context.PrimaryCandles; if (candles.Count < 5) return null; var current = candles.Last(); var prev = candles[^2]; decimal body = Math.Abs(current.Close - current.Open); decimal upperShadow = current.High - Math.Max(current.Open, current.Close); decimal lowerShadow = Math.Min(current.Open, current.Close) - current.Low; decimal totalRange = current.High - current.Low; if (totalRange <= 0) return null; // Hammer: Lower shadow >= 2x body, upper shadow <= 0.2x body, downtrend context if (lowerShadow >= 2.0m * Math.Max(body, 0.01m) && upperShadow <= 0.3m * totalRange && current.Close < prev.Close * 1.02m) { decimal score = Math.Min(95m, 60m + (lowerShadow / totalRange * 40m)); return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.Hammer, Category: PatternCategory.Candlestick, Bias: PatternBias.Bullish, Name: "Bullish Hammer", Timeframe: context.Timeframe, DetectedAt: current.Timestamp, KeyPriceLevel: current.Low, UpperBoundary: current.High, LowerBoundary: current.Low, InvalidationLevel: current.Low * 0.995m, QualityScore: score, Description: $"Bullish hammer with {lowerShadow / totalRange:P0} rejection lower wick at {current.Low:F2}." ); } // Shooting Star: Upper shadow >= 2x body, lower shadow <= 0.2x body, uptrend context if (upperShadow >= 2.0m * Math.Max(body, 0.01m) && lowerShadow <= 0.3m * totalRange && current.Close > prev.Close * 0.98m) { decimal score = Math.Min(95m, 60m + (upperShadow / totalRange * 40m)); return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.ShootingStar, Category: PatternCategory.Candlestick, Bias: PatternBias.Bearish, Name: "Bearish Shooting Star", Timeframe: context.Timeframe, DetectedAt: current.Timestamp, KeyPriceLevel: current.High, UpperBoundary: current.High, LowerBoundary: current.Low, InvalidationLevel: current.High * 1.005m, QualityScore: score, Description: $"Bearish shooting star with {upperShadow / totalRange:P0} rejection upper wick at {current.High:F2}." ); } return null; } } /// /// Detects Bullish and Bearish Engulfing candles. /// public class EngulfingPatternDetector : IPatternDetector { public PatternType HandledType => PatternType.BullishEngulfing; public PatternCategory Category => PatternCategory.Candlestick; public PatternResultDto? Evaluate(TechnicalContext context) { var candles = context.PrimaryCandles; if (candles.Count < 3) return null; var curr = candles.Last(); var prev = candles[^2]; bool prevBearish = prev.Close < prev.Open; bool currBullish = curr.Close > curr.Open; // Bullish Engulfing: previous red, current green completely engulfing previous body if (prevBearish && currBullish && curr.Open <= prev.Close && curr.Close >= prev.Open) { decimal score = Math.Min(90m, 70m + (curr.Volume > prev.Volume ? 15m : 0m)); return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.BullishEngulfing, Category: PatternCategory.Candlestick, Bias: PatternBias.Bullish, Name: "Bullish Engulfing", Timeframe: context.Timeframe, DetectedAt: curr.Timestamp, KeyPriceLevel: curr.Open, UpperBoundary: curr.High, LowerBoundary: curr.Low, InvalidationLevel: curr.Low * 0.995m, QualityScore: score, Description: $"Bullish engulfing candle covering previous range [{prev.Close:F2} - {prev.Open:F2}]." ); } bool prevBullish = prev.Close > prev.Open; bool currBearish = curr.Close < curr.Open; // Bearish Engulfing: previous green, current red completely engulfing previous body if (prevBullish && currBearish && curr.Open >= prev.Close && curr.Close <= prev.Open) { decimal score = Math.Min(90m, 70m + (curr.Volume > prev.Volume ? 15m : 0m)); return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.BearishEngulfing, Category: PatternCategory.Candlestick, Bias: PatternBias.Bearish, Name: "Bearish Engulfing", Timeframe: context.Timeframe, DetectedAt: curr.Timestamp, KeyPriceLevel: curr.Open, UpperBoundary: curr.High, LowerBoundary: curr.Low, InvalidationLevel: curr.High * 1.005m, QualityScore: score, Description: $"Bearish engulfing candle covering previous range [{prev.Open:F2} - {prev.Close:F2}]." ); } return null; } } /// /// Detects Morning Star and Evening Star 3-bar reversal patterns. /// public class MorningEveningStarDetector : IPatternDetector { public PatternType HandledType => PatternType.MorningStar; public PatternCategory Category => PatternCategory.Candlestick; public PatternResultDto? Evaluate(TechnicalContext context) { var candles = context.PrimaryCandles; if (candles.Count < 4) return null; var c1 = candles[^3]; var c2 = candles[^2]; // Star var c3 = candles.Last(); decimal body1 = Math.Abs(c1.Close - c1.Open); decimal body2 = Math.Abs(c2.Close - c2.Open); decimal body3 = Math.Abs(c3.Close - c3.Open); // Morning Star: Large Bearish + Small Star + Strong Bullish closing > 50% into candle 1 if (c1.Close < c1.Open && body2 < body1 * 0.4m && c3.Close > c3.Open && c3.Close >= (c1.Open + c1.Close) / 2m) { return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.MorningStar, Category: PatternCategory.Candlestick, Bias: PatternBias.Bullish, Name: "Morning Star", Timeframe: context.Timeframe, DetectedAt: c3.Timestamp, KeyPriceLevel: c2.Low, UpperBoundary: c3.High, LowerBoundary: c2.Low, InvalidationLevel: c2.Low * 0.995m, QualityScore: 85m, Description: $"Morning star reversal with low at {c2.Low:F2}." ); } // Evening Star: Large Bullish + Small Star + Strong Bearish closing < 50% into candle 1 if (c1.Close > c1.Open && body2 < body1 * 0.4m && c3.Close < c3.Open && c3.Close <= (c1.Open + c1.Close) / 2m) { return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.EveningStar, Category: PatternCategory.Candlestick, Bias: PatternBias.Bearish, Name: "Evening Star", Timeframe: context.Timeframe, DetectedAt: c3.Timestamp, KeyPriceLevel: c2.High, UpperBoundary: c2.High, LowerBoundary: c3.Low, InvalidationLevel: c2.High * 1.005m, QualityScore: 85m, Description: $"Evening star reversal with peak at {c2.High:F2}." ); } return null; } } /// /// Detects Doji indecision candles at key swing points. /// public class DojiPatternDetector : IPatternDetector { public PatternType HandledType => PatternType.Doji; public PatternCategory Category => PatternCategory.Candlestick; public PatternResultDto? Evaluate(TechnicalContext context) { var candles = context.PrimaryCandles; if (candles.Count < 3) return null; var curr = candles.Last(); decimal body = Math.Abs(curr.Close - curr.Open); decimal totalRange = curr.High - curr.Low; if (totalRange <= 0) return null; if (body <= totalRange * 0.10m) { return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.Doji, Category: PatternCategory.Candlestick, Bias: PatternBias.Neutral, Name: "Doji", Timeframe: context.Timeframe, DetectedAt: curr.Timestamp, KeyPriceLevel: curr.Close, UpperBoundary: curr.High, LowerBoundary: curr.Low, InvalidationLevel: curr.Low, QualityScore: 65m, Description: $"Doji indecision bar with tight body ({body:F2}) and range [{curr.Low:F2} - {curr.High:F2}]." ); } return null; } }