using System; using System.Collections.Generic; using System.Linq; using FinlyticCore.Dtos.TechnicalAnalysis; namespace FinlyticTechnicals.Patterns.ChartPatterns; /// /// Detects Double Top (M-reversal) formation where price tests a major resistance peak twice and breaks lower. /// public class DoubleTopDetector : IPatternDetector { /// public PatternType HandledType => PatternType.DoubleTop; /// public PatternCategory Category => PatternCategory.Chart; /// public PatternResultDto? Evaluate(TechnicalContext context) { var candles = context.PrimaryCandles; if (candles.Count < 25) return null; var recent = candles.TakeLast(25).ToList(); decimal max1 = decimal.MinValue; int max1Idx = -1; decimal max2 = decimal.MinValue; int max2Idx = -1; decimal troughBetween = decimal.MaxValue; // Search for two prominent swing highs for (int i = 2; i < recent.Count - 2; i++) { if (recent[i].High >= recent[i - 1].High && recent[i].High >= recent[i - 2].High && recent[i].High >= recent[i + 1].High && recent[i].High >= recent[i + 2].High) { if (max1Idx == -1) { max1 = recent[i].High; max1Idx = i; } else if (max2Idx == -1 && i > max1Idx + 4) { max2 = recent[i].High; max2Idx = i; break; } } } if (max1Idx != -1 && max2Idx != -1) { // Calculate trough between the two highs (neckline) for (int i = max1Idx; i <= max2Idx; i++) { if (recent[i].Low < troughBetween) troughBetween = recent[i].Low; } decimal priceDifference = Math.Abs(max1 - max2) / max1; var current = recent.Last(); // Double Top validation: highs within 1.5% of each other, neckline clearly below highs if (priceDifference <= 0.015m && current.Close <= max2 && troughBetween < max1 * 0.99m) { decimal target = troughBetween - (Math.Max(max1, max2) - troughBetween); return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.DoubleTop, Category: PatternCategory.Chart, Bias: PatternBias.Bearish, Name: "Double Top (M-Pattern)", Timeframe: context.Timeframe, DetectedAt: current.Timestamp, KeyPriceLevel: troughBetween, UpperBoundary: Math.Max(max1, max2), LowerBoundary: target, InvalidationLevel: Math.Max(max1, max2) * 1.005m, QualityScore: 82m, Description: $"Double top with peaks at {max1:F2} & {max2:F2}, neckline support at {troughBetween:F2}." ); } } return null; } } /// /// Detects Inverse Head & Shoulders (bullish reversal) formation. /// public class InverseHeadAndShouldersDetector : IPatternDetector { /// public PatternType HandledType => PatternType.InverseHeadAndShoulders; /// public PatternCategory Category => PatternCategory.Chart; /// public PatternResultDto? Evaluate(TechnicalContext context) { var candles = context.PrimaryCandles; if (candles.Count < 30) return null; var recent = candles.TakeLast(30).ToList(); // Look for Left Shoulder Low, Head Low (lowest), Right Shoulder Low decimal minPrice = recent.Min(c => c.Low); int headIdx = recent.FindIndex(c => c.Low == minPrice); if (headIdx >= 5 && headIdx <= recent.Count - 5) { decimal leftShoulderLow = recent.Take(headIdx).Min(c => c.Low); decimal rightShoulderLow = recent.Skip(headIdx + 1).Min(c => c.Low); // Head must be strictly lower than both shoulders if (minPrice < leftShoulderLow * 0.99m && minPrice < rightShoulderLow * 0.99m && Math.Abs(leftShoulderLow - rightShoulderLow) / leftShoulderLow <= 0.03m) { decimal neckline = recent.Skip(headIdx - 3).Take(6).Max(c => c.High); var current = recent.Last(); if (current.Close >= rightShoulderLow) { decimal target = neckline + (neckline - minPrice); return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.InverseHeadAndShoulders, Category: PatternCategory.Chart, Bias: PatternBias.Bullish, Name: "Inverse Head & Shoulders", Timeframe: context.Timeframe, DetectedAt: current.Timestamp, KeyPriceLevel: neckline, UpperBoundary: target, LowerBoundary: minPrice, InvalidationLevel: minPrice * 0.995m, QualityScore: 85m, Description: $"Bullish Inverse Head & Shoulders with Head low at {minPrice:F2}, Shoulders ~{leftShoulderLow:F2}, Neckline at {neckline:F2}." ); } } } return null; } } /// /// Detects Descending Triangle (bearish continuation / breakdown) consolidation. /// public class DescendingTriangleDetector : IPatternDetector { /// public PatternType HandledType => PatternType.DescendingTriangle; /// public PatternCategory Category => PatternCategory.Chart; /// public PatternResultDto? Evaluate(TechnicalContext context) { var candles = context.PrimaryCandles; if (candles.Count < 20) return null; var recent = candles.TakeLast(20).ToList(); decimal lowSupport = recent.Take(15).Min(c => c.Low); // Check if lows are flat (horizontal support) while highs are falling (lower highs) decimal high1 = recent.Take(7).Max(c => c.High); decimal high2 = recent.Skip(7).Take(7).Max(c => c.High); decimal high3 = recent.Skip(14).Max(c => c.High); if (high3 < high2 && high2 < high1 && Math.Abs(recent.Last().Low - lowSupport) / Math.Max(lowSupport, 0.01m) <= 0.01m) { var curr = recent.Last(); decimal target = lowSupport - (high1 - lowSupport); return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.DescendingTriangle, Category: PatternCategory.Chart, Bias: PatternBias.Bearish, Name: "Descending Triangle", Timeframe: context.Timeframe, DetectedAt: curr.Timestamp, KeyPriceLevel: lowSupport, UpperBoundary: high3, LowerBoundary: target, InvalidationLevel: high3 * 1.005m, QualityScore: 80m, Description: $"Descending triangle with horizontal support at {lowSupport:F2} and descending highs ({high1:F2} -> {high2:F2} -> {high3:F2})." ); } return null; } }