1123 lines
49 KiB
C#
1123 lines
49 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using FinlyticCore.Dtos.TechnicalAnalysis;
|
|
using FinlyticTechnicals.Indicators;
|
|
|
|
namespace FinlyticTechnicals.Strategies;
|
|
|
|
/// <summary>
|
|
/// 1. Trend Pullback into Fair Value Gap with Staged Scale-Out & Free-Roll Break-Even exit.
|
|
/// </summary>
|
|
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<PatternResultDto> activePatterns)
|
|
{
|
|
var candles = context.PrimaryCandles;
|
|
if (candles.Count < 30) return null;
|
|
|
|
// 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);
|
|
|
|
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 (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<PatternResultDto> { fvgBullish };
|
|
var indicators = new Dictionary<string, decimal>
|
|
{
|
|
["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<PatternResultDto> { fvgBearish };
|
|
var indicators = new Dictionary<string, decimal>
|
|
{
|
|
["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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 2. Volatility Squeeze Breakout with Fixed Single Target (+2.0 ATR).
|
|
/// </summary>
|
|
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<PatternResultDto> 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<string, decimal> { ["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<string, decimal> { ["ATR_14"] = atr, ["SqueezeMomentum"] = squeeze.MomentumHistogram },
|
|
CreatedAt: current.Timestamp,
|
|
ExpiresAt: current.Timestamp.AddHours(4),
|
|
IsTopPick: true,
|
|
Rating: "A"
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 3. SMC Liquidity Sweep & Structural Flip with tight SL over the sweep wick.
|
|
/// </summary>
|
|
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<PatternResultDto> 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<PatternResultDto>();
|
|
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<string, decimal> { ["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<PatternResultDto>();
|
|
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<string, decimal> { ["ATR_14"] = atr },
|
|
CreatedAt: current.Timestamp,
|
|
ExpiresAt: current.Timestamp.AddHours(5),
|
|
IsTopPick: true,
|
|
Rating: "A+"
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 4. Mean Reversion from 2.5-Sigma Bollinger Band in Rangebound markets.
|
|
/// </summary>
|
|
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<PatternResultDto> 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<string, decimal>
|
|
{
|
|
["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<string, decimal>
|
|
{
|
|
["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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 5. SuperTrend Multi-Timeframe Trend Follower with Pure Trailing Stop.
|
|
/// </summary>
|
|
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<PatternResultDto> 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<string, decimal>
|
|
{
|
|
["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<string, decimal>
|
|
{
|
|
["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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<PatternResultDto> 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<string, decimal>
|
|
{
|
|
["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"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 7. EMA50/EMA200 Golden Cross & Death Cross - the textbook long-horizon trend-change signal.
|
|
/// </summary>
|
|
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<PatternResultDto> 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<string, decimal> { ["EMA_50"] = ema50Now, ["EMA_200"] = ema200Now, ["ATR_14"] = atr },
|
|
CreatedAt: current.Timestamp,
|
|
ExpiresAt: current.Timestamp.AddHours(24),
|
|
IsTopPick: true,
|
|
Rating: "A"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 8. RSI Overbought/Oversold Threshold Cross - simple, direction-agnostic momentum-reversal strategy
|
|
/// (distinct from <see cref="MeanReversionStrategy"/>, which additionally requires Bollinger-band + ADX confluence).
|
|
/// </summary>
|
|
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<PatternResultDto> 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<string, decimal> { ["RSI_14"] = rsiNow, ["ATR_14"] = atr },
|
|
CreatedAt: current.Timestamp,
|
|
ExpiresAt: current.Timestamp.AddHours(3),
|
|
IsTopPick: false,
|
|
Rating: "B"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 9. 20-Period Donchian Channel Breakout - the classic "Turtle Trading" breakout system.
|
|
/// </summary>
|
|
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<PatternResultDto> 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<string, decimal> { ["DonchianHigh"] = highestHigh, ["DonchianLow"] = lowestLow, ["ATR_14"] = atr },
|
|
CreatedAt: current.Timestamp,
|
|
ExpiresAt: current.Timestamp.AddHours(8),
|
|
IsTopPick: true,
|
|
Rating: "A"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<PatternResultDto> 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<EMA50), rally to VWAP ({vwap:F2}) with a rejection back below it.",
|
|
TriggeringPatterns: activePatterns.ToList(),
|
|
IndicatorSnapshot: new Dictionary<string, decimal> { ["VWAP"] = vwap, ["EMA_20"] = ema20, ["EMA_50"] = ema50, ["ATR_14"] = atr },
|
|
CreatedAt: current.Timestamp,
|
|
ExpiresAt: current.Timestamp.AddHours(4),
|
|
IsTopPick: false,
|
|
Rating: "B"
|
|
);
|
|
}
|
|
}
|