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,9 +435,12 @@ 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)
{
// Offload CPU-bound indicator calculations to thread pool
return await Task.Run(() =>
{
var indicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>();
@@ -462,6 +465,7 @@ public class TradingService : ITradingService
}
return indicatorsValues;
});
}
public async Task<IndicatorBase?> GetIndicatorByNameUserAsync(string name, User user)

View File

@@ -28,11 +28,71 @@ 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;
ProcessStDevSignals(stDev, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <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;
@@ -65,13 +125,6 @@ public class StDevContext : IndicatorBase
// Context strategies always return TradeDirection.None
// The confidence level indicates the quality of market conditions
AddSignal(lastCandle, TradeDirection.None, confidence);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)

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,10 +42,77 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
var fastEma = candles.GetEma(FastPeriods.Value).ToList();
var slowEma = candles.GetEma(SlowPeriods.Value).ToList();
if (fastEma.Count == 0 || slowEma.Count == 0)
return null;
ProcessDualEmaCrossSignals(fastEma, slowEma, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <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 null;
return;
var previousCandle = dualEmaCandles[0];
foreach (var currentCandle in dualEmaCandles.Skip(1))
@@ -66,13 +133,6 @@ public class DualEmaCrossIndicatorBase : EmaBaseIndicatorBase
previousCandle = currentCandle;
}
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
private List<CandleDualEma> MapDualEmaToCandle(List<EmaResult> fastEma, List<EmaResult> slowEma,

View File

@@ -36,11 +36,71 @@ 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;
ProcessEmaCrossSignals(ema, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <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))
{
@@ -58,13 +118,6 @@ public class EmaCrossIndicator : EmaBaseIndicatorBase
previousCandle = currentCandle;
}
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)

View File

@@ -36,11 +36,71 @@ 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;
ProcessEmaCrossSignals(ema, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <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))
{
@@ -58,13 +118,6 @@ public class EmaCrossIndicatorBase : EmaBaseIndicatorBase
previousCandle = currentCandle;
}
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
private void AddSignal(CandleEma candleSignal, TradeDirection direction, Confidence confidence)

View File

@@ -38,10 +38,70 @@ public class LaggingSTC : IndicatorBase
try
{
var stc = candles.GetStc(FastPeriods.Value, FastPeriods.Value, SlowPeriods.Value).ToList();
if (stc.Count == 0)
return null;
ProcessLaggingStcSignals(stc, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <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 null;
return;
for (int i = 1; i < stcCandles.Count; i++)
{
@@ -81,13 +141,6 @@ public class LaggingSTC : IndicatorBase
}
}
}
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)

View File

