Improve perf for backtests

This commit is contained in:
2025-11-10 02:15:43 +07:00
parent 7e52b7a734
commit 51a227e27e
24 changed files with 1327 additions and 443 deletions

View File

@@ -30,8 +30,7 @@ public class ChandelierExitIndicatorBase : IndicatorBase
try
{
GetSignals(ChandelierType.Long, candles);
GetSignals(ChandelierType.Short, candles);
ProcessChandelierSignals(candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
@@ -41,6 +40,77 @@ public class ChandelierExitIndicatorBase : IndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated Chandelier values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= MinimumHistory)
{
return null;
}
try
{
// Use pre-calculated Chandelier values if available
List<ChandelierResult> chandelierLong = null;
List<ChandelierResult> chandelierShort = null;
if (preCalculatedValues?.ChandelierLong != null && preCalculatedValues.ChandelierLong.Any() &&
preCalculatedValues?.ChandelierShort != null && preCalculatedValues.ChandelierShort.Any())
{
// Filter pre-calculated Chandelier values to match the candles we're processing
chandelierLong = preCalculatedValues.ChandelierLong
.Where(c => c.ChandelierExit.HasValue && candles.Any(candle => candle.Date == c.Date))
.OrderBy(c => c.Date)
.ToList();
chandelierShort = preCalculatedValues.ChandelierShort
.Where(c => c.ChandelierExit.HasValue && candles.Any(candle => candle.Date == c.Date))
.OrderBy(c => c.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (chandelierLong == null || !chandelierLong.Any() || chandelierShort == null || !chandelierShort.Any())
{
return Run(candles);
}
ProcessChandelierSignalsWithPreCalculated(chandelierLong, chandelierShort, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes Chandelier signals for both Long and Short types.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="candles">Candles to process</param>
private void ProcessChandelierSignals(HashSet<Candle> candles)
{
GetSignals(ChandelierType.Long, candles);
GetSignals(ChandelierType.Short, candles);
}
/// <summary>
/// Processes Chandelier signals using pre-calculated values.
/// </summary>
/// <param name="chandelierLong">Pre-calculated Long Chandelier values</param>
/// <param name="chandelierShort">Pre-calculated Short Chandelier values</param>
/// <param name="candles">Candles to process</param>
private void ProcessChandelierSignalsWithPreCalculated(
List<ChandelierResult> chandelierLong,
List<ChandelierResult> chandelierShort,
HashSet<Candle> candles)
{
GetSignalsWithPreCalculated(ChandelierType.Long, chandelierLong, candles);
GetSignalsWithPreCalculated(ChandelierType.Short, chandelierShort, candles);
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
{
return new IndicatorsResultBase()
@@ -54,9 +124,28 @@ public class ChandelierExitIndicatorBase : IndicatorBase
{
var chandelier = candles.GetChandelier(Period.Value, Multiplier.Value, chandelierType)
.Where(s => s.ChandelierExit.HasValue).ToList();
var chandelierCandle = MapChandelierToCandle(chandelier, candles.TakeLast(MinimumHistory));
var previousCandle = chandelierCandle[0];
ProcessChandelierSignalsForType(chandelier, chandelierType, candles);
}
private void GetSignalsWithPreCalculated(ChandelierType chandelierType, List<ChandelierResult> chandelier, HashSet<Candle> candles)
{
ProcessChandelierSignalsForType(chandelier, chandelierType, candles);
}
/// <summary>
/// Processes Chandelier signals for a specific type (Long or Short).
/// This method is shared between regular and optimized signal processing.
/// </summary>
/// <param name="chandelier">Chandelier calculation results</param>
/// <param name="chandelierType">Type of Chandelier (Long or Short)</param>
/// <param name="candles">Candles to process</param>
private void ProcessChandelierSignalsForType(List<ChandelierResult> chandelier, ChandelierType chandelierType, HashSet<Candle> candles)
{
var chandelierCandle = MapChandelierToCandle(chandelier, candles.TakeLast(MinimumHistory));
if (chandelierCandle.Count == 0)
return;
var previousCandle = chandelierCandle[0];
foreach (var currentCandle in chandelierCandle.Skip(1))
{
// Short

View File

@@ -42,30 +42,10 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
var fastEma = candles.GetEma(FastPeriods.Value).ToList();
var slowEma = candles.GetEma(SlowPeriods.Value).ToList();
var dualEmaCandles = MapDualEmaToCandle(fastEma, slowEma, candles.TakeLast(MinimumHistory));
if (dualEmaCandles.Count < 2)
if (fastEma.Count == 0 || slowEma.Count == 0)
return null;
var previousCandle = dualEmaCandles[0];
foreach (var currentCandle in dualEmaCandles.Skip(1))
{
// Short signal: Fast EMA crosses below Slow EMA
if (previousCandle.FastEma > previousCandle.SlowEma &&
currentCandle.FastEma < currentCandle.SlowEma)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
// Long signal: Fast EMA crosses above Slow EMA
if (previousCandle.FastEma < previousCandle.SlowEma &&
currentCandle.FastEma > currentCandle.SlowEma)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
previousCandle = currentCandle;
}
ProcessDualEmaCrossSignals(fastEma, slowEma, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
@@ -75,6 +55,86 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated EMA values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= MinimumHistory)
{
return null;
}
try
{
// Use pre-calculated EMA values if available
List<EmaResult> fastEma = null;
List<EmaResult> slowEma = null;
if (preCalculatedValues?.FastEma != null && preCalculatedValues.FastEma.Any() &&
preCalculatedValues?.SlowEma != null && preCalculatedValues.SlowEma.Any())
{
// Filter pre-calculated EMA values to match the candles we're processing
fastEma = preCalculatedValues.FastEma
.Where(e => candles.Any(c => c.Date == e.Date))
.OrderBy(e => e.Date)
.ToList();
slowEma = preCalculatedValues.SlowEma
.Where(e => candles.Any(c => c.Date == e.Date))
.OrderBy(e => e.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (fastEma == null || !fastEma.Any() || slowEma == null || !slowEma.Any())
{
return Run(candles);
}
ProcessDualEmaCrossSignals(fastEma, slowEma, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes dual EMA cross signals based on Fast EMA crossing Slow EMA.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="fastEma">List of Fast EMA calculation results</param>
/// <param name="slowEma">List of Slow EMA calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessDualEmaCrossSignals(List<EmaResult> fastEma, List<EmaResult> slowEma, HashSet<Candle> candles)
{
var dualEmaCandles = MapDualEmaToCandle(fastEma, slowEma, candles.TakeLast(MinimumHistory));
if (dualEmaCandles.Count < 2)
return;
var previousCandle = dualEmaCandles[0];
foreach (var currentCandle in dualEmaCandles.Skip(1))
{
// Short signal: Fast EMA crosses below Slow EMA
if (previousCandle.FastEma > previousCandle.SlowEma &&
currentCandle.FastEma < currentCandle.SlowEma)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
// Long signal: Fast EMA crosses above Slow EMA
if (previousCandle.FastEma < previousCandle.SlowEma &&
currentCandle.FastEma > currentCandle.SlowEma)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
previousCandle = currentCandle;
}
}
private List<CandleDualEma> MapDualEmaToCandle(List<EmaResult> fastEma, List<EmaResult> slowEma,
IEnumerable<Candle> candles)
{

View File

@@ -36,28 +36,10 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
try
{
var ema = candles.GetEma(Period.Value).ToList();
var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value));
if (ema.Count == 0)
return null;
var previousCandle = emaCandles[0];
foreach (var currentCandle in emaCandles.Skip(1))
{
if (previousCandle.Close > (decimal)currentCandle.Ema &&
currentCandle.Close < (decimal)currentCandle.Ema)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
if (previousCandle.Close < (decimal)currentCandle.Ema &&
currentCandle.Close > (decimal)currentCandle.Ema)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
previousCandle = currentCandle;
}
ProcessEmaCrossSignals(ema, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
@@ -67,6 +49,77 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated EMA values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= Period)
{
return null;
}
try
{
// Use pre-calculated EMA values if available
List<EmaResult> ema = null;
if (preCalculatedValues?.Ema != null && preCalculatedValues.Ema.Any())
{
// Filter pre-calculated EMA values to match the candles we're processing
ema = preCalculatedValues.Ema
.Where(e => candles.Any(c => c.Date == e.Date))
.OrderBy(e => e.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (ema == null || !ema.Any())
{
return Run(candles);
}
ProcessEmaCrossSignals(ema, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes EMA cross signals based on price crossing the EMA line.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="ema">List of EMA calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessEmaCrossSignals(List<EmaResult> ema, HashSet<Candle> candles)
{
var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value));
if (emaCandles.Count == 0)
return;
var previousCandle = emaCandles[0];
foreach (var currentCandle in emaCandles.Skip(1))
{
if (previousCandle.Close > (decimal)currentCandle.Ema &&
currentCandle.Close < (decimal)currentCandle.Ema)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
if (previousCandle.Close < (decimal)currentCandle.Ema &&
currentCandle.Close > (decimal)currentCandle.Ema)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
previousCandle = currentCandle;
}
}
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
{
var signal = new LightSignal(candleSignal.Ticker, direction, confidence,

View File

@@ -36,28 +36,10 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
try
{
var ema = candles.GetEma(Period.Value).ToList();
var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value).ToHashSet());
if (ema.Count == 0)
return null;
var previousCandle = emaCandles[0];
foreach (var currentCandle in emaCandles.Skip(1))
{
if (previousCandle.Close > (decimal)currentCandle.Ema &&
currentCandle.Close < (decimal)currentCandle.Ema)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
if (previousCandle.Close < (decimal)currentCandle.Ema &&
currentCandle.Close > (decimal)currentCandle.Ema)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
previousCandle = currentCandle;
}
ProcessEmaCrossSignals(ema, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
@@ -67,6 +49,77 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated EMA values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= Period)
{
return null;
}
try
{
// Use pre-calculated EMA values if available
List<EmaResult> ema = null;
if (preCalculatedValues?.Ema != null && preCalculatedValues.Ema.Any())
{
// Filter pre-calculated EMA values to match the candles we're processing
ema = preCalculatedValues.Ema
.Where(e => candles.Any(c => c.Date == e.Date))
.OrderBy(e => e.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (ema == null || !ema.Any())
{
return Run(candles);
}
ProcessEmaCrossSignals(ema, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes EMA cross signals based on price crossing the EMA line.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="ema">List of EMA calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessEmaCrossSignals(List<EmaResult> ema, HashSet<Candle> candles)
{
var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value).ToHashSet());
if (emaCandles.Count == 0)
return;
var previousCandle = emaCandles[0];
foreach (var currentCandle in emaCandles.Skip(1))
{
if (previousCandle.Close > (decimal)currentCandle.Ema &&
currentCandle.Close < (decimal)currentCandle.Ema)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
if (previousCandle.Close < (decimal)currentCandle.Ema &&
currentCandle.Close > (decimal)currentCandle.Ema)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
previousCandle = currentCandle;
}
}
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)
{
var signal = new LightSignal(candleSignal.Ticker, direction, confidence,

View File

@@ -38,49 +38,10 @@ public class LaggingSTC : IndicatorBase
try
{
var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
var stcCandles = MapStcToCandle(stc, candles.TakeLast(CyclePeriods.Value * 3));
if (stcCandles.Count == 0)
if (stc.Count == 0)
return null;
for (int i = 1; i < stcCandles.Count; i++)
{
var currentCandle = stcCandles[i];
var previousCandle = stcCandles[i - 1];
/* VOLATILITY CONFIRMATION WINDOW
* - 22-period rolling window (≈1 trading month)
* - Ends at previous candle to avoid inclusion of current break
* - Dynamic sizing for early dataset cases */
// Calculate the lookback window ending at previousCandle (excludes currentCandle)
int windowSize = 40;
int windowStart = Math.Max(0, i - windowSize); // Ensure no negative indices
var lookbackWindow = stcCandles
.Skip(windowStart)
.Take(i - windowStart) // Take up to previousCandle (i-1)
.ToList();
double? minStc = lookbackWindow.Min(c => c.Stc);
double? maxStc = lookbackWindow.Max(c => c.Stc);
// Short Signal: Break below 75 with prior min >78
if (previousCandle.Stc > 75 && currentCandle.Stc <= 75)
{
if (minStc > 78)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
}
// Long Signal: Break above 25 with prior max <11
if (previousCandle.Stc < 25 && currentCandle.Stc >= 25)
{
if (maxStc < 11)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
}
}
ProcessLaggingStcSignals(stc, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
@@ -90,6 +51,98 @@ public class LaggingSTC : IndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated STC values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
{
return null;
}
try
{
// Use pre-calculated STC values if available
List<StcResult> stc = null;
if (preCalculatedValues?.Stc != null && preCalculatedValues.Stc.Any())
{
// Filter pre-calculated STC values to match the candles we're processing
stc = preCalculatedValues.Stc
.Where(s => candles.Any(c => c.Date == s.Date))
.OrderBy(s => s.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (stc == null || !stc.Any())
{
return Run(candles);
}
ProcessLaggingStcSignals(stc, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes Lagging STC signals based on STC values crossing thresholds with volatility confirmation.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="stc">List of STC calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessLaggingStcSignals(List<StcResult> stc, HashSet<Candle> candles)
{
var stcCandles = MapStcToCandle(stc, candles.TakeLast(CyclePeriods.Value * 3));
if (stcCandles.Count == 0)
return;
for (int i = 1; i < stcCandles.Count; i++)
{
var currentCandle = stcCandles[i];
var previousCandle = stcCandles[i - 1];
/* VOLATILITY CONFIRMATION WINDOW
* - 22-period rolling window (≈1 trading month)
* - Ends at previous candle to avoid inclusion of current break
* - Dynamic sizing for early dataset cases */
// Calculate the lookback window ending at previousCandle (excludes currentCandle)
int windowSize = 40;
int windowStart = Math.Max(0, i - windowSize); // Ensure no negative indices
var lookbackWindow = stcCandles
.Skip(windowStart)
.Take(i - windowStart) // Take up to previousCandle (i-1)
.ToList();
double? minStc = lookbackWindow.Min(c => c.Stc);
double? maxStc = lookbackWindow.Max(c => c.Stc);
// Short Signal: Break below 75 with prior min >78
if (previousCandle.Stc > 75 && currentCandle.Stc <= 75)
{
if (minStc > 78)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
}
// Long Signal: Break above 25 with prior max <11
if (previousCandle.Stc < 25 && currentCandle.Stc >= 25)
{
if (maxStc < 11)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
}
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
{
var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();

View File

@@ -31,34 +31,10 @@ public class MacdCrossIndicatorBase : IndicatorBase
try
{
var macd = candles.GetMacd(FastPeriods.Value, SlowPeriods.Value, SignalPeriods.Value).ToList();
var macdCandle = MapMacdToCandle(macd, candles.TakeLast(SignalPeriods.Value));
if (macd.Count == 0)
return null;
var previousCandle = macdCandle[0];
foreach (var currentCandle in macdCandle.Skip(1))
{
// // Only trigger signals when Signal line is outside -100 to 100 range (extreme conditions)
// if (currentCandle.Signal < -200 || currentCandle.Signal > 200)
// {
//
// }
// Check for MACD line crossing below Signal line (bearish cross)
if (previousCandle.Macd > previousCandle.Signal && currentCandle.Macd < currentCandle.Signal)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
// Check for MACD line crossing above Signal line (bullish cross)
if (previousCandle.Macd < previousCandle.Signal && currentCandle.Macd > currentCandle.Signal)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
previousCandle = currentCandle;
}
ProcessMacdSignals(macd, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
@@ -68,6 +44,77 @@ public class MacdCrossIndicatorBase : IndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated MACD values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= 2 * (SlowPeriods + SignalPeriods))
{
return null;
}
try
{
// Use pre-calculated MACD values if available
List<MacdResult> macd = null;
if (preCalculatedValues?.Macd != null && preCalculatedValues.Macd.Any())
{
// Filter pre-calculated MACD values to match the candles we're processing
macd = preCalculatedValues.Macd
.Where(m => candles.Any(c => c.Date == m.Date))
.OrderBy(m => m.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (macd == null || !macd.Any())
{
return Run(candles);
}
ProcessMacdSignals(macd, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes MACD signals based on MACD line crossing the Signal line.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="macd">List of MACD calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessMacdSignals(List<MacdResult> macd, HashSet<Candle> candles)
{
var macdCandle = MapMacdToCandle(macd, candles.TakeLast(SignalPeriods.Value));
if (macdCandle.Count == 0)
return;
var previousCandle = macdCandle[0];
foreach (var currentCandle in macdCandle.Skip(1))
{
// Check for MACD line crossing below Signal line (bearish cross)
if (previousCandle.Macd > previousCandle.Signal && currentCandle.Macd < currentCandle.Signal)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
// Check for MACD line crossing above Signal line (bullish cross)
if (previousCandle.Macd < previousCandle.Signal && currentCandle.Macd > currentCandle.Signal)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
previousCandle = currentCandle;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
{
return new IndicatorsResultBase()

View File

@@ -29,18 +29,13 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
return null;
}
var ticker = candles.First().Ticker;
try
{
var rsiResult = candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList();
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
if (candlesRsi.Count(c => c.Rsi > 0) == 0)
if (rsiResult.Count == 0)
return null;
GetLongSignals(candlesRsi, candles);
GetShortSignals(candlesRsi, candles);
ProcessRsiDivergenceConfirmSignals(rsiResult, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
@@ -50,6 +45,63 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated RSI values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (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 relevantCandles = candles.TakeLast(10 * Period.Value);
rsiResult = preCalculatedValues.Rsi
.Where(r => relevantCandles.Any(c => c.Date == r.Date))
.OrderBy(r => r.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);
}
ProcessRsiDivergenceConfirmSignals(rsiResult, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes RSI divergence confirmation 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 ProcessRsiDivergenceConfirmSignals(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()

View File

@@ -32,18 +32,13 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
return null;
}
var ticker = candles.First().Ticker;
try
{
var rsiResult = candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList();
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
if (candlesRsi.Count(c => c.Rsi > 0) == 0)
if (rsiResult.Count == 0)
return null;
GetLongSignals(candlesRsi, candles);
GetShortSignals(candlesRsi, candles);
ProcessRsiDivergenceSignals(rsiResult, candles);
return Signals;
}
@@ -53,6 +48,63 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
}
}
/// <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 relevantCandles = candles.TakeLast(10 * Period.Value);
rsiResult = preCalculatedValues.Rsi
.Where(r => relevantCandles.Any(c => c.Date == r.Date))
.OrderBy(r => r.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()

View File

@@ -33,29 +33,10 @@ public class StcIndicatorBase : IndicatorBase
if (FastPeriods != null)
{
var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
if (CyclePeriods != null)
{
var stcCandles = MapStcToCandle(stc, candles.TakeLast(CyclePeriods.Value));
if (stc.Count == 0)
return null;
if (stc.Count == 0)
return null;
var previousCandle = stcCandles[0];
foreach (var currentCandle in stcCandles.Skip(1))
{
if (previousCandle.Stc > 75 && currentCandle.Stc <= 75)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
if (previousCandle.Stc < 25 && currentCandle.Stc >= 25)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
previousCandle = currentCandle;
}
}
ProcessStcSignals(stc, candles);
}
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
@@ -66,6 +47,45 @@ public class StcIndicatorBase : IndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated STC values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= 2 * (SlowPeriods + CyclePeriods))
{
return null;
}
try
{
// Use pre-calculated STC values if available
List<StcResult> stc = null;
if (preCalculatedValues?.Stc != null && preCalculatedValues.Stc.Any())
{
// Filter pre-calculated STC values to match the candles we're processing
stc = preCalculatedValues.Stc
.Where(s => candles.Any(c => c.Date == s.Date))
.OrderBy(s => s.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (stc == null || !stc.Any())
{
return Run(candles);
}
ProcessStcSignals(stc, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
{
if (FastPeriods != null && SlowPeriods != null)
@@ -80,6 +100,39 @@ public class StcIndicatorBase : IndicatorBase
return null;
}
/// <summary>
/// Processes STC signals based on STC values crossing thresholds (25 and 75).
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="stc">List of STC calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessStcSignals(List<StcResult> stc, HashSet<Candle> candles)
{
if (CyclePeriods == null)
return;
var stcCandles = MapStcToCandle(stc, candles.TakeLast(CyclePeriods.Value));
if (stcCandles.Count == 0)
return;
var previousCandle = stcCandles[0];
foreach (var currentCandle in stcCandles.Skip(1))
{
if (previousCandle.Stc > 75 && currentCandle.Stc <= 75)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
if (previousCandle.Stc < 25 && currentCandle.Stc >= 25)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
previousCandle = currentCandle;
}
}
private List<CandleSct> MapStcToCandle(List<StcResult> stc, IEnumerable<Candle> candles)
{
var sctList = new List<CandleSct>();

View File

@@ -48,80 +48,7 @@ public class SuperTrendCrossEma : IndicatorBase
.Where(a => a.Adx.HasValue && a.Pdi.HasValue && a.Mdi.HasValue) // Ensure all values exist
.ToList();
// 2. Create merged dataset with price + indicators
var superTrendCandles = MapSuperTrendToCandle(superTrend, candles.TakeLast(minimumRequiredHistory));
if (superTrendCandles.Count == 0)
return null;
// 3. Add EMA50 and ADX values to the CandleSuperTrend objects
foreach (var candle in superTrendCandles)
{
var emaValue = ema50.Find(e => e.Date == candle.Date)?.Ema;
var adxValue = adxResults.Find(a => a.Date == candle.Date);
if (emaValue.HasValue)
candle.Ema50 = emaValue.Value;
if (adxValue != null)
{
candle.Adx = (decimal)adxValue.Adx.Value;
candle.Pdi = (decimal)adxValue.Pdi.Value;
candle.Mdi = (decimal)adxValue.Mdi.Value;
}
}
// 4. Signal detection logic with ADX filter
for (int i = 1; i < superTrendCandles.Count; i++)
{
var current = superTrendCandles[i];
var previous = superTrendCandles[i - 1];
// Convert SuperTrend to double for comparison
double currentSuperTrend = (double)current.SuperTrend;
double previousSuperTrend = (double)previous.SuperTrend;
// Ensure ADX data exists
if (current.Adx < adxThreshold) // Only trade when ADX confirms trend strength
continue;
/* LONG SIGNAL CONDITIONS:
* 1. SuperTrend crosses above EMA50
* 2. Price > SuperTrend and > EMA50
* 3. Previous state shows SuperTrend < EMA50
* 4. ADX > threshold and +DI > -DI (bullish momentum)
*/
bool longCross = currentSuperTrend > current.Ema50 &&
previousSuperTrend < previous.Ema50;
bool longPricePosition = current.Close > (decimal)currentSuperTrend &&
current.Close > (decimal)current.Ema50;
bool adxBullish = current.Pdi > current.Mdi; // Bullish momentum confirmation
if (longCross && longPricePosition && adxBullish)
{
AddSignal(current, TradeDirection.Long, Confidence.Medium);
}
/* SHORT SIGNAL CONDITIONS:
* 1. SuperTrend crosses below EMA50
* 2. Price < SuperTrend and < EMA50
* 3. Previous state shows SuperTrend > EMA50
* 4. ADX > threshold and -DI > +DI (bearish momentum)
*/
bool shortCross = currentSuperTrend < current.Ema50 &&
previousSuperTrend > previous.Ema50;
bool shortPricePosition = current.Close < (decimal)currentSuperTrend &&
current.Close < (decimal)current.Ema50;
bool adxBearish = current.Mdi > current.Pdi; // Bearish momentum confirmation
if (shortCross && shortPricePosition && adxBearish)
{
AddSignal(current, TradeDirection.Short, Confidence.Medium);
}
}
ProcessSuperTrendCrossEmaSignals(superTrend, ema50, adxResults, candles, minimumRequiredHistory, adxThreshold);
return Signals.Where(s => s.Confidence != Confidence.None)
.OrderBy(s => s.Date)
@@ -158,6 +85,159 @@ public class SuperTrendCrossEma : IndicatorBase
return superTrends;
}
/// <summary>
/// Runs the indicator using pre-calculated SuperTrend values for performance optimization.
/// Note: EMA50 and ADX are still calculated on-the-fly as they're not part of the standard indicator values.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
// Validate sufficient historical data for all indicators
const int emaPeriod = 50;
const int adxPeriod = 14; // Standard ADX period
const int adxThreshold = 25; // Minimum ADX level to confirm a trend
int minimumRequiredHistory = Math.Max(Math.Max(emaPeriod, adxPeriod), Period.Value * 2); // Ensure enough data
if (candles.Count < minimumRequiredHistory)
{
return null;
}
try
{
// Use pre-calculated SuperTrend values if available
List<SuperTrendResult> superTrend = null;
if (preCalculatedValues?.SuperTrend != null && preCalculatedValues.SuperTrend.Any())
{
// Filter pre-calculated SuperTrend values to match the candles we're processing
superTrend = preCalculatedValues.SuperTrend
.Where(s => s.SuperTrend.HasValue && candles.Any(c => c.Date == s.Date))
.OrderBy(s => s.Date)
.ToList();
}
// If no pre-calculated SuperTrend values, calculate them
if (superTrend == null || !superTrend.Any())
{
superTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value)
.Where(s => s.SuperTrend.HasValue)
.ToList();
}
// EMA50 and ADX are still calculated on-the-fly (not part of standard pre-calculation)
var ema50 = candles.GetEma(emaPeriod)
.Where(e => e.Ema.HasValue)
.ToList();
var adxResults = candles.GetAdx(adxPeriod)
.Where(a => a.Adx.HasValue && a.Pdi.HasValue && a.Mdi.HasValue)
.ToList();
ProcessSuperTrendCrossEmaSignals(superTrend, ema50, adxResults, candles, minimumRequiredHistory, adxThreshold);
return Signals.Where(s => s.Confidence != Confidence.None)
.OrderBy(s => s.Date)
.ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes SuperTrendCrossEma signals based on SuperTrend crossing EMA50 with ADX confirmation.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="superTrend">List of SuperTrend calculation results</param>
/// <param name="ema50">List of EMA50 calculation results</param>
/// <param name="adxResults">List of ADX calculation results</param>
/// <param name="candles">Candles to process</param>
/// <param name="minimumRequiredHistory">Minimum history required</param>
/// <param name="adxThreshold">ADX threshold for trend confirmation</param>
private void ProcessSuperTrendCrossEmaSignals(
List<SuperTrendResult> superTrend,
List<EmaResult> ema50,
List<AdxResult> adxResults,
HashSet<Candle> candles,
int minimumRequiredHistory,
int adxThreshold)
{
// 2. Create merged dataset with price + indicators
var superTrendCandles = MapSuperTrendToCandle(superTrend, candles.TakeLast(minimumRequiredHistory));
if (superTrendCandles.Count == 0)
return;
// 3. Add EMA50 and ADX values to the CandleSuperTrend objects
foreach (var candle in superTrendCandles)
{
var emaValue = ema50.Find(e => e.Date == candle.Date)?.Ema;
var adxValue = adxResults.Find(a => a.Date == candle.Date);
if (emaValue.HasValue)
candle.Ema50 = emaValue.Value;
if (adxValue != null)
{
candle.Adx = (decimal)adxValue.Adx.Value;
candle.Pdi = (decimal)adxValue.Pdi.Value;
candle.Mdi = (decimal)adxValue.Mdi.Value;
}
}
// 4. Signal detection logic with ADX filter
for (int i = 1; i < superTrendCandles.Count; i++)
{
var current = superTrendCandles[i];
var previous = superTrendCandles[i - 1];
// Convert SuperTrend to double for comparison
double currentSuperTrend = (double)current.SuperTrend;
double previousSuperTrend = (double)previous.SuperTrend;
// Ensure ADX data exists
if (current.Adx < adxThreshold) // Only trade when ADX confirms trend strength
continue;
/* LONG SIGNAL CONDITIONS:
* 1. SuperTrend crosses above EMA50
* 2. Price > SuperTrend and > EMA50
* 3. Previous state shows SuperTrend < EMA50
* 4. ADX > threshold and +DI > -DI (bullish momentum)
*/
bool longCross = currentSuperTrend > current.Ema50 &&
previousSuperTrend < previous.Ema50;
bool longPricePosition = current.Close > (decimal)currentSuperTrend &&
current.Close > (decimal)current.Ema50;
bool adxBullish = current.Pdi > current.Mdi; // Bullish momentum confirmation
if (longCross && longPricePosition && adxBullish)
{
AddSignal(current, TradeDirection.Long, Confidence.Medium);
}
/* SHORT SIGNAL CONDITIONS:
* 1. SuperTrend crosses below EMA50
* 2. Price < SuperTrend and < EMA50
* 3. Previous state shows SuperTrend > EMA50
* 4. ADX > threshold and -DI > +DI (bearish momentum)
*/
bool shortCross = currentSuperTrend < current.Ema50 &&
previousSuperTrend > previous.Ema50;
bool shortPricePosition = current.Close < (decimal)currentSuperTrend &&
current.Close < (decimal)current.Ema50;
bool adxBearish = current.Mdi > current.Pdi; // Bearish momentum confirmation
if (shortCross && shortPricePosition && adxBearish)
{
AddSignal(current, TradeDirection.Short, Confidence.Medium);
}
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
{
return new IndicatorsResultBase()

View File

@@ -29,38 +29,13 @@ public class SuperTrendIndicatorBase : IndicatorBase
try
{
var superTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue);
var superTrendCandle = MapSuperTrendToCandle(superTrend, candles.TakeLast(MinimumHistory));
if (superTrendCandle.Count == 0)
var superTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value)
.Where(s => s.SuperTrend.HasValue)
.ToList();
if (superTrend.Count == 0)
return null;
var previousCandle = superTrendCandle[0];
foreach (var currentCandle in superTrendCandle.Skip(1))
{
// // Short
// if (currentCandle.Close < previousCandle.SuperTrend && previousCandle.Close > previousCandle.SuperTrend)
// {
// AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
// }
//
// // Long
// if (currentCandle.Close > previousCandle.SuperTrend && previousCandle.Close < previousCandle.SuperTrend)
// {
// AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
// }
if (currentCandle.SuperTrend < currentCandle.Close)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
else if (currentCandle.SuperTrend > currentCandle.Close)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
previousCandle = currentCandle;
}
ProcessSuperTrendSignals(superTrend, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
@@ -70,6 +45,74 @@ public class SuperTrendIndicatorBase : IndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated SuperTrend values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= MinimumHistory)
{
return null;
}
try
{
// Use pre-calculated SuperTrend values if available
List<SuperTrendResult> superTrend = null;
if (preCalculatedValues?.SuperTrend != null && preCalculatedValues.SuperTrend.Any())
{
// Filter pre-calculated SuperTrend values to match the candles we're processing
superTrend = preCalculatedValues.SuperTrend
.Where(s => s.SuperTrend.HasValue && candles.Any(c => c.Date == s.Date))
.OrderBy(s => s.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (superTrend == null || !superTrend.Any())
{
return Run(candles);
}
ProcessSuperTrendSignals(superTrend, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes SuperTrend signals based on price position relative to SuperTrend line.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="superTrend">List of SuperTrend calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessSuperTrendSignals(List<SuperTrendResult> superTrend, HashSet<Candle> candles)
{
var superTrendCandle = MapSuperTrendToCandle(superTrend, candles.TakeLast(MinimumHistory));
if (superTrendCandle.Count == 0)
return;
var previousCandle = superTrendCandle[0];
foreach (var currentCandle in superTrendCandle.Skip(1))
{
if (currentCandle.SuperTrend < currentCandle.Close)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
}
else if (currentCandle.SuperTrend > currentCandle.Close)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
}
previousCandle = currentCandle;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
{
return new IndicatorsResultBase()

View File

@@ -17,42 +17,64 @@ namespace Managing.Domain.Strategies.Signals
public TradeDirection Direction { get; }
public override List<LightSignal> Run(HashSet<Candle> candles)
public override List<LightSignal> Run(HashSet<Candle> candles)
{
var signals = new List<LightSignal>();
if (candles.Count <= 3)
{
var signals = new List<LightSignal>();
if (candles.Count <= 3)
{
return null;
}
try
{
var lastFourCandles = candles.TakeLast(4);
Candle previousCandles = null;
foreach (var currentCandle in lastFourCandles)
{
if (Direction == TradeDirection.Long)
{
Check.That(new CloseHigherThanThePreviousHigh(previousCandles, currentCandle));
}
else
{
Check.That(new CloseLowerThanThePreviousHigh(previousCandles, currentCandle));
}
previousCandles = currentCandle;
}
return signals;
}
catch (RuleException)
{
return null;
}
return null;
}
try
{
ProcessThreeWhiteSoldiersSignals(candles);
return signals;
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Runs the indicator using pre-calculated values.
/// Note: ThreeWhiteSoldiers is a pattern-based indicator that doesn't use traditional indicator calculations,
/// so pre-calculated values are not applicable. This method falls back to regular Run().
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
// ThreeWhiteSoldiers doesn't use traditional indicators, so pre-calculated values don't apply
// Fall back to regular calculation
return Run(candles);
}
/// <summary>
/// Processes ThreeWhiteSoldiers pattern signals based on candle patterns.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="candles">Candles to process</param>
private void ProcessThreeWhiteSoldiersSignals(HashSet<Candle> candles)
{
var lastFourCandles = candles.TakeLast(4);
Candle previousCandles = null;
foreach (var currentCandle in lastFourCandles)
{
if (Direction == TradeDirection.Long)
{
Check.That(new CloseHigherThanThePreviousHigh(previousCandles, currentCandle));
}
else
{
Check.That(new CloseLowerThanThePreviousHigh(previousCandles, currentCandle));
}
previousCandles = currentCandle;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
{
throw new NotImplementedException();