251 lines
9.6 KiB
C#
251 lines
9.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using FinlyticCore.Dtos.TechnicalAnalysis;
|
|
|
|
namespace FinlyticTechnicals.Patterns.Candlesticks;
|
|
|
|
/// <summary>
|
|
/// Detects Bullish Hammer (long lower wick at support) and Bearish Shooting Star (long upper wick at resistance).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects Bullish and Bearish Engulfing candles.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects Morning Star and Evening Star 3-bar reversal patterns.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects Doji indecision candles at key swing points.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|