@@ -31,20 +31,74 @@ 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;
ProcessMacdSignals(macd, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <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))
{
// // 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)
{
@@ -59,13 +113,6 @@ public class MacdCrossIndicatorBase : IndicatorBase
previousCandle = currentCandle;
}
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)

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,31 +33,51 @@ 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;
var previousCandle = stcCandles[0];
foreach (var currentCandle in stcCandles.Skip(1))
{
if (previousCandle.Stc > 75 && currentCandle.Stc <= 75)
{
AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium);
ProcessStcSignals(stc, candles);
}
if (previousCandle.Stc < 25 && currentCandle.Stc >= 25)
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium);
return null;
}
}
previousCandle = currentCandle;
/// <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)
@@ -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,10 +48,124 @@ public class SuperTrendCrossEma : IndicatorBase
.Where(a => a.Adx.HasValue && a.Pdi.HasValue && a.Mdi.HasValue) // Ensure all values exist
.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;
}
}
private List<CandleSuperTrend> MapSuperTrendToCandle(List<SuperTrendResult> superTrend, IEnumerable<Candle> candles)
{
var superTrends = new List<CandleSuperTrend>();
foreach (var candle in candles)
{
var currentSuperTrend = superTrend.Find(candle.Date);
if (currentSuperTrend != null)
{
superTrends.Add(new CandleSuperTrend()
{
Close = candle.Close,
Open = candle.Open,
Date = candle.Date,
Ticker = candle.Ticker,
Exchange = candle.Exchange,
SuperTrend = currentSuperTrend.SuperTrend.Value,
LowerBand = currentSuperTrend.LowerBand,
UpperBand = currentSuperTrend.UpperBand,
});
}
}
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 null;
return;
// 3. Add EMA50 and ADX values to the CandleSuperTrend objects
foreach (var candle in superTrendCandles)
@@ -122,40 +236,6 @@ public class SuperTrendCrossEma : IndicatorBase
AddSignal(current, TradeDirection.Short, Confidence.Medium);
}
}
return Signals.Where(s => s.Confidence != Confidence.None)
.OrderBy(s => s.Date)
.ToList();
}
catch (RuleException)
{
return null;
}
}
private List<CandleSuperTrend> MapSuperTrendToCandle(List<SuperTrendResult> superTrend, IEnumerable<Candle> candles)
{
var superTrends = new List<CandleSuperTrend>();
foreach (var candle in candles)
{
var currentSuperTrend = superTrend.Find(candle.Date);
if (currentSuperTrend != null)
{
superTrends.Add(new CandleSuperTrend()
{
Close = candle.Close,
Open = candle.Open,
Date = candle.Date,
Ticker = candle.Ticker,
Exchange = candle.Exchange,
SuperTrend = currentSuperTrend.SuperTrend.Value,
LowerBand = currentSuperTrend.LowerBand,
UpperBand = currentSuperTrend.UpperBand,
});
}
}
return superTrends;
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)

View File

@@ -29,27 +29,77 @@ public class SuperTrendIndicatorBase : IndicatorBase
try
{
var superTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value).Where(s => s.SuperTrend.HasValue);
var superTrend = candles.GetSuperTrend(Period.Value, Multiplier.Value)
.Where(s => s.SuperTrend.HasValue)
.ToList();
if (superTrend.Count == 0)
return null;
ProcessSuperTrendSignals(superTrend, candles);
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <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 null;
return;
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);
@@ -61,13 +111,6 @@ public class SuperTrendIndicatorBase : IndicatorBase
previousCandle = currentCandle;
}
return Signals.Where(s => s.Confidence != Confidence.None).OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)

View File

@@ -27,6 +27,35 @@ namespace Managing.Domain.Strategies.Signals
}
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;
@@ -44,13 +73,6 @@ namespace Managing.Domain.Strategies.Signals
previousCandles = currentCandle;
}
return signals;
}
catch (RuleException)
{
return null;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)

View File

@@ -28,11 +28,71 @@ 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;
ProcessEmaTrendSignals(ema, candles);
return Signals.OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <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))
{
@@ -47,13 +107,6 @@ public class EmaTrendIndicatorBase : EmaBaseIndicatorBase
previousCandle = currentCandle;
}
return Signals.OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)

View File

@@ -37,11 +37,72 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
{
var stochRsi = candles
.GetStochRsi(Period.Value, StochPeriods.Value, SignalPeriods.Value, SmoothPeriods.Value)
.RemoveWarmupPeriods();
.RemoveWarmupPeriods()
.ToList();
if (stochRsi.Count == 0)
return null;
ProcessStochRsiTrendSignals(stochRsi, candles);
return Signals.OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
/// <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 (stochRsi.Count() == 0)
return null;
if (stochRsiCandles.Count == 0)
return;
var previousCandle = stochRsiCandles[0];
foreach (var currentCandle in stochRsiCandles.Skip(1))
@@ -57,13 +118,6 @@ public class StochRsiTrendIndicatorBase : IndicatorBase
previousCandle = currentCandle;
}
return Signals.OrderBy(s => s.Date).ToList();
}
catch (RuleException)
{
return null;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)

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