feat(technicals): add technical analysis microservice with indicator engines, pattern detectors, and strategies
This commit is contained in:
@@ -0,0 +1,250 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
|
||||
namespace FinlyticTechnicals.Patterns;
|
||||
|
||||
/// <summary>
|
||||
/// Isolated detector contract for a specific candlestick, chart, or SMC pattern.
|
||||
/// </summary>
|
||||
public interface IPatternDetector
|
||||
{
|
||||
PatternType HandledType { get; }
|
||||
PatternCategory Category { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the technical context and returns a detected pattern or null if conditions are not met.
|
||||
/// </summary>
|
||||
PatternResultDto? Evaluate(TechnicalContext context);
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FinlyticCore.Dtos.TechnicalAnalysis;
|
||||
|
||||
namespace FinlyticTechnicals.Patterns.SmartMoney;
|
||||
|
||||
/// <summary>
|
||||
/// Detects Bullish and Bearish Fair Value Gaps (FVG) across 3-candle sequences.
|
||||
/// </summary>
|
||||
public class FairValueGapDetector : IPatternDetector
|
||||
{
|
||||
public PatternType HandledType => PatternType.FairValueGapBullish;
|
||||
public PatternCategory Category => PatternCategory.SmartMoney;
|
||||
|
||||
public PatternResultDto? Evaluate(TechnicalContext context)
|
||||
{
|
||||
var candles = context.PrimaryCandles;
|
||||
if (candles.Count < 3) return null;
|
||||
|
||||
var c1 = candles[^3];
|
||||
var c2 = candles[^2]; // Impulse candle
|
||||
var c3 = candles.Last();
|
||||
|
||||
// Bullish FVG: Candle 1 High < Candle 3 Low (Gap between c1.High and c3.Low)
|
||||
if (c3.Low > c1.High && c2.Close > c2.Open)
|
||||
{
|
||||
decimal gapSize = c3.Low - c1.High;
|
||||
decimal midGap = (c3.Low + c1.High) / 2m;
|
||||
|
||||
if (gapSize >= context.CurrentAtr * 0.25m)
|
||||
{
|
||||
return new PatternResultDto(
|
||||
Id: Guid.NewGuid(),
|
||||
Type: PatternType.FairValueGapBullish,
|
||||
Category: PatternCategory.SmartMoney,
|
||||
Bias: PatternBias.Bullish,
|
||||
Name: "Bullish Fair Value Gap (FVG)",
|
||||
Timeframe: context.Timeframe,
|
||||
DetectedAt: c3.Timestamp,
|
||||
KeyPriceLevel: midGap,
|
||||
UpperBoundary: c3.Low,
|
||||
LowerBoundary: c1.High,
|
||||
InvalidationLevel: c1.High * 0.995m,
|
||||
QualityScore: 88m,
|
||||
Description: $"Bullish FVG imbalance zone [{c1.High:F2} - {c3.Low:F2}] with midpoint at {midGap:F2}."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Bearish FVG: Candle 1 Low > Candle 3 High (Gap between c3.High and c1.Low)
|
||||
if (c3.High < c1.Low && c2.Close < c2.Open)
|
||||
{
|
||||
decimal gapSize = c1.Low - c3.High;
|
||||
decimal midGap = (c1.Low + c3.High) / 2m;
|
||||
|
||||
if (gapSize >= context.CurrentAtr * 0.25m)
|
||||
{
|
||||
return new PatternResultDto(
|
||||
Id: Guid.NewGuid(),
|
||||
Type: PatternType.FairValueGapBearish,
|
||||
Category: PatternCategory.SmartMoney,
|
||||
Bias: PatternBias.Bearish,
|
||||
Name: "Bearish Fair Value Gap (FVG)",
|
||||
Timeframe: context.Timeframe,
|
||||
DetectedAt: c3.Timestamp,
|
||||
KeyPriceLevel: midGap,
|
||||
UpperBoundary: c1.Low,
|
||||
LowerBoundary: c3.High,
|
||||
InvalidationLevel: c1.Low * 1.005m,
|
||||
QualityScore: 88m,
|
||||
Description: $"Bearish FVG imbalance zone [{c3.High:F2} - {c1.Low:F2}] with midpoint at {midGap:F2}."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects Liquidity Sweeps where price takes out multi-period swing highs/lows and immediately rejects back inside the range.
|
||||
/// </summary>
|
||||
public class LiquiditySweepDetector : IPatternDetector
|
||||
{
|
||||
public PatternType HandledType => PatternType.LiquiditySweepLow;
|
||||
public PatternCategory Category => PatternCategory.SmartMoney;
|
||||
|
||||
public PatternResultDto? Evaluate(TechnicalContext context)
|
||||
{
|
||||
var candles = context.PrimaryCandles;
|
||||
if (candles.Count < 20) return null;
|
||||
|
||||
var lookback = candles.Take(candles.Count - 1).TakeLast(20).ToList();
|
||||
var current = candles.Last();
|
||||
|
||||
decimal swingLow = lookback.Min(c => c.Low);
|
||||
decimal swingHigh = lookback.Max(c => c.High);
|
||||
|
||||
// Bullish Liquidity Sweep (Sweep Low): Pierced previous swing low but closed back above it
|
||||
if (current.Low < swingLow && current.Close > swingLow)
|
||||
{
|
||||
return new PatternResultDto(
|
||||
Id: Guid.NewGuid(),
|
||||
Type: PatternType.LiquiditySweepLow,
|
||||
Category: PatternCategory.SmartMoney,
|
||||
Bias: PatternBias.Bullish,
|
||||
Name: "Bullish Liquidity Sweep (Stop Hunt)",
|
||||
Timeframe: context.Timeframe,
|
||||
DetectedAt: current.Timestamp,
|
||||
KeyPriceLevel: swingLow,
|
||||
UpperBoundary: swingHigh,
|
||||
LowerBoundary: current.Low,
|
||||
InvalidationLevel: current.Low * 0.995m,
|
||||
QualityScore: 92m,
|
||||
Description: $"Bullish liquidity sweep below swing low {swingLow:F2} with wick rejection to {current.Low:F2}."
|
||||
);
|
||||
}
|
||||
|
||||
// Bearish Liquidity Sweep (Sweep High): Pierced previous swing high but closed back below it
|
||||
if (current.High > swingHigh && current.Close < swingHigh)
|
||||
{
|
||||
return new PatternResultDto(
|
||||
Id: Guid.NewGuid(),
|
||||
Type: PatternType.LiquiditySweepHigh,
|
||||
Category: PatternCategory.SmartMoney,
|
||||
Bias: PatternBias.Bearish,
|
||||
Name: "Bearish Liquidity Sweep (Buy-Side Sweep)",
|
||||
Timeframe: context.Timeframe,
|
||||
DetectedAt: current.Timestamp,
|
||||
KeyPriceLevel: swingHigh,
|
||||
UpperBoundary: current.High,
|
||||
LowerBoundary: swingLow,
|
||||
InvalidationLevel: current.High * 1.005m,
|
||||
QualityScore: 92m,
|
||||
Description: $"Bearish liquidity sweep above swing high {swingHigh:F2} with wick rejection to {current.High:F2}."
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects Change of Character (CHoCH) structural trend reversals and Break of Structure (BOS) continuations.
|
||||
/// </summary>
|
||||
public class ChochBosDetector : IPatternDetector
|
||||
{
|
||||
public PatternType HandledType => PatternType.ChangeOfCharacter;
|
||||
public PatternCategory Category => PatternCategory.SmartMoney;
|
||||
|
||||
public PatternResultDto? Evaluate(TechnicalContext context)
|
||||
{
|
||||
var candles = context.PrimaryCandles;
|
||||
if (candles.Count < 20) return null;
|
||||
|
||||
var current = candles.Last();
|
||||
var prevCandles = candles.Take(candles.Count - 1).TakeLast(15).ToList();
|
||||
|
||||
decimal priorSwingHigh = prevCandles.Max(c => c.High);
|
||||
decimal priorSwingLow = prevCandles.Min(c => c.Low);
|
||||
|
||||
// Bullish CHoCH: Clean candle body close above previous major swing high
|
||||
if (current.Close > priorSwingHigh && current.Open < priorSwingHigh)
|
||||
{
|
||||
return new PatternResultDto(
|
||||
Id: Guid.NewGuid(),
|
||||
Type: PatternType.ChangeOfCharacter,
|
||||
Category: PatternCategory.SmartMoney,
|
||||
Bias: PatternBias.Bullish,
|
||||
Name: "Bullish Change of Character (CHoCH)",
|
||||
Timeframe: context.Timeframe,
|
||||
DetectedAt: current.Timestamp,
|
||||
KeyPriceLevel: priorSwingHigh,
|
||||
UpperBoundary: current.Close + context.CurrentAtr * 2m,
|
||||
LowerBoundary: priorSwingLow,
|
||||
InvalidationLevel: priorSwingLow,
|
||||
QualityScore: 90m,
|
||||
Description: $"Bullish structural break closing above swing high {priorSwingHigh:F2}."
|
||||
);
|
||||
}
|
||||
|
||||
// Bearish CHoCH: Clean candle body close below previous major swing low
|
||||
if (current.Close < priorSwingLow && current.Open > priorSwingLow)
|
||||
{
|
||||
return new PatternResultDto(
|
||||
Id: Guid.NewGuid(),
|
||||
Type: PatternType.ChangeOfCharacter,
|
||||
Category: PatternCategory.SmartMoney,
|
||||
Bias: PatternBias.Bearish,
|
||||
Name: "Bearish Change of Character (CHoCH)",
|
||||
Timeframe: context.Timeframe,
|
||||
DetectedAt: current.Timestamp,
|
||||
KeyPriceLevel: priorSwingLow,
|
||||
UpperBoundary: priorSwingHigh,
|
||||
LowerBoundary: current.Close - context.CurrentAtr * 2m,
|
||||
InvalidationLevel: priorSwingHigh,
|
||||
QualityScore: 90m,
|
||||
Description: $"Bearish structural break closing below swing low {priorSwingLow:F2}."
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detects institutional Order Blocks (last opposing candle before a strong directional displacement).
|
||||
/// </summary>
|
||||
public class OrderBlockDetector : IPatternDetector
|
||||
{
|
||||
public PatternType HandledType => PatternType.OrderBlock;
|
||||
public PatternCategory Category => PatternCategory.SmartMoney;
|
||||
|
||||
public PatternResultDto? Evaluate(TechnicalContext context)
|
||||
{
|
||||
var candles = context.PrimaryCandles;
|
||||
if (candles.Count < 5) return null;
|
||||
|
||||
var obCandle = candles[^3];
|
||||
var impulse1 = candles[^2];
|
||||
var impulse2 = candles.Last();
|
||||
|
||||
// Bullish Order Block: Red candle followed by 2 strong green candles that expand price > 1.5 ATR
|
||||
if (obCandle.Close < obCandle.Open && impulse1.Close > impulse1.Open && impulse2.Close > impulse2.Open)
|
||||
{
|
||||
decimal displacement = impulse2.Close - obCandle.Low;
|
||||
if (displacement >= context.CurrentAtr * 1.5m)
|
||||
{
|
||||
return new PatternResultDto(
|
||||
Id: Guid.NewGuid(),
|
||||
Type: PatternType.OrderBlock,
|
||||
Category: PatternCategory.SmartMoney,
|
||||
Bias: PatternBias.Bullish,
|
||||
Name: "Bullish Institutional Order Block",
|
||||
Timeframe: context.Timeframe,
|
||||
DetectedAt: impulse2.Timestamp,
|
||||
KeyPriceLevel: (obCandle.Open + obCandle.Close) / 2m,
|
||||
UpperBoundary: obCandle.High,
|
||||
LowerBoundary: obCandle.Low,
|
||||
InvalidationLevel: obCandle.Low * 0.995m,
|
||||
QualityScore: 86m,
|
||||
Description: $"Bullish order block zone [{obCandle.Low:F2} - {obCandle.High:F2}] with strong displacement."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user