Improve perf for backtests

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

View File

@@ -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

View File

@@ -58,7 +58,7 @@ public interface ITradingService
/// <param name="scenario">The scenario containing indicators.</param>
/// <param name="candles">The candles to calculate indicators for.</param>
/// <returns>A dictionary of indicator types to their calculated values.</returns>
Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValuesAsync(
Task<Dictionary<IndicatorType, IndicatorsResultBase>> CalculateIndicatorsValuesAsync(
Scenario scenario,
HashSet<Candle> candles);

View File

@@ -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<IndicatorType, IndicatorsResultBase> 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<ITradingService, Dictionary<IndicatorType, IndicatorsResultBase>>(
_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
/// <summary>
/// Creates a TradingBotBase instance for backtesting
/// </summary>
private async Task<TradingBotBase> CreateTradingBotInstance(TradingBotConfig config)
private TradingBotBase CreateTradingBotInstance(TradingBotConfig config)
{
// Validate configuration for backtesting
if (config == null)

View File

@@ -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<HashSet<Candle>> 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<bool> DeleteBacktestAsync(string id)
{
try
@@ -243,7 +208,6 @@ namespace Managing.Application.Backtests
return (backtests, totalCount);
}
public async Task<Backtest> 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
}
}

View File

@@ -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<TradingBotBase> 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; }
/// <summary>
/// Pre-calculated indicator values for backtesting optimization.
/// Key is IndicatorType, Value is the calculated indicator result.
/// </summary>
public Dictionary<IndicatorType, IndicatorsResultBase> PreCalculatedIndicatorValues { get; set; }
public TradingBotBase(
ILogger<TradingBotBase> logger,
@@ -56,6 +65,7 @@ public class TradingBotBase : ITradingBot
Positions = new Dictionary<Guid, Position>();
WalletBalances = new Dictionary<DateTime, decimal>();
PreloadSince = CandleHelpers.GetBotPreloadSinceFromTimeframe(config.Timeframe);
PreCalculatedIndicatorValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
}
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))
{

View File

@@ -435,33 +435,37 @@ public class TradingService : ITradingService
/// <param name="scenario">The scenario containing indicators.</param>
/// <param name="candles">The candles to calculate indicators for.</param>
/// <returns>A dictionary of indicator types to their calculated values.</returns>
public Dictionary<IndicatorType, IndicatorsResultBase> CalculateIndicatorsValuesAsync(
public async Task<Dictionary<IndicatorType, IndicatorsResultBase>> CalculateIndicatorsValuesAsync(
Scenario scenario,
HashSet<Candle> candles)
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
if (scenario?.Indicators == null || scenario.Indicators.Count == 0)
// Offload CPU-bound indicator calculations to thread pool
return await Task.Run(() =>
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
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<IndicatorBase?> GetIndicatorByNameUserAsync(string name, User user)

View File

@@ -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
}
}
/// <summary>
/// Runs the indicator using pre-calculated StdDev values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= Period)
{
return null;
}
try
{
// Use pre-calculated StdDev values if available
List<StdDevResult> 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;
}
}
/// <summary>
/// Processes StdDev context signals based on Z-score volatility analysis.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="stDev">List of StdDev calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessStDevSignals(List<StdDevResult> stDev, HashSet<Candle> 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<Candle> candles)
{
var test = new IndicatorsResultBase()

View File

@@ -20,6 +20,16 @@ namespace Managing.Domain.Strategies
int? CyclePeriods { get; set; }
List<LightSignal> Run(HashSet<Candle> candles);
/// <summary>
/// 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().
/// </summary>
/// <param name="candles">The candles to process</param>
/// <param name="preCalculatedValues">Pre-calculated indicator values (optional)</param>
/// <returns>List of signals generated by the indicator</returns>
List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues);
IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles);
}
}

View File

