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.Trends; public class IchimokuKumoTrend : IndicatorBase { public List Signals { get; set; } public IchimokuKumoTrend( string name, int tenkanPeriods = 9, int kijunPeriods = 26, int senkouBPeriods = 52, int offsetPeriods = 26, int? senkouOffset = null, int? chikouOffset = null) : base(name, IndicatorType.IchimokuKumoTrend) { Signals = new List(); TenkanPeriods = tenkanPeriods; // Tenkan-sen periods KijunPeriods = kijunPeriods; // Kijun-sen periods SenkouBPeriods = senkouBPeriods; // Senkou Span B periods OffsetPeriods = offsetPeriods; // Default offset periods SenkouOffset = senkouOffset; // Separate offset for Senkou span ChikouOffset = chikouOffset; // Separate offset for Chikou span } public override List Run(HashSet candles) { // Need at least the greater of tenkanPeriods, kijunPeriods, senkouBPeriods, and all offset periods var maxOffset = Math.Max(Math.Max(OffsetPeriods.Value, SenkouOffset ?? OffsetPeriods.Value), ChikouOffset ?? OffsetPeriods.Value); var minRequired = Math.Max(Math.Max(Math.Max(TenkanPeriods.Value, KijunPeriods.Value), SenkouBPeriods.Value), maxOffset); if (candles.Count <= minRequired) { return null; } try { var ichimokuResults = CalculateIchimoku(candles.ToList()); if (ichimokuResults.Count == 0) return null; ProcessKumoTrendSignals(ichimokuResults, candles); return Signals.ToList(); } catch (RuleException) { return null; } } public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) { // Need at least the greater of tenkanPeriods, kijunPeriods, senkouBPeriods, and all offset periods var maxOffset = Math.Max(Math.Max(OffsetPeriods.Value, SenkouOffset ?? OffsetPeriods.Value), ChikouOffset ?? OffsetPeriods.Value); var minRequired = Math.Max(Math.Max(Math.Max(TenkanPeriods.Value, KijunPeriods.Value), SenkouBPeriods.Value), maxOffset); if (candles.Count <= minRequired) { return null; } try { List ichimokuResults = null; // Use pre-calculated Ichimoku values if available if (preCalculatedValues?.Ichimoku != null && preCalculatedValues.Ichimoku.Any()) { // Filter pre-calculated Ichimoku values to match the candles we're processing ichimokuResults = preCalculatedValues.Ichimoku .Where(i => i.SenkouSpanA.HasValue && i.SenkouSpanB.HasValue && candles.Any(c => c.Date == i.Date)) .ToList(); } // If no pre-calculated values or they don't match, fall back to regular calculation if (ichimokuResults == null || !ichimokuResults.Any()) { return Run(candles); } ProcessKumoTrendSignalsFromResults(ichimokuResults, candles); return Signals.ToList(); } catch (RuleException) { return null; } } private List CalculateIchimoku(List candles) { // Use Skender.Stock.Indicators GetIchimoku method with all supported parameters IEnumerable ichimokuResults; // Use the appropriate overload based on which parameters are specified if (SenkouOffset.HasValue && ChikouOffset.HasValue) { // Use separate offsets for Senkou and Chikou spans ichimokuResults = candles.GetIchimoku( tenkanPeriods: TenkanPeriods.Value, kijunPeriods: KijunPeriods.Value, senkouBPeriods: SenkouBPeriods.Value, senkouOffset: SenkouOffset.Value, chikouOffset: ChikouOffset.Value ); } else { // Use default offsetPeriods for both Senkou and Chikou spans ichimokuResults = candles.GetIchimoku( tenkanPeriods: TenkanPeriods.Value, kijunPeriods: KijunPeriods.Value, senkouBPeriods: SenkouBPeriods.Value, offsetPeriods: OffsetPeriods.Value ); } var ichimokuList = ichimokuResults.ToList(); var candleIchimokuResults = new List(); // Map IchimokuResult to CandleIchimoku foreach (var ichimoku in ichimokuList) { // Find the corresponding candle var candle = candles.FirstOrDefault(c => c.Date == ichimoku.Date); if (candle == null) continue; candleIchimokuResults.Add(new CandleIchimoku() { Close = candle.Close, Open = candle.Open, Date = candle.Date, Ticker = candle.Ticker, Exchange = candle.Exchange, TenkanSen = ichimoku.TenkanSen ?? 0, KijunSen = ichimoku.KijunSen ?? 0, SenkouSpanA = ichimoku.SenkouSpanA ?? 0, SenkouSpanB = ichimoku.SenkouSpanB ?? 0 }); } return candleIchimokuResults; } private void ProcessKumoTrendSignals(List ichimokuResults, HashSet candles) { var mappedData = ichimokuResults; if (mappedData.Count == 0) return; var previousCandle = mappedData[0]; foreach (var currentCandle in mappedData.Skip(1)) { // For trend assessment, check if price is above or below the cloud // The cloud is formed by Senkou Span A and Senkou Span B var cloudTop = Math.Max(currentCandle.SenkouSpanA, currentCandle.SenkouSpanB); var cloudBottom = Math.Min(currentCandle.SenkouSpanA, currentCandle.SenkouSpanB); if (currentCandle.Close > cloudTop) { AddSignal(currentCandle, TradeDirection.Long, Confidence.None); } else if (currentCandle.Close < cloudBottom) { AddSignal(currentCandle, TradeDirection.Short, Confidence.None); } // If price is within the cloud, no signal (neutral) previousCandle = currentCandle; } } private void ProcessKumoTrendSignalsFromResults(List ichimokuResults, HashSet candles) { if (ichimokuResults.Count == 0) return; var previousResult = ichimokuResults[0]; foreach (var currentResult in ichimokuResults.Skip(1)) { // Find the corresponding candle var candle = candles.FirstOrDefault(c => c.Date == currentResult.Date); if (candle == null || !currentResult.SenkouSpanA.HasValue || !currentResult.SenkouSpanB.HasValue) continue; // For trend assessment, check if price is above or below the cloud // The cloud is formed by Senkou Span A and Senkou Span B var cloudTop = Math.Max(currentResult.SenkouSpanA.Value, currentResult.SenkouSpanB.Value); var cloudBottom = Math.Min(currentResult.SenkouSpanA.Value, currentResult.SenkouSpanB.Value); if (candle.Close > cloudTop) { AddSignal(candle, TradeDirection.Long, Confidence.None); } else if (candle.Close < cloudBottom) { AddSignal(candle, TradeDirection.Short, Confidence.None); } // If price is within the cloud, no signal (neutral) previousResult = currentResult; } } public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { IEnumerable ichimokuResults; // Use the appropriate overload based on which parameters are specified if (SenkouOffset.HasValue && ChikouOffset.HasValue) { // Use separate offsets for Senkou and Chikou spans ichimokuResults = candles.GetIchimoku( tenkanPeriods: TenkanPeriods.Value, kijunPeriods: KijunPeriods.Value, senkouBPeriods: SenkouBPeriods.Value, senkouOffset: SenkouOffset.Value, chikouOffset: ChikouOffset.Value ); } else { // Use default offsetPeriods for both Senkou and Chikou spans ichimokuResults = candles.GetIchimoku( tenkanPeriods: TenkanPeriods.Value, kijunPeriods: KijunPeriods.Value, senkouBPeriods: SenkouBPeriods.Value, offsetPeriods: OffsetPeriods.Value ); } return new IndicatorsResultBase() { Ichimoku = ichimokuResults.ToList() }; } private void AddSignal(CandleIchimoku 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 void AddSignal(Candle 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 CandleIchimoku : Candle { public decimal TenkanSen { get; set; } public decimal KijunSen { get; set; } public decimal SenkouSpanA { get; set; } public decimal SenkouSpanB { get; set; } } }