295 lines
9.9 KiB
C#
295 lines
9.9 KiB
C#
using Managing.Core;
|
|
using Managing.Domain.Indicators;
|
|
using Managing.Domain.Shared.Rules;
|
|
using Managing.Domain.Strategies.Base;
|
|
using Skender.Stock.Indicators;
|
|
using static Managing.Common.Enums;
|
|
using Candle = Managing.Domain.Candles.Candle;
|
|
|
|
namespace Managing.Domain.Strategies.Signals;
|
|
|
|
public class RsiDivergenceIndicatorBase : IndicatorBase
|
|
{
|
|
public List<LightSignal> Signals { get; set; }
|
|
public TradeDirection Direction { get; set; }
|
|
private const int UpperBand = 70;
|
|
private const int LowerBand = 30;
|
|
|
|
public RsiDivergenceIndicatorBase(string name, int period) : base(name, IndicatorType.RsiDivergence)
|
|
{
|
|
Period = period;
|
|
Signals = new List<LightSignal>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get RSI signals
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public override List<LightSignal> Run(HashSet<Candle> candles)
|
|
{
|
|
if (!Period.HasValue || candles.Count <= Period)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
var rsiResult = candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList();
|
|
if (rsiResult.Count == 0)
|
|
return null;
|
|
|
|
ProcessRsiDivergenceSignals(rsiResult, candles);
|
|
|
|
return Signals;
|
|
}
|
|
catch (RuleException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs the indicator using pre-calculated RSI values for performance optimization.
|
|
/// </summary>
|
|
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
|
|
{
|
|
if (!Period.HasValue || candles.Count <= Period)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Use pre-calculated RSI values if available
|
|
List<RsiResult> rsiResult = null;
|
|
if (preCalculatedValues?.Rsi != null && preCalculatedValues.Rsi.Any())
|
|
{
|
|
// Filter pre-calculated RSI values to match the candles we're processing
|
|
var lastCandle = candles.Last();
|
|
rsiResult = preCalculatedValues.Rsi
|
|
.Where(r => r.Date <= lastCandle.Date)
|
|
.ToList();
|
|
}
|
|
|
|
// If no pre-calculated values or they don't match, fall back to regular calculation
|
|
if (rsiResult == null || !rsiResult.Any())
|
|
{
|
|
return Run(candles);
|
|
}
|
|
|
|
ProcessRsiDivergenceSignals(rsiResult, candles);
|
|
|
|
return Signals;
|
|
}
|
|
catch (RuleException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes RSI divergence signals based on price and RSI divergence patterns.
|
|
/// This method is shared between the regular Run() and optimized Run() methods.
|
|
/// </summary>
|
|
/// <param name="rsiResult">List of RSI calculation results</param>
|
|
/// <param name="candles">Candles to process</param>
|
|
private void ProcessRsiDivergenceSignals(List<RsiResult> rsiResult, HashSet<Candle> candles)
|
|
{
|
|
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
|
|
|
|
if (candlesRsi.Count(c => c.Rsi > 0) == 0)
|
|
return;
|
|
|
|
GetLongSignals(candlesRsi, candles);
|
|
GetShortSignals(candlesRsi, candles);
|
|
}
|
|
|
|
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
|
|
{
|
|
return new IndicatorsResultBase()
|
|
{
|
|
Rsi = candles.GetRsi(Period.Value).ToList()
|
|
};
|
|
}
|
|
|
|
private void GetLongSignals(List<CandleRsi> candlesRsi, HashSet<Candle> candles)
|
|
{
|
|
// Set the low and high for first candle
|
|
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0);
|
|
var highPrices = new List<CandleRsi>();
|
|
var lowPrices = new List<CandleRsi>();
|
|
|
|
var highRsi = new List<CandleRsi>();
|
|
var lowRsi = new List<CandleRsi>();
|
|
|
|
highPrices.Add(firstCandleRsi);
|
|
lowPrices.Add(firstCandleRsi);
|
|
|
|
highRsi.Add(firstCandleRsi);
|
|
lowRsi.Add(firstCandleRsi);
|
|
|
|
var previousCandle = firstCandleRsi;
|
|
|
|
// For a long
|
|
foreach (var currentCandle in candlesRsi.FindAll(r => r.Rsi > 0).Skip(1))
|
|
{
|
|
// If price go down
|
|
if (previousCandle.Close > currentCandle.Close)
|
|
{
|
|
// because the last price is upper than the current
|
|
highPrices.AddItem(previousCandle);
|
|
|
|
// Check if rsi is higher than the last lowest
|
|
if (currentCandle.Rsi > lowRsi.TakeLast(Period.Value).Min(r => r.Rsi))
|
|
{
|
|
// If new higher high, we set it
|
|
if (currentCandle.Rsi > highRsi.Last().Rsi)
|
|
highRsi.AddItem(currentCandle);
|
|
|
|
if (currentCandle.Rsi > lowRsi.Last().Rsi)
|
|
lowRsi.AddItem(currentCandle);
|
|
|
|
// Price go down but RSI go up
|
|
if (currentCandle.Close < lowPrices.TakeLast(Period.Value).Min(p => p.Close))
|
|
{
|
|
AddSignal(currentCandle, TradeDirection.Long, candles);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No divergence, price go down, rsi go down
|
|
lowRsi.AddItem(currentCandle);
|
|
}
|
|
|
|
lowPrices.AddItem(currentCandle);
|
|
}
|
|
else
|
|
{
|
|
// Price go up, so we have to update if price is a new higher high than previous candle
|
|
// Normally always true
|
|
if (previousCandle.Close < currentCandle.Close)
|
|
highPrices.AddItem(currentCandle); //15-15-12-14-17
|
|
|
|
// If rsi is lower low or not set
|
|
if (currentCandle.Rsi < lowRsi.Last().Rsi || lowRsi.Last().Rsi == 0)
|
|
lowRsi.AddItem(currentCandle);
|
|
|
|
// Price going up, so if its a new high we set it
|
|
if (currentCandle.Rsi > highRsi.Last().Rsi)
|
|
highRsi.AddItem(currentCandle);
|
|
}
|
|
|
|
previousCandle = currentCandle;
|
|
}
|
|
}
|
|
|
|
private void GetShortSignals(List<CandleRsi> candlesRsi, HashSet<Candle> candles)
|
|
{
|
|
// Set the low and high for first candle
|
|
var firstCandleRsi = candlesRsi.First(c => c.Rsi > 0);
|
|
|
|
var signals = new List<Signal>();
|
|
var highPrices = new List<CandleRsi>();
|
|
var lowPrices = new List<CandleRsi>();
|
|
|
|
var highRsi = new List<CandleRsi>();
|
|
var lowRsi = new List<CandleRsi>();
|
|
|
|
highPrices.Add(firstCandleRsi);
|
|
lowPrices.Add(firstCandleRsi);
|
|
|
|
highRsi.Add(firstCandleRsi);
|
|
lowRsi.Add(firstCandleRsi);
|
|
|
|
var previousCandle = firstCandleRsi;
|
|
|
|
// For a short
|
|
foreach (var currentCandle in candlesRsi.FindAll(r => r.Rsi > 0).Skip(1))
|
|
{
|
|
// If price go up
|
|
if (previousCandle.Close < currentCandle.Close)
|
|
{
|
|
// because the last price is lower than the current
|
|
lowPrices.AddItem(previousCandle);
|
|
|
|
// Check if rsi is lower than the last high
|
|
if (currentCandle.Rsi < highRsi.TakeLast(Period.Value).Max(r => r.Rsi))
|
|
{
|
|
// If new lower low, we set it
|
|
if (currentCandle.Rsi < lowRsi.Last().Rsi)
|
|
lowRsi.AddItem(currentCandle);
|
|
|
|
if (currentCandle.Rsi < highRsi.Last().Rsi)
|
|
highRsi.AddItem(currentCandle);
|
|
|
|
// Price go up but RSI go down
|
|
if (currentCandle.Close > highPrices.TakeLast(Period.Value).Max(p => p.Close))
|
|
{
|
|
AddSignal(currentCandle, TradeDirection.Short, candles);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No divergence, price go up, rsi go up
|
|
highRsi.AddItem(currentCandle);
|
|
}
|
|
|
|
highPrices.AddItem(currentCandle);
|
|
}
|
|
else
|
|
{
|
|
// Price go down, so we have to update if price is a new lower low than previous candle
|
|
if (previousCandle.Close > currentCandle.Close)
|
|
lowPrices.AddItem(currentCandle);
|
|
|
|
// If rsi is higher high or not set
|
|
if (currentCandle.Rsi > highRsi.Last().Rsi || highRsi.Last().Rsi == 0)
|
|
highRsi.AddItem(currentCandle);
|
|
|
|
// Price going down, so if its a new low we set it
|
|
if (currentCandle.Rsi < lowRsi.Last().Rsi)
|
|
lowRsi.AddItem(currentCandle);
|
|
}
|
|
|
|
previousCandle = currentCandle;
|
|
}
|
|
}
|
|
|
|
private void AddSignal(CandleRsi candleSignal, TradeDirection direction, HashSet<Candle> candles)
|
|
{
|
|
var signal = new LightSignal(candleSignal.Ticker, direction, Confidence.Low,
|
|
candleSignal, candleSignal.Date, candleSignal.Exchange, Type, SignalType, Name);
|
|
|
|
if (Signals.Count(s => s.Identifier == signal.Identifier) < 1)
|
|
{
|
|
var lastCandleOnPeriod = candles.TakeLast(Period.Value).ToList();
|
|
var signalsOnPeriod = Signals.Where(s => s.Date >= lastCandleOnPeriod[0].Date).ToList();
|
|
|
|
if (signalsOnPeriod.Count == 1)
|
|
signal.SetConfidence(Confidence.Medium);
|
|
|
|
if (signalsOnPeriod.Count >= 2)
|
|
signal.SetConfidence(Confidence.High);
|
|
|
|
Signals.AddItem(signal);
|
|
}
|
|
}
|
|
|
|
private List<CandleRsi> MapRsiToCandle(IReadOnlyCollection<RsiResult> rsiResult,
|
|
IEnumerable<Candle> candles)
|
|
{
|
|
return candles.Select(c => new CandleRsi()
|
|
{
|
|
Close = c.Close,
|
|
Rsi = rsiResult.Find(c.Date).Rsi.GetValueOrDefault(),
|
|
Date = c.Date,
|
|
Ticker = c.Ticker,
|
|
Exchange = c.Exchange
|
|
}).ToList();
|
|
}
|
|
|
|
private class CandleRsi : Candle
|
|
{
|
|
public double Rsi { get; set; }
|
|
}
|
|
} |