feat(technicals,engine): add V2 multi-timeframe scoring, SMC patterns, and COS V2 engine
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
|
||||
namespace FinlyticTechnicals.Patterns.ChartPatterns;
|
||||
|
||||
/// <summary>
|
||||
/// Detects Double Top (M-reversal) formation where price tests a major resistance peak twice and breaks lower.
|
||||
/// </summary>
|
||||
public class DoubleTopDetector : IPatternDetector
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public PatternType HandledType => PatternType.DoubleTop;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PatternCategory Category => PatternCategory.Chart;
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects Inverse Head & Shoulders (bullish reversal) formation.
|
||||
/// </summary>
|
||||
public class InverseHeadAndShouldersDetector : IPatternDetector
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public PatternType HandledType => PatternType.InverseHeadAndShoulders;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PatternCategory Category => PatternCategory.Chart;
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects Descending Triangle (bearish continuation / breakdown) consolidation.
|
||||
/// </summary>
|
||||
public class DescendingTriangleDetector : IPatternDetector
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public PatternType HandledType => PatternType.DescendingTriangle;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PatternCategory Category => PatternCategory.Chart;
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user