From 51a227e27ed36e7a6aabc7cbf6f282f4bbe7e28b Mon Sep 17 00:00:00 2001 From: cryptooda Date: Mon, 10 Nov 2025 02:15:43 +0700 Subject: [PATCH] Improve perf for backtests --- .../Controllers/DataController.cs | 2 +- .../Services/ITradingService.cs | 2 +- .../Backtests/BacktestExecutor.cs | 42 +++- .../Backtests/Backtester.cs | 38 --- .../Bots/TradingBotBase.cs | 27 ++- .../Trading/TradingService.cs | 48 ++-- .../Indicators/Context/StDevContext.cs | 121 +++++++--- src/Managing.Domain/Indicators/IIndicator.cs | 10 + .../Indicators/IndicatorBase.cs | 11 + .../Signals/ChandelierExitIndicatorBase.cs | 97 +++++++- .../Signals/DualEmaCrossIndicatorBase.cs | 104 ++++++-- .../Indicators/Signals/EmaCrossIndicator.cs | 91 +++++-- .../Signals/EmaCrossIndicatorBase.cs | 91 +++++-- .../Indicators/Signals/LaggingSTC.cs | 135 +++++++---- .../Signals/MacdCrossIndicatorBase.cs | 97 ++++++-- .../RsiDivergenceConfirmIndicatorBase.cs | 66 ++++- .../Signals/RsiDivergenceIndicatorBase.cs | 66 ++++- .../Indicators/Signals/StcIndicatorBase.cs | 97 ++++++-- .../Indicators/Signals/SuperTrendCrossEma.cs | 228 ++++++++++++------ .../Signals/SuperTrendIndicatorBase.cs | 103 +++++--- .../ThreeWhiteSoldiersIndicatorBase.cs | 88 ++++--- .../Trends/EmaTrendIndicatorBase.cs | 85 +++++-- .../Trends/StochRsiTrendIndicatorBase.cs | 90 +++++-- .../Shared/Helpers/TradingBox.cs | 31 ++- 24 files changed, 1327 insertions(+), 443 deletions(-) diff --git a/src/Managing.Api/Controllers/DataController.cs b/src/Managing.Api/Controllers/DataController.cs index cc809954..45ea08fc 100644 --- a/src/Managing.Api/Controllers/DataController.cs +++ b/src/Managing.Api/Controllers/DataController.cs @@ -346,7 +346,7 @@ public class DataController : ControllerBase { // Map ScenarioRequest to domain Scenario object var domainScenario = MapScenarioRequestToScenario(request.Scenario); - indicatorsValues = _tradingService.CalculateIndicatorsValuesAsync(domainScenario, candles); + indicatorsValues = await _tradingService.CalculateIndicatorsValuesAsync(domainScenario, candles); } return Ok(new CandlesWithIndicatorsResponse diff --git a/src/Managing.Application.Abstractions/Services/ITradingService.cs b/src/Managing.Application.Abstractions/Services/ITradingService.cs index 89086b01..5958b5a0 100644 --- a/src/Managing.Application.Abstractions/Services/ITradingService.cs +++ b/src/Managing.Application.Abstractions/Services/ITradingService.cs @@ -58,7 +58,7 @@ public interface ITradingService /// The scenario containing indicators. /// The candles to calculate indicators for. /// A dictionary of indicator types to their calculated values. - Dictionary CalculateIndicatorsValuesAsync( + Task> CalculateIndicatorsValuesAsync( Scenario scenario, HashSet candles); diff --git a/src/Managing.Application/Backtests/BacktestExecutor.cs b/src/Managing.Application/Backtests/BacktestExecutor.cs index eb1ad1ef..07501feb 100644 --- a/src/Managing.Application/Backtests/BacktestExecutor.cs +++ b/src/Managing.Application/Backtests/BacktestExecutor.cs @@ -3,13 +3,16 @@ using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Services; using Managing.Application.Bots; using Managing.Common; +using Managing.Core; using Managing.Domain.Backtests; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.Shared.Helpers; +using Managing.Domain.Strategies.Base; using Managing.Domain.Users; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using static Managing.Common.Enums; namespace Managing.Application.Backtests; @@ -76,7 +79,7 @@ public class BacktestExecutor } // Create a fresh TradingBotBase instance for this backtest - var tradingBot = await CreateTradingBotInstance(config); + var tradingBot = CreateTradingBotInstance(config); tradingBot.Account = user.Accounts.First(); var totalCandles = candles.Count; @@ -86,6 +89,41 @@ public class BacktestExecutor _logger.LogInformation("Backtest requested by {UserId} with {TotalCandles} candles for {Ticker} on {Timeframe}", user.Id, totalCandles, config.Ticker, config.Timeframe); + // Pre-calculate indicator values once for all candles to optimize performance + // This avoids recalculating indicators for every candle iteration + Dictionary preCalculatedIndicatorValues = null; + if (config.Scenario != null) + { + try + { + _logger.LogInformation("Pre-calculating indicator values for {IndicatorCount} indicators", + config.Scenario.Indicators?.Count ?? 0); + + // Convert LightScenario to Scenario for CalculateIndicatorsValuesAsync + var scenario = config.Scenario.ToScenario(); + + // Calculate all indicator values once with all candles + preCalculatedIndicatorValues = await ServiceScopeHelpers.WithScopedService>( + _scopeFactory, + async tradingService => + { + return await tradingService.CalculateIndicatorsValuesAsync(scenario, candles); + }); + + // Store pre-calculated values in trading bot for use during signal generation + tradingBot.PreCalculatedIndicatorValues = preCalculatedIndicatorValues; + + _logger.LogInformation("Successfully pre-calculated indicator values for {IndicatorCount} indicator types", + preCalculatedIndicatorValues?.Count ?? 0); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to pre-calculate indicator values, will calculate on-the-fly. Error: {ErrorMessage}", ex.Message); + // Continue with normal calculation if pre-calculation fails + preCalculatedIndicatorValues = null; + } + } + // Initialize wallet balance with first candle tradingBot.WalletBalances.Clear(); tradingBot.WalletBalances.Add(candles.FirstOrDefault()!.Date, config.BotTradingBalance); @@ -239,7 +277,7 @@ public class BacktestExecutor /// /// Creates a TradingBotBase instance for backtesting /// - private async Task CreateTradingBotInstance(TradingBotConfig config) + private TradingBotBase CreateTradingBotInstance(TradingBotConfig config) { // Validate configuration for backtesting if (config == null) diff --git a/src/Managing.Application/Backtests/Backtester.cs b/src/Managing.Application/Backtests/Backtester.cs index d4c3ce2e..452e3b1a 100644 --- a/src/Managing.Application/Backtests/Backtester.cs +++ b/src/Managing.Application/Backtests/Backtester.cs @@ -139,41 +139,6 @@ namespace Managing.Application.Backtests return await RunTradingBotBacktest(config, startDate, endDate, user, false, withCandles, requestId, metadata); } - // Removed RunBacktestWithCandles - backtests now run via compute workers - // This method is kept for backward compatibility but should not be called directly - - private async Task> GetCandles(Ticker ticker, Timeframe timeframe, - DateTime startDate, DateTime endDate) - { - var candles = await _exchangeService.GetCandlesInflux(TradingExchanges.Evm, ticker, - startDate, timeframe, endDate); - - if (candles == null || candles.Count == 0) - throw new Exception( - $"No candles for {ticker} on {timeframe} timeframe for start {startDate} to end {endDate}"); - - return candles; - } - - - // Removed CreateCleanConfigForOrleans - no longer needed with job queue approach - - private async Task SendBacktestNotificationIfCriteriaMet(Backtest backtest) - { - try - { - if (backtest.Score > 60) - { - await _messengerService.SendBacktestNotification(backtest); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to send backtest notification for backtest {Id}", backtest.Id); - } - } - - public async Task DeleteBacktestAsync(string id) { try @@ -243,7 +208,6 @@ namespace Managing.Application.Backtests return (backtests, totalCount); } - public async Task GetBacktestByIdForUserAsync(User user, string id) { var backtest = await _backtestRepository.GetBacktestByIdForUserAsync(user, id); @@ -605,7 +569,5 @@ namespace Managing.Application.Backtests if (string.IsNullOrWhiteSpace(requestId) || response == null) return; await _hubContext.Clients.Group($"bundle-{requestId}").SendAsync("BundleBacktestUpdate", response); } - - // Removed TriggerBundleBacktestGrain methods - bundle backtests now use job queue } } \ No newline at end of file diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index d7c89ead..41569111 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -14,6 +14,7 @@ using Managing.Domain.Indicators; using Managing.Domain.Scenarios; using Managing.Domain.Shared.Helpers; using Managing.Domain.Strategies; +using Managing.Domain.Strategies.Base; using Managing.Domain.Synth.Models; using Managing.Domain.Trades; using Microsoft.Extensions.DependencyInjection; @@ -28,7 +29,9 @@ public class TradingBotBase : ITradingBot public readonly ILogger Logger; private readonly IServiceScopeFactory _scopeFactory; private const int NEW_POSITION_GRACE_SECONDS = 45; // grace window before evaluating missing orders - private const int CLOSE_POSITION_GRACE_MS = 20000; // grace window before closing position to allow broker processing (20 seconds) + + private const int + CLOSE_POSITION_GRACE_MS = 20000; // grace window before closing position to allow broker processing (20 seconds) public TradingBotConfig Config { get; set; } public Account Account { get; set; } @@ -42,6 +45,12 @@ public class TradingBotBase : ITradingBot public Candle LastCandle { get; set; } public DateTime? LastPositionClosingTime { get; set; } + /// + /// Pre-calculated indicator values for backtesting optimization. + /// Key is IndicatorType, Value is the calculated indicator result. + /// + public Dictionary PreCalculatedIndicatorValues { get; set; } + public TradingBotBase( ILogger logger, @@ -56,6 +65,7 @@ public class TradingBotBase : ITradingBot Positions = new Dictionary(); WalletBalances = new Dictionary(); PreloadSince = CandleHelpers.GetBotPreloadSinceFromTimeframe(config.Timeframe); + PreCalculatedIndicatorValues = new Dictionary(); } public async Task Start(BotStatus previousStatus) @@ -269,7 +279,8 @@ public class TradingBotBase : ITradingBot if (Config.IsForBacktest && candles != null) { var backtestSignal = - TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod); + TradingBox.GetSignal(candles, Config.Scenario, Signals, Config.Scenario.LoopbackPeriod, + PreCalculatedIndicatorValues); if (backtestSignal == null) return; await AddSignal(backtestSignal); } @@ -768,7 +779,8 @@ public class TradingBotBase : ITradingBot await LogInformation( $"ā° Time Limit Close\nClosing position due to time limit: `{Config.MaxPositionTimeHours}h` exceeded\nšŸ“ˆ Position Status: {profitStatus}\nšŸ’° Entry: `${positionForSignal.Open.Price}` → Current: `${lastCandle.Close}`\nšŸ“Š Realized PNL: `${currentPnl:F2}` (`{pnlPercentage:F2}%`)"); // Force a market close: compute PnL based on current price instead of SL/TP - await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true, true); + await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true, + true); return; } } @@ -1355,7 +1367,8 @@ public class TradingBotBase : ITradingBot await SetPositionStatus(signal.Identifier, PositionStatus.Finished); } - await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose); + await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null, + forceMarketClose); } else { @@ -1371,13 +1384,15 @@ public class TradingBotBase : ITradingBot // Trade close on exchange => Should close trade manually await SetPositionStatus(signal.Identifier, PositionStatus.Finished); // Ensure trade dates are properly updated even for canceled/rejected positions - await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose); + await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null, + forceMarketClose); } } } } - private async Task HandleClosedPosition(Position position, decimal? forcedClosingPrice = null, bool forceMarketClose = false) + private async Task HandleClosedPosition(Position position, decimal? forcedClosingPrice = null, + bool forceMarketClose = false) { if (Positions.ContainsKey(position.Identifier)) { diff --git a/src/Managing.Application/Trading/TradingService.cs b/src/Managing.Application/Trading/TradingService.cs index 33ac8d6b..27ea2208 100644 --- a/src/Managing.Application/Trading/TradingService.cs +++ b/src/Managing.Application/Trading/TradingService.cs @@ -435,33 +435,37 @@ public class TradingService : ITradingService /// The scenario containing indicators. /// The candles to calculate indicators for. /// A dictionary of indicator types to their calculated values. - public Dictionary CalculateIndicatorsValuesAsync( + public async Task> CalculateIndicatorsValuesAsync( Scenario scenario, HashSet candles) { - var indicatorsValues = new Dictionary(); - - if (scenario?.Indicators == null || scenario.Indicators.Count == 0) + // Offload CPU-bound indicator calculations to thread pool + return await Task.Run(() => { + var indicatorsValues = new Dictionary(); + + if (scenario?.Indicators == null || scenario.Indicators.Count == 0) + { + return indicatorsValues; + } + + // Build indicators from scenario + foreach (var indicator in scenario.Indicators) + { + try + { + var buildedIndicator = ScenarioHelpers.BuildIndicator(ScenarioHelpers.BaseToLight(indicator)); + indicatorsValues[indicator.Type] = buildedIndicator.GetIndicatorValues(candles); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calculating indicator {IndicatorName}: {ErrorMessage}", + indicator.Name, ex.Message); + } + } + return indicatorsValues; - } - - // Build indicators from scenario - foreach (var indicator in scenario.Indicators) - { - try - { - var buildedIndicator = ScenarioHelpers.BuildIndicator(ScenarioHelpers.BaseToLight(indicator)); - indicatorsValues[indicator.Type] = buildedIndicator.GetIndicatorValues(candles); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error calculating indicator {IndicatorName}: {ErrorMessage}", - indicator.Name, ex.Message); - } - } - - return indicatorsValues; + }); } public async Task GetIndicatorByNameUserAsync(string name, User user) diff --git a/src/Managing.Domain/Indicators/Context/StDevContext.cs b/src/Managing.Domain/Indicators/Context/StDevContext.cs index 74d1b418..4a62ca1d 100644 --- a/src/Managing.Domain/Indicators/Context/StDevContext.cs +++ b/src/Managing.Domain/Indicators/Context/StDevContext.cs @@ -28,43 +28,10 @@ public class StDevContext : IndicatorBase try { var stDev = candles.GetStdDev(Period.Value).ToList(); - var stDevCandles = MapStDev(stDev, candles.TakeLast(Period.Value)); - if (stDev.Count == 0) return null; - var lastCandle = stDevCandles.Last(); - var zScore = lastCandle.ZScore ?? 0; - - // Determine confidence based on Z-score ranges - // Lower absolute Z-score = more normal volatility = higher confidence for trading - // Higher absolute Z-score = more extreme volatility = lower confidence for trading - Confidence confidence; - - if (Math.Abs(zScore) <= 0.5) - { - // Very low volatility - ideal conditions for trading - confidence = Confidence.High; - } - else if (Math.Abs(zScore) <= 1.0) - { - // Normal volatility - good conditions for trading - confidence = Confidence.Medium; - } - else if (Math.Abs(zScore) <= 1.5) - { - // Elevated volatility - caution advised - confidence = Confidence.Low; - } - else - { - // High volatility - trading not recommended - confidence = Confidence.None; - } - - // Context strategies always return TradeDirection.None - // The confidence level indicates the quality of market conditions - AddSignal(lastCandle, TradeDirection.None, confidence); + ProcessStDevSignals(stDev, candles); return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList(); } @@ -74,6 +41,92 @@ public class StDevContext : IndicatorBase } } + /// + /// Runs the indicator using pre-calculated StdDev values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= Period) + { + return null; + } + + try + { + // Use pre-calculated StdDev values if available + List stDev = null; + if (preCalculatedValues?.StdDev != null && preCalculatedValues.StdDev.Any()) + { + // Filter pre-calculated StdDev values to match the candles we're processing + stDev = preCalculatedValues.StdDev + .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 (stDev == null || !stDev.Any()) + { + return Run(candles); + } + + ProcessStDevSignals(stDev, candles); + + return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList(); + } + catch (RuleException) + { + return null; + } + } + + /// + /// Processes StdDev context signals based on Z-score volatility analysis. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of StdDev calculation results + /// Candles to process + private void ProcessStDevSignals(List stDev, HashSet candles) + { + var stDevCandles = MapStDev(stDev, candles.TakeLast(Period.Value)); + + if (stDevCandles.Count == 0) + return; + + var lastCandle = stDevCandles.Last(); + var zScore = lastCandle.ZScore ?? 0; + + // Determine confidence based on Z-score ranges + // Lower absolute Z-score = more normal volatility = higher confidence for trading + // Higher absolute Z-score = more extreme volatility = lower confidence for trading + Confidence confidence; + + if (Math.Abs(zScore) <= 0.5) + { + // Very low volatility - ideal conditions for trading + confidence = Confidence.High; + } + else if (Math.Abs(zScore) <= 1.0) + { + // Normal volatility - good conditions for trading + confidence = Confidence.Medium; + } + else if (Math.Abs(zScore) <= 1.5) + { + // Elevated volatility - caution advised + confidence = Confidence.Low; + } + else + { + // High volatility - trading not recommended + confidence = Confidence.None; + } + + // Context strategies always return TradeDirection.None + // The confidence level indicates the quality of market conditions + AddSignal(lastCandle, TradeDirection.None, confidence); + } + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { var test = new IndicatorsResultBase() diff --git a/src/Managing.Domain/Indicators/IIndicator.cs b/src/Managing.Domain/Indicators/IIndicator.cs index a3526c93..6a138fbd 100644 --- a/src/Managing.Domain/Indicators/IIndicator.cs +++ b/src/Managing.Domain/Indicators/IIndicator.cs @@ -20,6 +20,16 @@ namespace Managing.Domain.Strategies int? CyclePeriods { get; set; } List Run(HashSet candles); + + /// + /// Runs the indicator using pre-calculated indicator values for performance optimization. + /// If pre-calculated values are not available or not applicable, falls back to regular Run(). + /// + /// The candles to process + /// Pre-calculated indicator values (optional) + /// List of signals generated by the indicator + List Run(HashSet candles, IndicatorsResultBase preCalculatedValues); + IndicatorsResultBase GetIndicatorValues(HashSet candles); } } \ No newline at end of file diff --git a/src/Managing.Domain/Indicators/IndicatorBase.cs b/src/Managing.Domain/Indicators/IndicatorBase.cs index 95569b1f..851bad12 100644 --- a/src/Managing.Domain/Indicators/IndicatorBase.cs +++ b/src/Managing.Domain/Indicators/IndicatorBase.cs @@ -46,6 +46,17 @@ namespace Managing.Domain.Strategies throw new NotImplementedException(); } + /// + /// Runs the indicator using pre-calculated values if available, otherwise falls back to regular Run(). + /// Default implementation falls back to regular Run() - override in derived classes to use pre-calculated values. + /// + public virtual List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + // Default implementation: ignore pre-calculated values and use regular Run() + // Derived classes should override this to use pre-calculated values for performance + return Run(candles); + } + public virtual IndicatorsResultBase GetIndicatorValues(HashSet candles) { throw new NotImplementedException(); diff --git a/src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs index a0874f5d..c0a04bd5 100644 --- a/src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/ChandelierExitIndicatorBase.cs @@ -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 } } + /// + /// Runs the indicator using pre-calculated Chandelier values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= MinimumHistory) + { + return null; + } + + try + { + // Use pre-calculated Chandelier values if available + List chandelierLong = null; + List 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; + } + } + + /// + /// Processes Chandelier signals for both Long and Short types. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// Candles to process + private void ProcessChandelierSignals(HashSet candles) + { + GetSignals(ChandelierType.Long, candles); + GetSignals(ChandelierType.Short, candles); + } + + /// + /// Processes Chandelier signals using pre-calculated values. + /// + /// Pre-calculated Long Chandelier values + /// Pre-calculated Short Chandelier values + /// Candles to process + private void ProcessChandelierSignalsWithPreCalculated( + List chandelierLong, + List chandelierShort, + HashSet candles) + { + GetSignalsWithPreCalculated(ChandelierType.Long, chandelierLong, candles); + GetSignalsWithPreCalculated(ChandelierType.Short, chandelierShort, candles); + } + public override IndicatorsResultBase GetIndicatorValues(HashSet 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 chandelier, HashSet candles) + { + ProcessChandelierSignalsForType(chandelier, chandelierType, candles); + } + + /// + /// Processes Chandelier signals for a specific type (Long or Short). + /// This method is shared between regular and optimized signal processing. + /// + /// Chandelier calculation results + /// Type of Chandelier (Long or Short) + /// Candles to process + private void ProcessChandelierSignalsForType(List chandelier, ChandelierType chandelierType, HashSet 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 diff --git a/src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs index 809d74b7..04e53f8c 100644 --- a/src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/DualEmaCrossIndicatorBase.cs @@ -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 } } + /// + /// Runs the indicator using pre-calculated EMA values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= MinimumHistory) + { + return null; + } + + try + { + // Use pre-calculated EMA values if available + List fastEma = null; + List 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; + } + } + + /// + /// Processes dual EMA cross signals based on Fast EMA crossing Slow EMA. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of Fast EMA calculation results + /// List of Slow EMA calculation results + /// Candles to process + private void ProcessDualEmaCrossSignals(List fastEma, List slowEma, HashSet 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 MapDualEmaToCandle(List fastEma, List slowEma, IEnumerable candles) { diff --git a/src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs index 60f5d744..25b1b6e2 100644 --- a/src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs +++ b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicator.cs @@ -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 } } + /// + /// Runs the indicator using pre-calculated EMA values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= Period) + { + return null; + } + + try + { + // Use pre-calculated EMA values if available + List 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; + } + } + + /// + /// Processes EMA cross signals based on price crossing the EMA line. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of EMA calculation results + /// Candles to process + private void ProcessEmaCrossSignals(List ema, HashSet 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, diff --git a/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs index fb2c2857..82842a2d 100644 --- a/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/EmaCrossIndicatorBase.cs @@ -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 } } + /// + /// Runs the indicator using pre-calculated EMA values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= Period) + { + return null; + } + + try + { + // Use pre-calculated EMA values if available + List 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; + } + } + + /// + /// Processes EMA cross signals based on price crossing the EMA line. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of EMA calculation results + /// Candles to process + private void ProcessEmaCrossSignals(List ema, HashSet 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, diff --git a/src/Managing.Domain/Indicators/Signals/LaggingSTC.cs b/src/Managing.Domain/Indicators/Signals/LaggingSTC.cs index 76ae3640..b0004474 100644 --- a/src/Managing.Domain/Indicators/Signals/LaggingSTC.cs +++ b/src/Managing.Domain/Indicators/Signals/LaggingSTC.cs @@ -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 } } + /// + /// Runs the indicator using pre-calculated STC values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= 2 * (SlowPeriods + CyclePeriods)) + { + return null; + } + + try + { + // Use pre-calculated STC values if available + List 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; + } + } + + /// + /// 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. + /// + /// List of STC calculation results + /// Candles to process + private void ProcessLaggingStcSignals(List stc, HashSet 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 candles) { var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList(); diff --git a/src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs index 5fcdc980..2f6183db 100644 --- a/src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/MacdCrossIndicatorBase.cs @@ -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 } } + /// + /// Runs the indicator using pre-calculated MACD values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= 2 * (SlowPeriods + SignalPeriods)) + { + return null; + } + + try + { + // Use pre-calculated MACD values if available + List 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; + } + } + + /// + /// Processes MACD signals based on MACD line crossing the Signal line. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of MACD calculation results + /// Candles to process + private void ProcessMacdSignals(List macd, HashSet 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 candles) { return new IndicatorsResultBase() diff --git a/src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs index 791447ab..4471367c 100644 --- a/src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/RsiDivergenceConfirmIndicatorBase.cs @@ -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 } } + /// + /// Runs the indicator using pre-calculated RSI values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= Period) + { + return null; + } + + try + { + // Use pre-calculated RSI values if available + List 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; + } + } + + /// + /// Processes RSI divergence confirmation signals based on price and RSI divergence patterns. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of RSI calculation results + /// Candles to process + private void ProcessRsiDivergenceConfirmSignals(List rsiResult, HashSet 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 candles) { return new IndicatorsResultBase() diff --git a/src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs index 641d29af..0c2d7c3c 100644 --- a/src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/RsiDivergenceIndicatorBase.cs @@ -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 } } + /// + /// Runs the indicator using pre-calculated RSI values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (!Period.HasValue || candles.Count <= Period) + { + return null; + } + + try + { + // Use pre-calculated RSI values if available + List 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; + } + } + + /// + /// Processes RSI divergence signals based on price and RSI divergence patterns. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of RSI calculation results + /// Candles to process + private void ProcessRsiDivergenceSignals(List rsiResult, HashSet 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 candles) { return new IndicatorsResultBase() diff --git a/src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs index f7cf85f9..bbf16f9d 100644 --- a/src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/StcIndicatorBase.cs @@ -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 } } + /// + /// Runs the indicator using pre-calculated STC values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= 2 * (SlowPeriods + CyclePeriods)) + { + return null; + } + + try + { + // Use pre-calculated STC values if available + List 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 candles) { if (FastPeriods != null && SlowPeriods != null) @@ -80,6 +100,39 @@ public class StcIndicatorBase : IndicatorBase return null; } + /// + /// Processes STC signals based on STC values crossing thresholds (25 and 75). + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of STC calculation results + /// Candles to process + private void ProcessStcSignals(List stc, HashSet 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 MapStcToCandle(List stc, IEnumerable candles) { var sctList = new List(); diff --git a/src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs b/src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs index 03ada7c3..c2809e65 100644 --- a/src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs +++ b/src/Managing.Domain/Indicators/Signals/SuperTrendCrossEma.cs @@ -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; } + /// + /// 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. + /// + public override List Run(HashSet 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 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; + } + } + + /// + /// Processes SuperTrendCrossEma signals based on SuperTrend crossing EMA50 with ADX confirmation. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of SuperTrend calculation results + /// List of EMA50 calculation results + /// List of ADX calculation results + /// Candles to process + /// Minimum history required + /// ADX threshold for trend confirmation + private void ProcessSuperTrendCrossEmaSignals( + List superTrend, + List ema50, + List adxResults, + HashSet 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 candles) { return new IndicatorsResultBase() diff --git a/src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs index bf17e4b5..dcfd673b 100644 --- a/src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/SuperTrendIndicatorBase.cs @@ -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 } } + /// + /// Runs the indicator using pre-calculated SuperTrend values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= MinimumHistory) + { + return null; + } + + try + { + // Use pre-calculated SuperTrend values if available + List 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; + } + } + + /// + /// Processes SuperTrend signals based on price position relative to SuperTrend line. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of SuperTrend calculation results + /// Candles to process + private void ProcessSuperTrendSignals(List superTrend, HashSet 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 candles) { return new IndicatorsResultBase() diff --git a/src/Managing.Domain/Indicators/Signals/ThreeWhiteSoldiersIndicatorBase.cs b/src/Managing.Domain/Indicators/Signals/ThreeWhiteSoldiersIndicatorBase.cs index 712e60e9..d6cb7197 100644 --- a/src/Managing.Domain/Indicators/Signals/ThreeWhiteSoldiersIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Signals/ThreeWhiteSoldiersIndicatorBase.cs @@ -17,42 +17,64 @@ namespace Managing.Domain.Strategies.Signals public TradeDirection Direction { get; } - public override List Run(HashSet candles) + public override List Run(HashSet candles) + { + var signals = new List(); + + if (candles.Count <= 3) { - var signals = new List(); - - 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; + } + } + + /// + /// 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(). + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + // ThreeWhiteSoldiers doesn't use traditional indicators, so pre-calculated values don't apply + // Fall back to regular calculation + return Run(candles); + } + + /// + /// Processes ThreeWhiteSoldiers pattern signals based on candle patterns. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// Candles to process + private void ProcessThreeWhiteSoldiersSignals(HashSet 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 candles) { throw new NotImplementedException(); diff --git a/src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs b/src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs index 4725b13b..a2cf0244 100644 --- a/src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Trends/EmaTrendIndicatorBase.cs @@ -28,25 +28,10 @@ public class EmaTrendIndicatorBase : 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 (currentCandle.Close > (decimal)currentCandle.Ema) - { - AddSignal(currentCandle, TradeDirection.Long, Confidence.None); - } - else - { - AddSignal(currentCandle, TradeDirection.Short, Confidence.None); - } - - previousCandle = currentCandle; - } + ProcessEmaTrendSignals(ema, candles); return Signals.OrderBy(s => s.Date).ToList(); } @@ -56,6 +41,74 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase } } + /// + /// Runs the indicator using pre-calculated EMA values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= 2 * Period) + { + return null; + } + + try + { + // Use pre-calculated EMA values if available + List 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); + } + + ProcessEmaTrendSignals(ema, candles); + + return Signals.OrderBy(s => s.Date).ToList(); + } + catch (RuleException) + { + return null; + } + } + + /// + /// Processes EMA trend signals based on price position relative to EMA line. + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of EMA calculation results + /// Candles to process + private void ProcessEmaTrendSignals(List ema, HashSet 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 (currentCandle.Close > (decimal)currentCandle.Ema) + { + AddSignal(currentCandle, TradeDirection.Long, Confidence.None); + } + else + { + AddSignal(currentCandle, TradeDirection.Short, Confidence.None); + } + + previousCandle = currentCandle; + } + } + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() diff --git a/src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs b/src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs index b7236aec..35574362 100644 --- a/src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs +++ b/src/Managing.Domain/Indicators/Trends/StochRsiTrendIndicatorBase.cs @@ -37,26 +37,12 @@ public class StochRsiTrendIndicatorBase : IndicatorBase { var stochRsi = candles .GetStochRsi(Period.Value, StochPeriods.Value, SignalPeriods.Value, SmoothPeriods.Value) - .RemoveWarmupPeriods(); - var stochRsiCandles = MapStochRsiToCandle(stochRsi, candles.TakeLast(Period.Value)); - - if (stochRsi.Count() == 0) + .RemoveWarmupPeriods() + .ToList(); + if (stochRsi.Count == 0) return null; - var previousCandle = stochRsiCandles[0]; - foreach (var currentCandle in stochRsiCandles.Skip(1)) - { - if (currentCandle.Signal < 20) - { - AddSignal(currentCandle, TradeDirection.Long, Confidence.None); - } - else if (currentCandle.Signal > 80) - { - AddSignal(currentCandle, TradeDirection.Short, Confidence.None); - } - - previousCandle = currentCandle; - } + ProcessStochRsiTrendSignals(stochRsi, candles); return Signals.OrderBy(s => s.Date).ToList(); } @@ -66,6 +52,74 @@ public class StochRsiTrendIndicatorBase : IndicatorBase } } + /// + /// Runs the indicator using pre-calculated StochRsi values for performance optimization. + /// + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= 10 * Period + 50) + { + return null; + } + + try + { + // Use pre-calculated StochRsi values if available + List stochRsi = null; + if (preCalculatedValues?.StochRsi != null && preCalculatedValues.StochRsi.Any()) + { + // Filter pre-calculated StochRsi values to match the candles we're processing + stochRsi = preCalculatedValues.StochRsi + .Where(s => s.Signal.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 (stochRsi == null || !stochRsi.Any()) + { + return Run(candles); + } + + ProcessStochRsiTrendSignals(stochRsi, candles); + + return Signals.OrderBy(s => s.Date).ToList(); + } + catch (RuleException) + { + return null; + } + } + + /// + /// Processes StochRsi trend signals based on Signal line thresholds (20 and 80). + /// This method is shared between the regular Run() and optimized Run() methods. + /// + /// List of StochRsi calculation results + /// Candles to process + private void ProcessStochRsiTrendSignals(List stochRsi, HashSet candles) + { + var stochRsiCandles = MapStochRsiToCandle(stochRsi, candles.TakeLast(Period.Value)); + + if (stochRsiCandles.Count == 0) + return; + + var previousCandle = stochRsiCandles[0]; + foreach (var currentCandle in stochRsiCandles.Skip(1)) + { + if (currentCandle.Signal < 20) + { + AddSignal(currentCandle, TradeDirection.Long, Confidence.None); + } + else if (currentCandle.Signal > 80) + { + AddSignal(currentCandle, TradeDirection.Short, Confidence.None); + } + + previousCandle = currentCandle; + } + } + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) { return new IndicatorsResultBase() diff --git a/src/Managing.Domain/Shared/Helpers/TradingBox.cs b/src/Managing.Domain/Shared/Helpers/TradingBox.cs index 7c2624fe..eafdba57 100644 --- a/src/Managing.Domain/Shared/Helpers/TradingBox.cs +++ b/src/Managing.Domain/Shared/Helpers/TradingBox.cs @@ -3,6 +3,7 @@ using Managing.Domain.Indicators; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; using Managing.Domain.Strategies; +using Managing.Domain.Strategies.Base; using Managing.Domain.Trades; using static Managing.Common.Enums; @@ -53,11 +54,25 @@ public static class TradingBox public static LightSignal GetSignal(HashSet newCandles, LightScenario scenario, Dictionary previousSignal, int? loopbackPeriod = 1) { - return GetSignal(newCandles, scenario, previousSignal, _defaultConfig, loopbackPeriod); + return GetSignal(newCandles, scenario, previousSignal, _defaultConfig, loopbackPeriod, null); + } + + public static LightSignal GetSignal(HashSet newCandles, LightScenario scenario, + Dictionary previousSignal, int? loopbackPeriod, + Dictionary preCalculatedIndicatorValues) + { + return GetSignal(newCandles, scenario, previousSignal, _defaultConfig, loopbackPeriod, preCalculatedIndicatorValues); } public static LightSignal GetSignal(HashSet newCandles, LightScenario lightScenario, Dictionary previousSignal, IndicatorComboConfig config, int? loopbackPeriod = 1) + { + return GetSignal(newCandles, lightScenario, previousSignal, config, loopbackPeriod, null); + } + + public static LightSignal GetSignal(HashSet newCandles, LightScenario lightScenario, + Dictionary previousSignal, IndicatorComboConfig config, int? loopbackPeriod, + Dictionary preCalculatedIndicatorValues) { var signalOnCandles = new List(); var limitedCandles = newCandles.ToList().TakeLast(600).ToList(); @@ -65,7 +80,19 @@ public static class TradingBox foreach (var indicator in lightScenario.Indicators) { IIndicator indicatorInstance = indicator.ToInterface(); - var signals = indicatorInstance.Run(newCandles); + + // Use pre-calculated indicator values if available (for backtest optimization) + List signals; + if (preCalculatedIndicatorValues != null && preCalculatedIndicatorValues.ContainsKey(indicator.Type)) + { + // Use pre-calculated values to avoid recalculating indicators + signals = indicatorInstance.Run(newCandles, preCalculatedIndicatorValues[indicator.Type]); + } + else + { + // Normal path: calculate indicators on the fly + signals = indicatorInstance.Run(newCandles); + } if (signals == null || signals.Count() == 0) {