using System;
using System.Collections.Generic;
using System.Linq;
using FinlyticCore.Dtos.TechnicalAnalysis;
namespace FinlyticTechnicals.Patterns.SmartMoney;
///
/// Detects Bullish and Bearish Fair Value Gaps (FVG) across 3-candle sequences.
///
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;
}
}
///
/// Detects Liquidity Sweeps where price takes out multi-period swing highs/lows and immediately rejects back inside the range.
///
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;
}
}
///
/// Detects Change of Character (CHoCH) structural trend reversals and Break of Structure (BOS) continuations.
///
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;
}
}
///
/// Detects institutional Order Blocks (last opposing candle before a strong directional displacement).
///
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;
}
}