using System; using System.Collections.Generic; using System.Linq; using FinlyticCore.Dtos.TechnicalAnalysis; namespace FinlyticTechnicals.Patterns.ChartPatterns; /// /// Detects Double Bottom (W-reversal) and Double Top (M-reversal) formations. /// public class DoubleTopBottomDetector : IPatternDetector { public PatternType HandledType => PatternType.DoubleBottom; public PatternCategory Category => PatternCategory.Chart; public PatternResultDto? Evaluate(TechnicalContext context) { var candles = context.PrimaryCandles; if (candles.Count < 25) return null; // Search for two prominent swing lows within the last 20 candles int n = candles.Count; var recent = candles.TakeLast(25).ToList(); decimal min1 = decimal.MaxValue; int min1Idx = -1; decimal min2 = decimal.MaxValue; int min2Idx = -1; decimal peakBetween = 0m; for (int i = 2; i < recent.Count - 2; i++) { if (recent[i].Low <= recent[i - 1].Low && recent[i].Low <= recent[i - 2].Low && recent[i].Low <= recent[i + 1].Low && recent[i].Low <= recent[i + 2].Low) { if (min1Idx == -1) { min1 = recent[i].Low; min1Idx = i; } else if (min2Idx == -1 && i > min1Idx + 4) { min2 = recent[i].Low; min2Idx = i; break; } } } if (min1Idx != -1 && min2Idx != -1) { // Calculate peak between the two lows (neckline) for (int i = min1Idx; i <= min2Idx; i++) { if (recent[i].High > peakBetween) peakBetween = recent[i].High; } decimal priceDifference = Math.Abs(min1 - min2) / min1; var current = recent.Last(); // Double Bottom validation: lows within 1.5% of each other, current price breaking above neckline or holding second bottom if (priceDifference <= 0.015m && current.Close >= min2 && peakBetween > min1 * 1.01m) { decimal target = peakBetween + (peakBetween - Math.Min(min1, min2)); return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.DoubleBottom, Category: PatternCategory.Chart, Bias: PatternBias.Bullish, Name: "Double Bottom (W-Pattern)", Timeframe: context.Timeframe, DetectedAt: current.Timestamp, KeyPriceLevel: peakBetween, UpperBoundary: target, LowerBoundary: Math.Min(min1, min2), InvalidationLevel: Math.Min(min1, min2) * 0.995m, QualityScore: 82m, Description: $"Double bottom with bottoms at {min1:F2} & {min2:F2}, neckline at {peakBetween:F2}." ); } } return null; } } /// /// Detects Head & Shoulders and Inverse Head & Shoulders reversal formations. /// public class HeadAndShouldersDetector : IPatternDetector { public PatternType HandledType => PatternType.HeadAndShoulders; 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, Head, Right Shoulder // Head must be significantly higher than Left and Right shoulders decimal maxPrice = recent.Max(c => c.High); int headIdx = recent.FindIndex(c => c.High == maxPrice); if (headIdx >= 5 && headIdx <= recent.Count - 5) { decimal leftShoulder = recent.Take(headIdx).Max(c => c.High); decimal rightShoulder = recent.Skip(headIdx + 1).Max(c => c.High); if (maxPrice > leftShoulder * 1.01m && maxPrice > rightShoulder * 1.01m && Math.Abs(leftShoulder - rightShoulder) / leftShoulder <= 0.03m) { decimal neckline = recent.Skip(headIdx - 3).Take(6).Min(c => c.Low); var current = recent.Last(); if (current.Close <= rightShoulder) { return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.HeadAndShoulders, Category: PatternCategory.Chart, Bias: PatternBias.Bearish, Name: "Head & Shoulders", Timeframe: context.Timeframe, DetectedAt: current.Timestamp, KeyPriceLevel: neckline, UpperBoundary: maxPrice, LowerBoundary: neckline - (maxPrice - neckline), InvalidationLevel: maxPrice * 1.005m, QualityScore: 85m, Description: $"Bearish Head & Shoulders with Head at {maxPrice:F2}, Shoulders ~{leftShoulder:F2}, Neckline {neckline:F2}." ); } } } return null; } } /// /// Detects Ascending and Descending Triangle consolidations. /// public class TrianglePatternDetector : IPatternDetector { public PatternType HandledType => PatternType.AscendingTriangle; 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 highResistance = recent.Take(15).Max(c => c.High); // Check if highs are flat (horizontal resistance) while lows are rising (higher lows) decimal low1 = recent.Take(7).Min(c => c.Low); decimal low2 = recent.Skip(7).Take(7).Min(c => c.Low); decimal low3 = recent.Skip(14).Min(c => c.Low); if (low3 > low2 && low2 > low1 && Math.Abs(recent.Last().High - highResistance) / highResistance <= 0.01m) { var curr = recent.Last(); return new PatternResultDto( Id: Guid.NewGuid(), Type: PatternType.AscendingTriangle, Category: PatternCategory.Chart, Bias: PatternBias.Bullish, Name: "Ascending Triangle", Timeframe: context.Timeframe, DetectedAt: curr.Timestamp, KeyPriceLevel: highResistance, UpperBoundary: highResistance + (highResistance - low1), LowerBoundary: low3, InvalidationLevel: low3 * 0.995m, QualityScore: 80m, Description: $"Ascending triangle with horizontal resistance at {highResistance:F2} and rising lows ({low1:F2} -> {low2:F2} -> {low3:F2})." ); } return null; } }