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)
{