using System; using System.Collections.Generic; using System.Linq; using FinlyticCore.Dtos.TechnicalAnalysis; using FinlyticTechnicals.Indicators; namespace FinlyticTechnicals.Strategies; /// /// 1. Trend Pullback into Fair Value Gap with Staged Scale-Out & Free-Roll Break-Even exit. /// public class TrendPullbackFvgStrategy : ITechnicalStrategy { public string StrategyKey => "TrendPullbackFvg"; public string StrategyName => "Trend Pullback FVG Retracement"; public int Priority => 1; public bool IsApplicable(MarketRegime regime) => regime == MarketRegime.BullishTrending || regime == MarketRegime.BearishTrending; public StrategyResultDto? Evaluate(TechnicalContext context, IReadOnlyList activePatterns) { var candles = context.PrimaryCandles; // Tunable for backtesting only (see TechnicalContext.ParameterOverrides doc comment) - defaults match // this strategy's original hardcoded values, so live scanning behavior is unchanged. int emaFastPeriod = (int)context.GetParameter(StrategyKey, "EmaFast", 20m); int emaMidPeriod = (int)context.GetParameter(StrategyKey, "EmaMid", 50m); int emaSlowPeriod = (int)context.GetParameter(StrategyKey, "EmaSlow", 200m); decimal stopAtrMultiplier = context.GetParameter(StrategyKey, "StopAtrMultiplier", 1.2m); if (candles.Count < emaSlowPeriod + 5) return null; var current = candles.Last(); decimal ema20 = TechnicalIndicatorsEngine.CalculateEma(candles, emaFastPeriod); decimal ema50 = TechnicalIndicatorsEngine.CalculateEma(candles, emaMidPeriod); decimal ema200 = TechnicalIndicatorsEngine.CalculateEma(candles, emaSlowPeriod); decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); if (ema20 <= 0m || ema50 <= 0m || ema200 <= 0m || atr <= 0) return null; // Long Setup: Bullish Trend (EMA20 > EMA50 > EMA200) + Bullish FVG retracement. bool isBullishTrend = ema20 > ema50 && ema50 > ema200 && current.Close > ema50; var fvgBullish = activePatterns.FirstOrDefault(p => p.Type == PatternType.FairValueGapBullish && p.Bias == PatternBias.Bullish); if (isBullishTrend && fvgBullish != null) { decimal entry = current.Close; decimal stopLoss = Math.Min(fvgBullish.InvalidationLevel, entry - (stopAtrMultiplier * atr)); decimal risk = entry - stopLoss; if (risk <= 0) return null; decimal tp1 = entry + (1.5m * risk); decimal tp2 = entry + (3.0m * risk); decimal rrr = (tp2 - entry) / risk; var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.StagedScaleOutWithBreakEven, InitialStopLoss: stopLoss, TakeProfitStages: [ new TakeProfitStage(1, tp1, 0.50m, 1.5m, "TP1: Scale-out 50% & Trigger Break-Even"), new TakeProfitStage(2, tp2, 0.30m, 3.0m, "TP2: Scale-out 30%"), ], BreakEvenRule: new BreakEvenRule( Enabled: true, TriggerPrice: tp1, OffsetToCoverFees: entry + (risk * 0.05m) ), TrailingStopRule: new TrailingStopRule( Type: TrailingStopType.AtrMultiplier, Multiplier: 1.5m, ActivationPrice: tp1, IndicatorKey: "ATR_14" ), MaxHoldingBars: 50 ); var triggers = new List { fvgBullish }; var indicators = new Dictionary { ["EMA_20"] = ema20, ["EMA_50"] = ema50, ["EMA_200"] = ema200, ["ATR_14"] = atr }; return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: SignalDirection.Buy, QualityScore: 88m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: rrr, ExitPlan: exitPlan, TechnicalRationale: $"Bullish trend alignment (EMA20 > EMA50 > EMA200) with retracement into 15m FVG zone [{fvgBullish.LowerBoundary:F2} - {fvgBullish.UpperBoundary:F2}].", TriggeringPatterns: triggers, IndicatorSnapshot: indicators, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(6), IsTopPick: true, Rating: "A+" ); } // Short Setup (mirror image): Bearish Trend (EMA20 < EMA50 < EMA200) + Bearish FVG retracement. bool isBearishTrend = ema20 < ema50 && ema50 < ema200 && current.Close < ema50; var fvgBearish = activePatterns.FirstOrDefault(p => p.Type == PatternType.FairValueGapBearish && p.Bias == PatternBias.Bearish); if (isBearishTrend && fvgBearish != null) { decimal entry = current.Close; decimal stopLoss = Math.Max(fvgBearish.InvalidationLevel, entry + (stopAtrMultiplier * atr)); decimal risk = stopLoss - entry; if (risk <= 0) return null; decimal tp1 = entry - (1.5m * risk); decimal tp2 = entry - (3.0m * risk); decimal rrr = (entry - tp2) / risk; var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.StagedScaleOutWithBreakEven, InitialStopLoss: stopLoss, TakeProfitStages: [ new TakeProfitStage(1, tp1, 0.50m, 1.5m, "TP1: Scale-out 50% & Trigger Break-Even"), new TakeProfitStage(2, tp2, 0.30m, 3.0m, "TP2: Scale-out 30%"), ], BreakEvenRule: new BreakEvenRule( Enabled: true, TriggerPrice: tp1, OffsetToCoverFees: entry - (risk * 0.05m) ), TrailingStopRule: new TrailingStopRule( Type: TrailingStopType.AtrMultiplier, Multiplier: 1.5m, ActivationPrice: tp1, IndicatorKey: "ATR_14" ), MaxHoldingBars: 50 ); var triggers = new List { fvgBearish }; var indicators = new Dictionary { ["EMA_20"] = ema20, ["EMA_50"] = ema50, ["EMA_200"] = ema200, ["ATR_14"] = atr }; return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: SignalDirection.Sell, QualityScore: 88m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: rrr, ExitPlan: exitPlan, TechnicalRationale: $"Bearish trend alignment (EMA20 < EMA50 < EMA200) with retracement into 15m FVG zone [{fvgBearish.LowerBoundary:F2} - {fvgBearish.UpperBoundary:F2}].", TriggeringPatterns: triggers, IndicatorSnapshot: indicators, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(6), IsTopPick: true, Rating: "A+" ); } return null; } } /// /// 2. Volatility Squeeze Breakout with Fixed Single Target (+2.0 ATR). /// public class VolatilitySqueezeStrategy : ITechnicalStrategy { public string StrategyKey => "VolatilitySqueeze"; public string StrategyName => "Bollinger/Keltner Squeeze Breakout"; public int Priority => 2; public bool IsApplicable(MarketRegime regime) => true; public StrategyResultDto? Evaluate(TechnicalContext context, IReadOnlyList activePatterns) { var candles = context.PrimaryCandles; if (candles.Count < 25) return null; decimal stopAtrMultiplier = context.GetParameter(StrategyKey, "StopAtrMultiplier", 1.0m); decimal targetAtrMultiplier = context.GetParameter(StrategyKey, "TargetAtrMultiplier", 2.0m); var current = candles.Last(); var squeeze = TechnicalIndicatorsEngine.CalculateVolatilitySqueeze(candles); decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); if (atr <= 0) return null; // Fired Bullish: Squeeze fired out of compression with positive momentum if (squeeze.SqueezeState == "FIRED_BULLISH" && squeeze.MomentumHistogram > 0) { decimal entry = current.Close; decimal stopLoss = entry - (stopAtrMultiplier * atr); decimal target = entry + (targetAtrMultiplier * atr); decimal risk = entry - stopLoss; var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.FixedSingleTarget, InitialStopLoss: stopLoss, TakeProfitStages: [ new TakeProfitStage(1, target, 1.00m, 2.0m, "Target: 100% exit at +2.0 ATR") ], MaxHoldingBars: 20 ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: SignalDirection.Buy, QualityScore: 84m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: 2.0m, ExitPlan: exitPlan, TechnicalRationale: $"Bollinger compression inside Keltner Channels fired bullish momentum ({squeeze.MomentumHistogram:F3}).", TriggeringPatterns: activePatterns.ToList(), IndicatorSnapshot: new Dictionary { ["ATR_14"] = atr, ["SqueezeMomentum"] = squeeze.MomentumHistogram }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(4), IsTopPick: true, Rating: "A" ); } // Fired Bearish: Squeeze fired out of compression with negative momentum (mirror image of above). if (squeeze.SqueezeState == "FIRED_BEARISH" && squeeze.MomentumHistogram < 0) { decimal entry = current.Close; decimal stopLoss = entry + (stopAtrMultiplier * atr); decimal target = entry - (targetAtrMultiplier * atr); decimal risk = stopLoss - entry; var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.FixedSingleTarget, InitialStopLoss: stopLoss, TakeProfitStages: [ new TakeProfitStage(1, target, 1.00m, 2.0m, "Target: 100% exit at -2.0 ATR") ], MaxHoldingBars: 20 ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: SignalDirection.Sell, QualityScore: 84m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: 2.0m, ExitPlan: exitPlan, TechnicalRationale: $"Bollinger compression inside Keltner Channels fired bearish momentum ({squeeze.MomentumHistogram:F3}).", TriggeringPatterns: activePatterns.ToList(), IndicatorSnapshot: new Dictionary { ["ATR_14"] = atr, ["SqueezeMomentum"] = squeeze.MomentumHistogram }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(4), IsTopPick: true, Rating: "A" ); } return null; } } /// /// 3. SMC Liquidity Sweep & Structural Flip with tight SL over the sweep wick. /// public class SmcLiquiditySweepStrategy : ITechnicalStrategy { public string StrategyKey => "SmcLiquiditySweep"; public string StrategyName => "Smart Money Liquidity Sweep & CHoCH"; public int Priority => 3; public bool IsApplicable(MarketRegime regime) => true; public StrategyResultDto? Evaluate(TechnicalContext context, IReadOnlyList activePatterns) { var sweepLow = activePatterns.FirstOrDefault(p => p.Type == PatternType.LiquiditySweepLow); var choch = activePatterns.FirstOrDefault(p => p.Type == PatternType.ChangeOfCharacter && p.Bias == PatternBias.Bullish); var sweepHigh = activePatterns.FirstOrDefault(p => p.Type == PatternType.LiquiditySweepHigh); var chochBearish = activePatterns.FirstOrDefault(p => p.Type == PatternType.ChangeOfCharacter && p.Bias == PatternBias.Bearish); decimal stopBufferPercent = context.GetParameter(StrategyKey, "StopBufferPercent", 0.2m); if (sweepLow != null || choch != null) { var candles = context.PrimaryCandles; var current = candles.Last(); decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); decimal entry = current.Close; decimal stopLoss = (sweepLow?.LowerBoundary ?? current.Low) * (1m - (stopBufferPercent / 100m)); decimal risk = entry - stopLoss; if (risk <= 0) return null; decimal tp1 = entry + (2.0m * risk); decimal tp2 = entry + (4.0m * risk); var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.StagedScaleOutWithBreakEven, InitialStopLoss: stopLoss, TakeProfitStages: [ new TakeProfitStage(1, tp1, 0.60m, 2.0m, "TP1: 60% Scale-Out & Instant Free-Roll"), new TakeProfitStage(2, tp2, 0.40m, 4.0m, "TP2: 40% Final Target") ], BreakEvenRule: new BreakEvenRule(true, tp1, entry + (risk * 0.05m)), MaxHoldingBars: 35 ); var triggers = new List(); if (sweepLow != null) triggers.Add(sweepLow); if (choch != null) triggers.Add(choch); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: SignalDirection.Buy, QualityScore: 91m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: (tp2 - entry) / risk, ExitPlan: exitPlan, TechnicalRationale: $"Institutional liquidity sweep below {sweepLow?.KeyPriceLevel:F2} followed by buyer absorption and structural rejection.", TriggeringPatterns: triggers, IndicatorSnapshot: new Dictionary { ["ATR_14"] = atr }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(5), IsTopPick: true, Rating: "A+" ); } // Mirror image: a sweep above a known high (stop-loss hunt against shorts/breakout buyers) followed by // a bearish Change-of-Character - interpreted as institutional sellers absorbing that liquidity. if (sweepHigh != null || chochBearish != null) { var candles = context.PrimaryCandles; var current = candles.Last(); decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); decimal entry = current.Close; decimal stopLoss = (sweepHigh?.UpperBoundary ?? current.High) * (1m + (stopBufferPercent / 100m)); decimal risk = stopLoss - entry; if (risk <= 0) return null; decimal tp1 = entry - (2.0m * risk); decimal tp2 = entry - (4.0m * risk); var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.StagedScaleOutWithBreakEven, InitialStopLoss: stopLoss, TakeProfitStages: [ new TakeProfitStage(1, tp1, 0.60m, 2.0m, "TP1: 60% Scale-Out & Instant Free-Roll"), new TakeProfitStage(2, tp2, 0.40m, 4.0m, "TP2: 40% Final Target") ], BreakEvenRule: new BreakEvenRule(true, tp1, entry - (risk * 0.05m)), MaxHoldingBars: 35 ); var triggersBearish = new List(); if (sweepHigh != null) triggersBearish.Add(sweepHigh); if (chochBearish != null) triggersBearish.Add(chochBearish); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: SignalDirection.Sell, QualityScore: 91m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: (entry - tp2) / risk, ExitPlan: exitPlan, TechnicalRationale: $"Institutional liquidity sweep above {sweepHigh?.KeyPriceLevel:F2} followed by seller absorption and structural rejection.", TriggeringPatterns: triggersBearish, IndicatorSnapshot: new Dictionary { ["ATR_14"] = atr }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(5), IsTopPick: true, Rating: "A+" ); } return null; } } /// /// 4. Mean Reversion from 2.5-Sigma Bollinger Band in Rangebound markets. /// public class MeanReversionStrategy : ITechnicalStrategy { public string StrategyKey => "MeanReversion"; public string StrategyName => "Bollinger 2.5-Sigma Mean Reversion"; public int Priority => 4; public bool IsApplicable(MarketRegime regime) => regime == MarketRegime.LowVolatilityRangebound || regime == MarketRegime.HighVolatilityChoppy; public StrategyResultDto? Evaluate(TechnicalContext context, IReadOnlyList activePatterns) { var candles = context.PrimaryCandles; if (candles.Count < 25) return null; decimal bollingerMultiplier = context.GetParameter(StrategyKey, "BollingerMultiplier", 2.5m); decimal adxThreshold = context.GetParameter(StrategyKey, "AdxThreshold", 22m); decimal rsiOversold = context.GetParameter(StrategyKey, "RsiOversold", 32m); decimal rsiOverbought = context.GetParameter(StrategyKey, "RsiOverbought", 68m); var current = candles.Last(); var bb = TechnicalIndicatorsEngine.CalculateBollingerBands(candles, 20, bollingerMultiplier); decimal rsi = TechnicalIndicatorsEngine.CalculateRsi(candles, 14); var adx = TechnicalIndicatorsEngine.CalculateAdx(candles, 14); // Rangebound with low ADX and oversold RSI touching the lower band if (adx.Adx < adxThreshold && rsi <= rsiOversold && current.Low <= bb.LowerBand) { decimal entry = current.Close; decimal vwapTarget = TechnicalIndicatorsEngine.CalculateVwap(candles); if (vwapTarget <= entry) vwapTarget = bb.MiddleBand; decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); decimal stopLoss = current.Low - (0.8m * atr); decimal risk = entry - stopLoss; if (risk <= 0 || vwapTarget <= entry) return null; var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.DynamicBandTouch, InitialStopLoss: stopLoss, TakeProfitStages: [ new TakeProfitStage(1, vwapTarget, 1.00m, (vwapTarget - entry) / risk, "Target: 100% Exit at VWAP / SMA20") ], MaxHoldingBars: 15 ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: SignalDirection.Buy, QualityScore: 79m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: (vwapTarget - entry) / risk, ExitPlan: exitPlan, TechnicalRationale: $"Oversold {bollingerMultiplier:F1}-sigma Bollinger stretch (RSI {rsi:F1}, ADX {adx.Adx:F1}) targeting mean reversion back to VWAP {vwapTarget:F2}.", TriggeringPatterns: activePatterns.ToList(), IndicatorSnapshot: new Dictionary { ["RSI_14"] = rsi, ["ADX_14"] = adx.Adx, ["BB_Lower"] = bb.LowerBand, ["VWAP"] = vwapTarget }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(3), IsTopPick: false, Rating: "B" ); } // Mirror image: rangebound with low ADX and overbought RSI touching the upper band. if (adx.Adx < adxThreshold && rsi >= rsiOverbought && current.High >= bb.UpperBand) { decimal entry = current.Close; decimal vwapTarget = TechnicalIndicatorsEngine.CalculateVwap(candles); if (vwapTarget >= entry) vwapTarget = bb.MiddleBand; decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); decimal stopLoss = current.High + (0.8m * atr); decimal risk = stopLoss - entry; if (risk <= 0 || vwapTarget >= entry) return null; var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.DynamicBandTouch, InitialStopLoss: stopLoss, TakeProfitStages: [ new TakeProfitStage(1, vwapTarget, 1.00m, (entry - vwapTarget) / risk, "Target: 100% Exit at VWAP / SMA20") ], MaxHoldingBars: 15 ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: SignalDirection.Sell, QualityScore: 79m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: (entry - vwapTarget) / risk, TechnicalRationale: $"Overbought {bollingerMultiplier:F1}-sigma Bollinger stretch (RSI {rsi:F1}, ADX {adx.Adx:F1}) targeting mean reversion back to VWAP {vwapTarget:F2}.", ExitPlan: exitPlan, TriggeringPatterns: activePatterns.ToList(), IndicatorSnapshot: new Dictionary { ["RSI_14"] = rsi, ["ADX_14"] = adx.Adx, ["BB_Upper"] = bb.UpperBand, ["VWAP"] = vwapTarget }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(3), IsTopPick: false, Rating: "B" ); } return null; } } /// /// 5. SuperTrend Multi-Timeframe Trend Follower with Pure Trailing Stop. /// public class SuperTrendMultiTfStrategy : ITechnicalStrategy { public string StrategyKey => "SuperTrendMultiTf"; public string StrategyName => "SuperTrend Multi-Timeframe Alignment"; public int Priority => 5; public bool IsApplicable(MarketRegime regime) => true; public StrategyResultDto? Evaluate(TechnicalContext context, IReadOnlyList activePatterns) { var candles15m = context.GetCandles("15m"); var candles1h = context.GetCandles("1h"); if (candles15m.Count < 15 || candles1h.Count < 15) return null; int stPeriod = (int)context.GetParameter(StrategyKey, "Period", 10m); decimal stMultiplier = context.GetParameter(StrategyKey, "Multiplier", 3.0m); var st1h = TechnicalIndicatorsEngine.CalculateSuperTrend(candles1h, stPeriod, stMultiplier); var st15m = TechnicalIndicatorsEngine.CalculateSuperTrend(candles15m, stPeriod, stMultiplier); // Bullish Confluence: 1h SuperTrend is BUY and 15m SuperTrend just flipped to BUY or is bullish if (st1h.Direction == SignalDirection.Buy && st15m.Direction == SignalDirection.Buy) { var current = candles15m.Last(); decimal entry = current.Close; decimal stopLoss = st15m.Value; decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles15m, 14); decimal risk = entry - stopLoss; if (risk <= 0) return null; var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.PureTrailingStop, InitialStopLoss: stopLoss, TakeProfitStages: [], TrailingStopRule: new TrailingStopRule( Type: TrailingStopType.SuperTrendLine, Multiplier: stMultiplier, ActivationPrice: entry, IndicatorKey: "SuperTrend_15m" ), ReversalCondition: new ReversalCondition( RuleDescription: "Exit immediately if 15m SuperTrend flips to Bearish", IndicatorTrigger: "SuperTrend_15m_Flip_Sell" ) ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: "15m", StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: SignalDirection.Buy, QualityScore: 86m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: 3.0m, ExitPlan: exitPlan, TechnicalRationale: $"1h macro SuperTrend and 15m micro SuperTrend in bullish confluence with dynamic trailing stop at {stopLoss:F2}.", TriggeringPatterns: activePatterns.ToList(), IndicatorSnapshot: new Dictionary { ["SuperTrend_1h"] = st1h.Value, ["SuperTrend_15m"] = st15m.Value, ["ATR_14"] = atr }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(8), IsTopPick: true, Rating: "A" ); } // Bearish Confluence (mirror image): 1h SuperTrend is SELL and 15m SuperTrend is also bearish. if (st1h.Direction == SignalDirection.Sell && st15m.Direction == SignalDirection.Sell) { var current = candles15m.Last(); decimal entry = current.Close; decimal stopLoss = st15m.Value; decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles15m, 14); decimal risk = stopLoss - entry; if (risk <= 0) return null; var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.PureTrailingStop, InitialStopLoss: stopLoss, TakeProfitStages: [], TrailingStopRule: new TrailingStopRule( Type: TrailingStopType.SuperTrendLine, Multiplier: stMultiplier, ActivationPrice: entry, IndicatorKey: "SuperTrend_15m" ), ReversalCondition: new ReversalCondition( RuleDescription: "Exit immediately if 15m SuperTrend flips to Bullish", IndicatorTrigger: "SuperTrend_15m_Flip_Buy" ) ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: "15m", StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: SignalDirection.Sell, QualityScore: 86m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: 3.0m, ExitPlan: exitPlan, TechnicalRationale: $"1h macro SuperTrend and 15m micro SuperTrend in bearish confluence with dynamic trailing stop at {stopLoss:F2}.", TriggeringPatterns: activePatterns.ToList(), IndicatorSnapshot: new Dictionary { ["SuperTrend_1h"] = st1h.Value, ["SuperTrend_15m"] = st15m.Value, ["ATR_14"] = atr }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(8), IsTopPick: true, Rating: "A" ); } return null; } } /// /// 6. MACD Signal Line Crossover - classic momentum-shift strategy. Fires when the MACD line crosses the /// signal line (compared against the same calculation one bar earlier) with the histogram confirming direction. /// public class MacdCrossoverStrategy : ITechnicalStrategy { public string StrategyKey => "MacdCrossover"; public string StrategyName => "MACD Signal Line Crossover"; public int Priority => 6; public bool IsApplicable(MarketRegime regime) => true; public StrategyResultDto? Evaluate(TechnicalContext context, IReadOnlyList activePatterns) { var candles = context.PrimaryCandles; if (candles.Count < 40) return null; var current = candles.Last(); var previousCandles = candles.Take(candles.Count - 1).ToList(); if (previousCandles.Count < 35) return null; int fastPeriod = (int)context.GetParameter(StrategyKey, "FastPeriod", 12m); int slowPeriod = (int)context.GetParameter(StrategyKey, "SlowPeriod", 26m); int signalPeriod = (int)context.GetParameter(StrategyKey, "SignalPeriod", 9m); decimal stopAtrMultiplier = context.GetParameter(StrategyKey, "StopAtrMultiplier", 1.5m); var macdNow = TechnicalIndicatorsEngine.CalculateMacd(candles, fastPeriod, slowPeriod, signalPeriod); var macdPrev = TechnicalIndicatorsEngine.CalculateMacd(previousCandles, fastPeriod, slowPeriod, signalPeriod); decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); if (atr <= 0) return null; bool bullishCross = macdPrev.MacdLine <= macdPrev.SignalLine && macdNow.MacdLine > macdNow.SignalLine && macdNow.Histogram > 0; bool bearishCross = macdPrev.MacdLine >= macdPrev.SignalLine && macdNow.MacdLine < macdNow.SignalLine && macdNow.Histogram < 0; if (!bullishCross && !bearishCross) return null; decimal entry = current.Close; SignalDirection direction = bullishCross ? SignalDirection.Buy : SignalDirection.Sell; decimal stopLoss = direction == SignalDirection.Buy ? entry - (stopAtrMultiplier * atr) : entry + (stopAtrMultiplier * atr); decimal risk = Math.Abs(entry - stopLoss); if (risk <= 0) return null; decimal tp1 = direction == SignalDirection.Buy ? entry + (2.0m * risk) : entry - (2.0m * risk); var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.FixedSingleTarget, InitialStopLoss: stopLoss, TakeProfitStages: [new TakeProfitStage(1, tp1, 1.00m, 2.0m, "Target: 100% exit at +2.0R")], MaxHoldingBars: 30 ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: direction, QualityScore: 80m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: 2.0m, ExitPlan: exitPlan, TechnicalRationale: bullishCross ? $"MACD line ({macdNow.MacdLine:F3}) crossed above the signal line ({macdNow.SignalLine:F3}) with a positive histogram." : $"MACD line ({macdNow.MacdLine:F3}) crossed below the signal line ({macdNow.SignalLine:F3}) with a negative histogram.", TriggeringPatterns: activePatterns.ToList(), IndicatorSnapshot: new Dictionary { ["MACD_Line"] = macdNow.MacdLine, ["MACD_Signal"] = macdNow.SignalLine, ["MACD_Histogram"] = macdNow.Histogram, ["ATR_14"] = atr }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(5), IsTopPick: false, Rating: "B" ); } } /// /// 7. EMA50/EMA200 Golden Cross & Death Cross - the textbook long-horizon trend-change signal. /// public class MovingAverageCrossoverStrategy : ITechnicalStrategy { public string StrategyKey => "MovingAverageCrossover"; public string StrategyName => "EMA50/EMA200 Golden & Death Cross"; public int Priority => 7; public bool IsApplicable(MarketRegime regime) => true; public StrategyResultDto? Evaluate(TechnicalContext context, IReadOnlyList activePatterns) { var candles = context.PrimaryCandles; int fastPeriod = (int)context.GetParameter(StrategyKey, "FastPeriod", 50m); int slowPeriod = (int)context.GetParameter(StrategyKey, "SlowPeriod", 200m); decimal stopAtrMultiplier = context.GetParameter(StrategyKey, "StopAtrMultiplier", 2.0m); if (candles.Count < slowPeriod + 10) return null; var current = candles.Last(); var previousCandles = candles.Take(candles.Count - 1).ToList(); decimal ema50Now = TechnicalIndicatorsEngine.CalculateEma(candles, fastPeriod); decimal ema200Now = TechnicalIndicatorsEngine.CalculateEma(candles, slowPeriod); decimal ema50Prev = TechnicalIndicatorsEngine.CalculateEma(previousCandles, fastPeriod); decimal ema200Prev = TechnicalIndicatorsEngine.CalculateEma(previousCandles, slowPeriod); decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); if (atr <= 0) return null; bool goldenCross = ema50Prev <= ema200Prev && ema50Now > ema200Now; bool deathCross = ema50Prev >= ema200Prev && ema50Now < ema200Now; if (!goldenCross && !deathCross) return null; decimal entry = current.Close; SignalDirection direction = goldenCross ? SignalDirection.Buy : SignalDirection.Sell; decimal stopLoss = direction == SignalDirection.Buy ? entry - (stopAtrMultiplier * atr) : entry + (stopAtrMultiplier * atr); decimal risk = Math.Abs(entry - stopLoss); if (risk <= 0) return null; var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.PureTrailingStop, InitialStopLoss: stopLoss, TakeProfitStages: [], TrailingStopRule: new TrailingStopRule( Type: TrailingStopType.AtrMultiplier, Multiplier: 2.5m, ActivationPrice: entry, IndicatorKey: "ATR_14" ), MaxHoldingBars: 100 ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: direction, QualityScore: 82m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: 2.5m, ExitPlan: exitPlan, TechnicalRationale: goldenCross ? $"Golden Cross: EMA50 ({ema50Now:F2}) crossed above EMA200 ({ema200Now:F2})." : $"Death Cross: EMA50 ({ema50Now:F2}) crossed below EMA200 ({ema200Now:F2}).", TriggeringPatterns: activePatterns.ToList(), IndicatorSnapshot: new Dictionary { ["EMA_50"] = ema50Now, ["EMA_200"] = ema200Now, ["ATR_14"] = atr }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(24), IsTopPick: true, Rating: "A" ); } } /// /// 8. RSI Overbought/Oversold Threshold Cross - simple, direction-agnostic momentum-reversal strategy /// (distinct from , which additionally requires Bollinger-band + ADX confluence). /// public class RsiReversalStrategy : ITechnicalStrategy { public string StrategyKey => "RsiReversal"; public string StrategyName => "RSI Overbought/Oversold Threshold Cross"; public int Priority => 8; public bool IsApplicable(MarketRegime regime) => true; public StrategyResultDto? Evaluate(TechnicalContext context, IReadOnlyList activePatterns) { var candles = context.PrimaryCandles; if (candles.Count < 30) return null; var current = candles.Last(); var previousCandles = candles.Take(candles.Count - 1).ToList(); if (previousCandles.Count < 15) return null; int rsiPeriod = (int)context.GetParameter(StrategyKey, "Period", 14m); decimal oversoldThreshold = context.GetParameter(StrategyKey, "OversoldThreshold", 30m); decimal overboughtThreshold = context.GetParameter(StrategyKey, "OverboughtThreshold", 70m); decimal stopAtrMultiplier = context.GetParameter(StrategyKey, "StopAtrMultiplier", 1.2m); decimal rsiNow = TechnicalIndicatorsEngine.CalculateRsi(candles, rsiPeriod); decimal rsiPrev = TechnicalIndicatorsEngine.CalculateRsi(previousCandles, rsiPeriod); decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); if (atr <= 0) return null; bool bullishCross = rsiPrev <= oversoldThreshold && rsiNow > oversoldThreshold; bool bearishCross = rsiPrev >= overboughtThreshold && rsiNow < overboughtThreshold; if (!bullishCross && !bearishCross) return null; decimal entry = current.Close; SignalDirection direction = bullishCross ? SignalDirection.Buy : SignalDirection.Sell; decimal stopLoss = direction == SignalDirection.Buy ? entry - (stopAtrMultiplier * atr) : entry + (stopAtrMultiplier * atr); decimal risk = Math.Abs(entry - stopLoss); if (risk <= 0) return null; decimal tp1 = direction == SignalDirection.Buy ? entry + (1.5m * risk) : entry - (1.5m * risk); var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.FixedSingleTarget, InitialStopLoss: stopLoss, TakeProfitStages: [new TakeProfitStage(1, tp1, 1.00m, 1.5m, "Target: 100% exit at +1.5R")], MaxHoldingBars: 20 ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: direction, QualityScore: 75m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: 1.5m, ExitPlan: exitPlan, TechnicalRationale: bullishCross ? $"RSI ({rsiNow:F1}) crossed back above the oversold threshold of 30." : $"RSI ({rsiNow:F1}) crossed back below the overbought threshold of 70.", TriggeringPatterns: activePatterns.ToList(), IndicatorSnapshot: new Dictionary { ["RSI_14"] = rsiNow, ["ATR_14"] = atr }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(3), IsTopPick: false, Rating: "B" ); } } /// /// 9. 20-Period Donchian Channel Breakout - the classic "Turtle Trading" breakout system. /// public class DonchianBreakoutStrategy : ITechnicalStrategy { public string StrategyKey => "DonchianBreakout"; public string StrategyName => "20-Period Donchian Channel Breakout"; public int Priority => 9; public bool IsApplicable(MarketRegime regime) => true; public StrategyResultDto? Evaluate(TechnicalContext context, IReadOnlyList activePatterns) { int period = (int)context.GetParameter(StrategyKey, "Period", 20m); var candles = context.PrimaryCandles; if (candles.Count < period + 2) return null; var current = candles.Last(); // Prior N bars, excluding the current bar itself - a breakout is a close beyond the range that had // already formed BEFORE this bar, not beyond a range that includes the breakout bar itself. var priorWindow = candles.Skip(candles.Count - 1 - period).Take(period).ToList(); decimal highestHigh = priorWindow.Max(c => c.High); decimal lowestLow = priorWindow.Min(c => c.Low); decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); if (atr <= 0) return null; bool bullishBreakout = current.Close > highestHigh; bool bearishBreakout = current.Close < lowestLow; if (!bullishBreakout && !bearishBreakout) return null; decimal entry = current.Close; SignalDirection direction = bullishBreakout ? SignalDirection.Buy : SignalDirection.Sell; decimal stopLoss = direction == SignalDirection.Buy ? lowestLow : highestHigh; decimal risk = Math.Abs(entry - stopLoss); if (risk <= 0) return null; var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.PureTrailingStop, InitialStopLoss: stopLoss, TakeProfitStages: [], TrailingStopRule: new TrailingStopRule( Type: TrailingStopType.AtrMultiplier, Multiplier: 2.0m, ActivationPrice: entry, IndicatorKey: "ATR_14" ), MaxHoldingBars: 40 ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: direction, QualityScore: 83m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: 2.0m, ExitPlan: exitPlan, TechnicalRationale: bullishBreakout ? $"Breakout above the {period}-period high at {highestHigh:F2} (Donchian channel)." : $"Breakdown below the {period}-period low at {lowestLow:F2} (Donchian channel).", TriggeringPatterns: activePatterns.ToList(), IndicatorSnapshot: new Dictionary { ["DonchianHigh"] = highestHigh, ["DonchianLow"] = lowestLow, ["ATR_14"] = atr }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(8), IsTopPick: true, Rating: "A" ); } } /// /// 10. VWAP Pullback & Bounce Confirmation - trades a retest of the session VWAP in the direction of the /// prevailing short-term trend once price rejects back away from it. /// public class VwapBounceStrategy : ITechnicalStrategy { public string StrategyKey => "VwapBounce"; public string StrategyName => "VWAP Pullback & Bounce Confirmation"; public int Priority => 10; public bool IsApplicable(MarketRegime regime) => regime == MarketRegime.BullishTrending || regime == MarketRegime.BearishTrending; public StrategyResultDto? Evaluate(TechnicalContext context, IReadOnlyList activePatterns) { var candles = context.PrimaryCandles; if (candles.Count < 30) return null; int emaFastPeriod = (int)context.GetParameter(StrategyKey, "EmaFast", 20m); int emaSlowPeriod = (int)context.GetParameter(StrategyKey, "EmaSlow", 50m); decimal stopAtrMultiplier = context.GetParameter(StrategyKey, "StopAtrMultiplier", 1.0m); var current = candles.Last(); var previous = candles[^2]; decimal vwap = TechnicalIndicatorsEngine.CalculateVwap(candles); decimal ema20 = TechnicalIndicatorsEngine.CalculateEma(candles, emaFastPeriod); decimal ema50 = TechnicalIndicatorsEngine.CalculateEma(candles, emaSlowPeriod); decimal atr = context.CurrentAtr > 0 ? context.CurrentAtr : TechnicalIndicatorsEngine.CalculateAtr(candles, 14); if (atr <= 0 || vwap <= 0) return null; // Bullish: uptrend, prior bar dipped to/below VWAP, current bar closed back above it (rejection/bounce). bool bullishBounce = ema20 > ema50 && previous.Low <= vwap && current.Close > vwap; // Bearish: downtrend, prior bar rallied to/above VWAP, current bar closed back below it (rejection). bool bearishBounce = ema20 < ema50 && previous.High >= vwap && current.Close < vwap; if (!bullishBounce && !bearishBounce) return null; decimal entry = current.Close; SignalDirection direction = bullishBounce ? SignalDirection.Buy : SignalDirection.Sell; decimal stopLoss = direction == SignalDirection.Buy ? Math.Min(previous.Low, entry - (stopAtrMultiplier * atr)) : Math.Max(previous.High, entry + (stopAtrMultiplier * atr)); decimal risk = Math.Abs(entry - stopLoss); if (risk <= 0) return null; decimal tp1 = direction == SignalDirection.Buy ? entry + (2.0m * risk) : entry - (2.0m * risk); var exitPlan = new ExitPlan( StrategyType: ExitStrategyType.FixedSingleTarget, InitialStopLoss: stopLoss, TakeProfitStages: [new TakeProfitStage(1, tp1, 1.00m, 2.0m, "Target: 100% exit at +2.0R")], MaxHoldingBars: 25 ); return new StrategyResultDto( SetupId: Guid.NewGuid(), Isin: context.Isin, Symbol: context.Symbol, Timeframe: context.Timeframe, StrategyKey: StrategyKey, StrategyName: StrategyName, Direction: direction, QualityScore: 81m, CurrentPrice: current.Close, EntryPrice: entry, InvalidationPrice: stopLoss, CurrentAtr: atr, EstimatedRiskRewardRatio: 2.0m, ExitPlan: exitPlan, TechnicalRationale: bullishBounce ? $"Uptrend (EMA20>EMA50), pullback to VWAP ({vwap:F2}) with a bounce back above it." : $"Downtrend (EMA20 { ["VWAP"] = vwap, ["EMA_20"] = ema20, ["EMA_50"] = ema50, ["ATR_14"] = atr }, CreatedAt: current.Timestamp, ExpiresAt: current.Timestamp.AddHours(4), IsTopPick: false, Rating: "B" ); } }