- 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.
292 lines
10 KiB
C#
292 lines
10 KiB
C#
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; }
|
|
}
|
|
}
|