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;
}
}