From 43d7c5c9293746edc40ed87d8b15046434501384 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Mon, 24 Nov 2025 10:39:53 +0700 Subject: [PATCH] Add StochasticCross indicator support in GeneticService and related classes - Introduced StochasticCross indicator type in GeneticService with configuration settings for stochPeriods, signalPeriods, smoothPeriods, kFactor, and dFactor. - Updated IIndicator interface and IndicatorBase class to include KFactor and DFactor properties. - Enhanced LightIndicator class to support new properties and ensure proper conversion back to full Indicator. - Modified ScenarioHelpers to handle StochasticCross indicator creation and validation, ensuring default values and error handling for kFactor and dFactor. --- src/Managing.Application/GeneticService.cs | 17 ++ src/Managing.Common/Enums.cs | 1 + src/Managing.Domain/Indicators/IIndicator.cs | 2 + .../Indicators/IndicatorBase.cs | 4 + .../Indicators/LightIndicator.cs | 8 +- .../Signals/StochasticCrossIndicator.cs | 191 ++++++++++++++++++ .../Scenarios/ScenarioHelpers.cs | 35 +++- 7 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 src/Managing.Domain/Indicators/Signals/StochasticCrossIndicator.cs diff --git a/src/Managing.Application/GeneticService.cs b/src/Managing.Application/GeneticService.cs index d41fe329..d2ad31e3 100644 --- a/src/Managing.Application/GeneticService.cs +++ b/src/Managing.Application/GeneticService.cs @@ -84,6 +84,14 @@ public class GeneticService : IGeneticService ["signalPeriods"] = 9.0, ["smoothPeriods"] = 3.0 }, + [IndicatorType.StochasticCross] = new() + { + ["stochPeriods"] = 14.0, + ["signalPeriods"] = 3.0, + ["smoothPeriods"] = 3.0, + ["kFactor"] = 3.0, + ["dFactor"] = 2.0 + }, [IndicatorType.Stc] = new() { ["cyclePeriods"] = 10.0, @@ -159,6 +167,14 @@ public class GeneticService : IGeneticService ["signalPeriods"] = (3.0, 15.0), ["smoothPeriods"] = (1.0, 10.0) }, + [IndicatorType.StochasticCross] = new() + { + ["stochPeriods"] = (5.0, 30.0), + ["signalPeriods"] = (3.0, 15.0), + ["smoothPeriods"] = (1.0, 10.0), + ["kFactor"] = (0.1, 10.0), + ["dFactor"] = (0.1, 10.0) + }, [IndicatorType.Stc] = new() { ["cyclePeriods"] = (5.0, 50.0), @@ -188,6 +204,7 @@ public class GeneticService : IGeneticService [IndicatorType.SuperTrendCrossEma] = ["period", "multiplier"], [IndicatorType.ChandelierExit] = ["period", "multiplier"], [IndicatorType.StochRsiTrend] = ["period", "stochPeriods", "signalPeriods", "smoothPeriods"], + [IndicatorType.StochasticCross] = ["stochPeriods", "signalPeriods", "smoothPeriods", "kFactor", "dFactor"], [IndicatorType.Stc] = ["cyclePeriods", "fastPeriods", "slowPeriods"], [IndicatorType.LaggingStc] = ["cyclePeriods", "fastPeriods", "slowPeriods"] }; diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index 43810ebe..8ff9299f 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -60,6 +60,7 @@ public static class Enums EmaTrend, Composite, StochRsiTrend, + StochasticCross, Stc, StDev, LaggingStc, diff --git a/src/Managing.Domain/Indicators/IIndicator.cs b/src/Managing.Domain/Indicators/IIndicator.cs index 6a138fbd..731c5780 100644 --- a/src/Managing.Domain/Indicators/IIndicator.cs +++ b/src/Managing.Domain/Indicators/IIndicator.cs @@ -18,6 +18,8 @@ namespace Managing.Domain.Strategies int? StochPeriods { get; set; } int? SmoothPeriods { get; set; } int? CyclePeriods { get; set; } + double? KFactor { get; set; } + double? DFactor { get; set; } List Run(HashSet candles); diff --git a/src/Managing.Domain/Indicators/IndicatorBase.cs b/src/Managing.Domain/Indicators/IndicatorBase.cs index 851bad12..1e95c081 100644 --- a/src/Managing.Domain/Indicators/IndicatorBase.cs +++ b/src/Managing.Domain/Indicators/IndicatorBase.cs @@ -39,6 +39,10 @@ namespace Managing.Domain.Strategies public int? CyclePeriods { get; set; } + public double? KFactor { get; set; } + + public double? DFactor { get; set; } + public User User { get; set; } public virtual List Run(HashSet candles) diff --git a/src/Managing.Domain/Indicators/LightIndicator.cs b/src/Managing.Domain/Indicators/LightIndicator.cs index 639b6738..cd52e1ff 100644 --- a/src/Managing.Domain/Indicators/LightIndicator.cs +++ b/src/Managing.Domain/Indicators/LightIndicator.cs @@ -42,6 +42,10 @@ public class LightIndicator [Id(11)] public int? CyclePeriods { get; set; } + [Id(12)] public double? KFactor { get; set; } + + [Id(13)] public double? DFactor { get; set; } + /// /// Converts a LightIndicator back to a full Indicator /// @@ -64,7 +68,9 @@ public class LightIndicator Multiplier = Multiplier, SmoothPeriods = SmoothPeriods, StochPeriods = StochPeriods, - CyclePeriods = CyclePeriods + CyclePeriods = CyclePeriods, + KFactor = KFactor, + DFactor = DFactor }; return baseIndicator; diff --git a/src/Managing.Domain/Indicators/Signals/StochasticCrossIndicator.cs b/src/Managing.Domain/Indicators/Signals/StochasticCrossIndicator.cs new file mode 100644 index 00000000..cc0a6399 --- /dev/null +++ b/src/Managing.Domain/Indicators/Signals/StochasticCrossIndicator.cs @@ -0,0 +1,191 @@ +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(HashSet 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(HashSet 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, HashSet 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(HashSet 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; } + } +} diff --git a/src/Managing.Domain/Scenarios/ScenarioHelpers.cs b/src/Managing.Domain/Scenarios/ScenarioHelpers.cs index 47f811e5..2b9b2acc 100644 --- a/src/Managing.Domain/Scenarios/ScenarioHelpers.cs +++ b/src/Managing.Domain/Scenarios/ScenarioHelpers.cs @@ -91,6 +91,9 @@ public static class ScenarioHelpers IndicatorType.StochRsiTrend => new StochRsiTrendIndicatorBase(indicator.Name, indicator.Period.Value, indicator.StochPeriods.Value, indicator.SignalPeriods.Value, indicator.SmoothPeriods.Value), + IndicatorType.StochasticCross => new StochasticCrossIndicator(indicator.Name, + indicator.StochPeriods.Value, indicator.SignalPeriods.Value, indicator.SmoothPeriods.Value, + indicator.KFactor ?? 3.0, indicator.DFactor ?? 2.0), IndicatorType.Stc => new StcIndicatorBase(indicator.Name, indicator.CyclePeriods.Value, indicator.FastPeriods.Value, indicator.SlowPeriods.Value), IndicatorType.LaggingStc => new LaggingSTC(indicator.Name, indicator.CyclePeriods.Value, @@ -133,7 +136,9 @@ public static class ScenarioHelpers double? multiplier = null, int? stochPeriods = null, int? smoothPeriods = null, - int? cyclePeriods = null) + int? cyclePeriods = null, + double? kFactor = null, + double? dFactor = null) { IIndicator indicator = new IndicatorBase(name, type); @@ -214,6 +219,33 @@ public static class ScenarioHelpers indicator.SmoothPeriods = smoothPeriods; } + break; + case IndicatorType.StochasticCross: + if (!stochPeriods.HasValue || !signalPeriods.HasValue || !smoothPeriods.HasValue) + { + throw new Exception( + $"Missing stochPeriods, signalPeriods, smoothPeriods for {indicator.Type} strategy type"); + } + else + { + indicator.StochPeriods = stochPeriods; + indicator.SignalPeriods = signalPeriods; + indicator.SmoothPeriods = smoothPeriods; + // Set default values for optional parameters + indicator.KFactor = kFactor ?? 3.0; + indicator.DFactor = dFactor ?? 2.0; + + // Validate kFactor and dFactor are greater than 0 + if (indicator.KFactor <= 0) + { + throw new Exception($"kFactor must be greater than 0 for {indicator.Type} strategy type"); + } + if (indicator.DFactor <= 0) + { + throw new Exception($"dFactor must be greater than 0 for {indicator.Type} strategy type"); + } + } + break; case IndicatorType.Stc: case IndicatorType.LaggingStc: @@ -252,6 +284,7 @@ public static class ScenarioHelpers IndicatorType.EmaTrend => SignalType.Trend, IndicatorType.Composite => SignalType.Signal, IndicatorType.StochRsiTrend => SignalType.Trend, + IndicatorType.StochasticCross => SignalType.Signal, IndicatorType.Stc => SignalType.Signal, IndicatorType.StDev => SignalType.Context, IndicatorType.LaggingStc => SignalType.Signal,