Improve perf for backtests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user