186 lines
7.1 KiB
C#
186 lines
7.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using FinlyticCore.Dtos.TechnicalAnalysis;
|
|
|
|
namespace FinlyticTechnicals.Patterns.ChartPatterns;
|
|
|
|
/// <summary>
|
|
/// Detects Double Bottom (W-reversal) and Double Top (M-reversal) formations.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects Head & Shoulders and Inverse Head & Shoulders reversal formations.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detects Ascending and Descending Triangle consolidations.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|