Add IchimokuKumoTrend indicator support across application

- Introduced IchimokuKumoTrend indicator in GeneticService with configuration settings for tenkanPeriods, kijunPeriods, senkouBPeriods, offsetPeriods, senkouOffset, and chikouOffset.
- Updated ScenarioHelpers to handle creation and validation of the new indicator type.
- Enhanced CustomScenario, backtest, and scenario pages to include IchimokuKumoTrend in indicator lists and parameter mappings.
- Modified API and types to reflect the addition of the new indicator in relevant enums and mappings.
This commit is contained in:
2025-11-24 19:43:18 +07:00
parent 478dca51e7
commit 4268626897
16 changed files with 686 additions and 11 deletions

View File

@@ -0,0 +1,291 @@
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<LightSignal> 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<LightSignal>();
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<LightSignal> Run(HashSet<Candle> 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<LightSignal> Run(HashSet<Candle> 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<IchimokuResult> 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<CandleIchimoku> CalculateIchimoku(List<Candle> candles)
{
// Use Skender.Stock.Indicators GetIchimoku method with all supported parameters
IEnumerable<IchimokuResult> 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<CandleIchimoku>();
// 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<CandleIchimoku> ichimokuResults, HashSet<Candle> 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<IchimokuResult> ichimokuResults, HashSet<Candle> 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<Candle> candles)
{
IEnumerable<IchimokuResult> 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; }
}
}