using Managing.Core; using Managing.Domain.Candles; using Managing.Domain.Indicators; using Managing.Domain.Shared.Rules; using Managing.Domain.Strategies.Base; using Skender.Stock.Indicators; using static Managing.Common.Enums; namespace Managing.Domain.Strategies.Signals; public class StochasticCrossIndicator : IndicatorBase { public List Signals { get; set; } public StochasticCrossIndicator( string name, int stochPeriods, int signalPeriods, int smoothPeriods, double kFactor = 3.0, double dFactor = 2.0) : base(name, IndicatorType.StochasticCross) { Signals = new List(); StochPeriods = stochPeriods; SignalPeriods = signalPeriods; SmoothPeriods = smoothPeriods; KFactor = kFactor; DFactor = dFactor; } public override List Run(IReadOnlyList candles) { if (candles.Count <= 10 * StochPeriods.Value + 50) { return null; } try { var stochResults = candles .GetStoch(StochPeriods.Value, SmoothPeriods.Value, SignalPeriods.Value) .RemoveWarmupPeriods() .ToList(); if (stochResults.Count == 0) return null; ProcessStochasticSignals(stochResults, candles); return Signals.ToList(); } catch (RuleException) { return null; } } public override List Run(IReadOnlyList candles, IndicatorsResultBase preCalculatedValues) { if (candles.Count <= 10 * StochPeriods.Value + 50) { return null; } try { // Use pre-calculated Stoch values if available List stochResults = null; if (preCalculatedValues?.Stoch != null && preCalculatedValues.Stoch.Any()) { // Filter pre-calculated Stoch values to match the candles we're processing stochResults = preCalculatedValues.Stoch .Where(s => s.K.HasValue && s.D.HasValue && candles.Any(c => c.Date == s.Date)) .ToList(); } // If no pre-calculated values or they don't match, fall back to regular calculation if (stochResults == null || !stochResults.Any()) { return Run(candles); } ProcessStochasticSignals(stochResults, candles); return Signals.ToList(); } catch (RuleException) { return null; } } /// /// Processes Stochastic signals based on %K/%D crossovers filtered by extreme zones. /// Long signals: %K crosses above %D when both lines are below 20 (oversold) /// Short signals: %K crosses below %D when both lines are above 80 (overbought) /// private void ProcessStochasticSignals(List stochResults, IReadOnlyList candles) { var stochCandles = MapStochToCandle(stochResults, candles.TakeLast(StochPeriods.Value)); if (stochCandles.Count < 2) return; var previousCandle = stochCandles[0]; foreach (var currentCandle in stochCandles.Skip(1)) { // Check for bullish crossover in oversold zone (both %K and %D < 20) if (previousCandle.PercentK < 20 && previousCandle.PercentD < 20 && currentCandle.PercentK >= 20 && currentCandle.PercentD < 20) { // %K crossed above %D in oversold zone if (previousCandle.PercentK < previousCandle.PercentD && currentCandle.PercentK >= currentCandle.PercentD) { AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium); } } // Check for bearish crossover in overbought zone (both %K and %D > 80) else if (previousCandle.PercentK > 80 && previousCandle.PercentD > 80 && currentCandle.PercentK <= 80 && currentCandle.PercentD > 80) { // %K crossed below %D in overbought zone if (previousCandle.PercentK > previousCandle.PercentD && currentCandle.PercentK <= currentCandle.PercentD) { AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium); } } previousCandle = currentCandle; } } public override IndicatorsResultBase GetIndicatorValues(IReadOnlyList candles) { return new IndicatorsResultBase() { Stoch = candles.GetStoch(StochPeriods.Value, SmoothPeriods.Value, SignalPeriods.Value) .ToList() }; } private List MapStochToCandle(IEnumerable stochResults, IEnumerable candles) { var stochCandles = new List(); foreach (var candle in candles) { var currentStoch = stochResults.Find(candle.Date); if (currentStoch != null && currentStoch.K.HasValue && currentStoch.D.HasValue) { stochCandles.Add(new CandleStoch() { Close = candle.Close, Open = candle.Open, Date = candle.Date, Ticker = candle.Ticker, Exchange = candle.Exchange, PercentK = currentStoch.K.Value, PercentD = currentStoch.D.Value }); } } return stochCandles; } private void AddSignal(CandleStoch candleSignal, TradeDirection direction, Confidence confidence) { var signal = new LightSignal( candleSignal.Ticker, direction, confidence, candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name); if (!Signals.Any(s => s.Identifier == signal.Identifier)) { Signals.AddItem(signal); } } private class CandleStoch : Candle { public double PercentK { get; internal set; } public double PercentD { get; internal set; } } }