@@ -46,6 +46,17 @@ namespace Managing.Domain.Strategies
throw new NotImplementedException();
}
/// <summary>
/// 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.
/// </summary>
public virtual List<LightSignal> Run(HashSet<Candle> 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<Candle> candles)
{
throw new NotImplementedException();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,18 +29,13 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
return null;
}
var ticker = candles.First().Ticker;
try
{
var rsiResult = candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList();
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
if (candlesRsi.Count(c => c.Rsi > 0) == 0)
if (rsiResult.Count == 0)
return null;
GetLongSignals(candlesRsi, candles);
GetShortSignals(candlesRsi, candles);
ProcessRsiDivergenceConfirmSignals(rsiResult, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
@@ -50,6 +45,63 @@ public class RsiDivergenceConfirmIndicatorBase : IndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated RSI values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= Period)
{
return null;
}
try
{
// Use pre-calculated RSI values if available
List<RsiResult> rsiResult = null;
if (preCalculatedValues?.Rsi != null && preCalculatedValues.Rsi.Any())
{
// Filter pre-calculated RSI values to match the candles we're processing
var relevantCandles = candles.TakeLast(10 * Period.Value);
rsiResult = preCalculatedValues.Rsi
.Where(r => relevantCandles.Any(c => c.Date == r.Date))
.OrderBy(r => r.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (rsiResult == null || !rsiResult.Any())
{
return Run(candles);
}
ProcessRsiDivergenceConfirmSignals(rsiResult, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes RSI divergence confirmation signals based on price and RSI divergence patterns.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="rsiResult">List of RSI calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessRsiDivergenceConfirmSignals(List<RsiResult> rsiResult, HashSet<Candle> candles)
{
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
if (candlesRsi.Count(c => c.Rsi > 0) == 0)
return;
GetLongSignals(candlesRsi, candles);
GetShortSignals(candlesRsi, candles);
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
{
return new IndicatorsResultBase()

View File

@@ -32,18 +32,13 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
return null;
}
var ticker = candles.First().Ticker;
try
{
var rsiResult = candles.TakeLast(10 * Period.Value).GetRsi(Period.Value).ToList();
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
if (candlesRsi.Count(c => c.Rsi > 0) == 0)
if (rsiResult.Count == 0)
return null;
GetLongSignals(candlesRsi, candles);
GetShortSignals(candlesRsi, candles);
ProcessRsiDivergenceSignals(rsiResult, candles);
return Signals;
}
@@ -53,6 +48,63 @@ public class RsiDivergenceIndicatorBase : IndicatorBase
}
}
/// <summary>
/// Runs the indicator using pre-calculated RSI values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (!Period.HasValue || candles.Count <= Period)
{
return null;
}
try
{
// Use pre-calculated RSI values if available
List<RsiResult> rsiResult = null;
if (preCalculatedValues?.Rsi != null && preCalculatedValues.Rsi.Any())
{
// Filter pre-calculated RSI values to match the candles we're processing
var relevantCandles = candles.TakeLast(10 * Period.Value);
rsiResult = preCalculatedValues.Rsi
.Where(r => relevantCandles.Any(c => c.Date == r.Date))
.OrderBy(r => r.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (rsiResult == null || !rsiResult.Any())
{
return Run(candles);
}
ProcessRsiDivergenceSignals(rsiResult, candles);
return Signals;
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes RSI divergence signals based on price and RSI divergence patterns.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="rsiResult">List of RSI calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessRsiDivergenceSignals(List<RsiResult> rsiResult, HashSet<Candle> candles)
{
var candlesRsi = MapRsiToCandle(rsiResult, candles.TakeLast(10 * Period.Value));
if (candlesRsi.Count(c => c.Rsi > 0) == 0)
return;
GetLongSignals(candlesRsi, candles);
GetShortSignals(candlesRsi, candles);
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
{
return new IndicatorsResultBase()

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
}
}
/// <summary>
/// Runs the indicator using pre-calculated EMA values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= 2 * Period)
{
return null;
}
try
{
// Use pre-calculated EMA values if available
List<EmaResult> ema = null;
if (preCalculatedValues?.Ema != null && preCalculatedValues.Ema.Any())
{
// Filter pre-calculated EMA values to match the candles we're processing
ema = preCalculatedValues.Ema
.Where(e => candles.Any(c => c.Date == e.Date))
.OrderBy(e => e.Date)
.ToList();
}
// If no pre-calculated values or they don't match, fall back to regular calculation
if (ema == null || !ema.Any())
{
return Run(candles);
}
ProcessEmaTrendSignals(ema, candles);
return Signals.OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <summary>
/// Processes EMA trend signals based on price position relative to EMA line.
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="ema">List of EMA calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessEmaTrendSignals(List<EmaResult> ema, HashSet<Candle> candles)
{
var emaCandles = MapEmaToCandle(ema, candles.TakeLast(Period.Value));
if (emaCandles.Count == 0)
return;
var previousCandle = emaCandles[0];
foreach (var currentCandle in emaCandles.Skip(1))
{
if (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<Candle> candles)
{
return new IndicatorsResultBase()

View File

@@ -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
}
}
/// <summary>
/// Runs the indicator using pre-calculated StochRsi values for performance optimization.
/// </summary>
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= 10 * Period + 50)
{
return null;
}
try
{
// Use pre-calculated StochRsi values if available
List<StochRsiResult> 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;
}
}
/// <summary>
/// Processes StochRsi trend signals based on Signal line thresholds (20 and 80).
/// This method is shared between the regular Run() and optimized Run() methods.
/// </summary>
/// <param name="stochRsi">List of StochRsi calculation results</param>
/// <param name="candles">Candles to process</param>
private void ProcessStochRsiTrendSignals(List<StochRsiResult> stochRsi, HashSet<Candle> 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<Candle> candles)
{
return new IndicatorsResultBase()

View File

@@ -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<Candle> newCandles, LightScenario scenario,
Dictionary<string, LightSignal> previousSignal, int? loopbackPeriod = 1)
{
return GetSignal(newCandles, scenario, previousSignal, _defaultConfig, loopbackPeriod);
return GetSignal(newCandles, scenario, previousSignal, _defaultConfig, loopbackPeriod, null);
}
public static LightSignal GetSignal(HashSet<Candle> newCandles, LightScenario scenario,
Dictionary<string, LightSignal> previousSignal, int? loopbackPeriod,
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
{
return GetSignal(newCandles, scenario, previousSignal, _defaultConfig, loopbackPeriod, preCalculatedIndicatorValues);
}
public static LightSignal GetSignal(HashSet<Candle> newCandles, LightScenario lightScenario,
Dictionary<string, LightSignal> previousSignal, IndicatorComboConfig config, int? loopbackPeriod = 1)
{
return GetSignal(newCandles, lightScenario, previousSignal, config, loopbackPeriod, null);
}
public static LightSignal GetSignal(HashSet<Candle> newCandles, LightScenario lightScenario,
Dictionary<string, LightSignal> previousSignal, IndicatorComboConfig config, int? loopbackPeriod,
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
{
var signalOnCandles = new List<LightSignal>();
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<LightSignal> 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)
{