using Managing.Application.Abstractions.Repositories; using Managing.Application.Synth; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.MoneyManagements; using Managing.Domain.Risk; using Managing.Domain.Strategies; using Managing.Domain.Synth.Models; using Managing.Domain.Users; using Microsoft.Extensions.Logging; using Moq; using Xunit; using Xunit.Abstractions; using static Managing.Common.Enums; namespace Managing.Infrastructure.Tests; public class SynthPredictionTests { private readonly ITestOutputHelper _testOutputHelper; public SynthPredictionTests(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } /// /// Helper method to create a test signal with realistic candle data /// private static Signal CreateTestSignal(Ticker ticker, TradeDirection direction, decimal price, DateTime? date = null) { var signalDate = date ?? DateTime.UtcNow; var candle = new Candle { Date = signalDate, Open = price * 0.999m, High = price * 1.001m, Low = price * 0.998m, Close = price, Volume = price * 1000m }; return new Signal( ticker: ticker, direction: direction, confidence: Confidence.Medium, // Will be updated by validation candle: candle, date: signalDate, exchange: TradingExchanges.GmxV2, indicatorType: IndicatorType.Stc, signalType: SignalType.Signal, indicatorName: "TestIndicator", user: new User { Name = "TestUser" } ); } [Fact] public async Task GetProbabilityOfTargetPriceAsync_ShouldReturnValidProbability_ForBTC_RealAPI() { // Arrange - Static values for testing const decimal currentBtcPrice = 102000m; // Current BTC price at $102k const decimal takeProfitPrice = currentBtcPrice * 1.02m; // 2% TP = $104,040 const decimal stopLossPrice = currentBtcPrice * 0.99m; // 1% SL = $100,980 const int timeHorizonHours = 24; // 24 hour forecast Console.WriteLine($"šŸš€ Starting Synth API Test for BTC at ${currentBtcPrice:N0}"); // Create real API client and service var httpClient = new HttpClient(); var logger = new TestLogger(); var synthApiClient = new SynthApiClient(httpClient, new TestLogger()); var mockSynthRepository = new Mock(); var synthPredictionService = new SynthPredictionService(synthApiClient, mockSynthRepository.Object, logger); // Create configuration for enabled Synth API var config = new SynthConfiguration { IsEnabled = true, TopMinersCount = 5, // Use fewer miners for faster testing TimeIncrement = 300, // 5 minutes (supported by Synth API) DefaultTimeLength = timeHorizonHours * 3600, // 24 hours in seconds MaxLiquidationProbability = 0.10m, PredictionCacheDurationMinutes = 1 // Short cache for testing }; // Act & Assert - Test Take Profit probability (upward movement for LONG) try { Console.WriteLine("šŸ” Fetching Take Profit probability from Synth API..."); var takeProfitProbability = await synthPredictionService.GetProbabilityOfTargetPriceAsync( asset: "BTC", currentPrice: currentBtcPrice, targetPrice: takeProfitPrice, timeHorizonSeconds: timeHorizonHours * 3600, isLongPosition: false, // For TP, we want upward movement (opposite of liquidation direction) config: config); Console.WriteLine($"šŸŽÆ Take Profit Analysis (2% gain):"); Console.WriteLine($"Current Price: ${currentBtcPrice:N0}"); Console.WriteLine($"Target Price: ${takeProfitPrice:N0}"); Console.WriteLine($"Probability: {takeProfitProbability:P2}"); Assert.True(takeProfitProbability >= 0m && takeProfitProbability <= 1m, "Take profit probability should be between 0 and 1"); } catch (Exception ex) { Console.WriteLine($"āŒ Take Profit test failed: {ex.Message}"); Console.WriteLine("āš ļø Skipping Take Profit test due to API issue"); } // Act & Assert - Test Stop Loss probability (downward movement for LONG) try { Console.WriteLine("\nšŸ” Fetching Stop Loss probability from Synth API..."); var stopLossProbability = await synthPredictionService.GetProbabilityOfTargetPriceAsync( asset: "BTC", currentPrice: currentBtcPrice, targetPrice: stopLossPrice, timeHorizonSeconds: timeHorizonHours * 3600, isLongPosition: true, // For SL in long position, we check downward movement config: config); Console.WriteLine($"šŸ›‘ Stop Loss Analysis (1% loss):"); Console.WriteLine($"Current Price: ${currentBtcPrice:N0}"); Console.WriteLine($"Stop Loss Price: ${stopLossPrice:N0}"); Console.WriteLine($"Liquidation Risk: {stopLossProbability:P2}"); Assert.True(stopLossProbability >= 0m && stopLossProbability <= 1m, "Stop loss probability should be between 0 and 1"); // Risk assessment - typical risk thresholds if (stopLossProbability > 0.20m) { Console.WriteLine("āš ļø HIGH RISK: Liquidation probability exceeds 20%"); } else if (stopLossProbability > 0.10m) { Console.WriteLine("⚔ MODERATE RISK: Liquidation probability between 10-20%"); } else { Console.WriteLine("āœ… LOW RISK: Liquidation probability below 10%"); } } catch (Exception ex) { Console.WriteLine($"āŒ Stop Loss test failed: {ex.Message}"); Console.WriteLine("āš ļø Skipping Stop Loss test due to API issue"); } Console.WriteLine($"\nšŸ“Š Money Management Summary:"); Console.WriteLine($"Position: LONG BTC"); Console.WriteLine($"Entry: ${currentBtcPrice:N0}"); Console.WriteLine($"Take Profit: ${takeProfitPrice:N0} (+2.00%)"); Console.WriteLine($"Stop Loss: ${stopLossPrice:N0} (-1.00%)"); Console.WriteLine($"Risk/Reward Ratio: 1:2"); Console.WriteLine($"Time Horizon: {timeHorizonHours} hours"); Console.WriteLine("šŸ Test completed!"); } [Fact] public async Task ValidateSignalAsync_ShouldUseCustomThresholds_ForSignalFiltering_RealAPI() { // Arrange - Static values for custom threshold testing const decimal currentBtcPrice = 107300m; // Current BTC price at $105,700 Console.WriteLine($"šŸ”§ Starting RiskManagement Configuration Test for BTC at ${currentBtcPrice:N0}"); // Create real API client and service var httpClient = new HttpClient(); var logger = new TestLogger(); var synthApiClient = new SynthApiClient(httpClient, new TestLogger()); var mockSynthRepository = new Mock(); var synthPredictionService = new SynthPredictionService(synthApiClient, mockSynthRepository.Object, logger); // Define test scenarios for both LONG and SHORT signals var signalDirections = new[] { new { Direction = TradeDirection.Long, Name = "LONG" }, new { Direction = TradeDirection.Short, Name = "SHORT" } }; // Define RiskManagement configurations to test var riskConfigs = new[] { new { Name = "Default (Moderate)", RiskConfig = new RiskManagement { AdverseProbabilityThreshold = 0.25m, // 25% - balanced threshold FavorableProbabilityThreshold = 0.30m, // 30% - reasonable expectation RiskAversion = 1.5m, // Moderate risk aversion KellyMinimumThreshold = 0.02m, // 2% - practical minimum KellyMaximumCap = 0.20m, // 20% - reasonable maximum KellyFractionalMultiplier = 0.75m, // 75% of Kelly (conservative) RiskTolerance = RiskToleranceLevel.Moderate } }, new { Name = "Conservative", RiskConfig = new RiskManagement { AdverseProbabilityThreshold = 0.20m, // 20% - stricter threshold FavorableProbabilityThreshold = 0.40m, // 40% - higher TP expectation RiskAversion = 2.0m, // Higher risk aversion KellyMinimumThreshold = 0.03m, // 3% - higher minimum KellyMaximumCap = 0.15m, // 15% - lower maximum KellyFractionalMultiplier = 0.50m, // 50% of Kelly (very conservative) RiskTolerance = RiskToleranceLevel.Conservative } }, new { Name = "Aggressive", RiskConfig = new RiskManagement { AdverseProbabilityThreshold = 0.35m, // 35% - more permissive FavorableProbabilityThreshold = 0.25m, // 25% - lower TP barrier RiskAversion = 1.0m, // Lower risk aversion KellyMinimumThreshold = 0.01m, // 1% - lower minimum KellyMaximumCap = 0.30m, // 30% - higher maximum KellyFractionalMultiplier = 1.0m, // 100% of Kelly (full Kelly) RiskTolerance = RiskToleranceLevel.Aggressive } }, new { Name = "Moderate-Plus", RiskConfig = new RiskManagement { AdverseProbabilityThreshold = 0.30m, // 30% - slightly more permissive FavorableProbabilityThreshold = 0.35m, // 35% - balanced expectation RiskAversion = 1.2m, // Slightly less risk-averse KellyMinimumThreshold = 0.015m, // 1.5% - practical minimum KellyMaximumCap = 0.25m, // 25% - reasonable maximum KellyFractionalMultiplier = 0.85m, // 85% of Kelly RiskTolerance = RiskToleranceLevel.Moderate } }, new { Name = "Risk-Focused", RiskConfig = new RiskManagement { AdverseProbabilityThreshold = 0.18m, // 18% - tight risk control FavorableProbabilityThreshold = 0.45m, // 45% - high TP requirement RiskAversion = 2.5m, // High risk aversion KellyMinimumThreshold = 0.025m, // 2.5% - higher minimum KellyMaximumCap = 0.12m, // 12% - very conservative maximum KellyFractionalMultiplier = 0.40m, // 40% of Kelly (very conservative) RiskTolerance = RiskToleranceLevel.Conservative } }, new { Name = "Ultra-Conservative", RiskConfig = new RiskManagement { AdverseProbabilityThreshold = 0.16m, // 16% - very strict threshold (should trigger some LOWs) FavorableProbabilityThreshold = 0.60m, // 60% - very high TP requirement RiskAversion = 3.5m, // Very high risk aversion KellyMinimumThreshold = 0.04m, // 4% - high minimum barrier KellyMaximumCap = 0.08m, // 8% - very low maximum (forces heavy capping) KellyFractionalMultiplier = 0.25m, // 25% of Kelly (ultra conservative) RiskTolerance = RiskToleranceLevel.Conservative } }, new { Name = "Paranoid-Blocking", RiskConfig = new RiskManagement { AdverseProbabilityThreshold = 0.12m, // 12% - very strict (should block 22-25% SL signals) FavorableProbabilityThreshold = 0.60m, // 60% - very high TP requirement RiskAversion = 4.0m, // Extremely high risk aversion KellyMinimumThreshold = 0.05m, // 5% - very high minimum KellyMaximumCap = 0.06m, // 6% - extremely conservative maximum KellyFractionalMultiplier = 0.15m, // 15% of Kelly (extremely conservative) RiskTolerance = RiskToleranceLevel.Conservative, SignalValidationTimeHorizonHours = 24 } }, new { Name = "Extreme-Blocking", RiskConfig = new RiskManagement { AdverseProbabilityThreshold = 0.08m, // 8% - extremely strict (will block 22-25% SL signals) FavorableProbabilityThreshold = 0.70m, // 70% - extremely high TP requirement RiskAversion = 5.0m, // Maximum risk aversion KellyMinimumThreshold = 0.08m, // 8% - very high minimum KellyMaximumCap = 0.05m, // 5% - extremely small maximum KellyFractionalMultiplier = 0.10m, // 10% of Kelly (ultra-conservative) RiskTolerance = RiskToleranceLevel.Conservative, SignalValidationTimeHorizonHours = 24 } } }; // Store results for summary var testResults = new Dictionary>(); // Test each RiskManagement configuration with both LONG and SHORT signals foreach (var configTest in riskConfigs) { Console.WriteLine($"\nšŸ“Š Testing {configTest.Name})"); testResults[configTest.Name] = new Dictionary(); // Create bot configuration with the specific RiskManagement var botConfig = new TradingBotConfig { BotTradingBalance = 50000m, // $50k trading balance for realistic utility calculations Timeframe = Timeframe.FifteenMinutes, UseSynthApi = true, UseForSignalFiltering = true, UseForPositionSizing = true, UseForDynamicStopLoss = false, RiskManagement = configTest.RiskConfig, // Use the specific risk configuration MoneyManagement = new MoneyManagement { Name = "Test Money Management", StopLoss = 0.02m, // 2% stop loss TakeProfit = 0.022m, // 4% take profit (1:2 risk/reward ratio) Leverage = 10m, Timeframe = Timeframe.FifteenMinutes } }; foreach (var signal in signalDirections) { try { Console.WriteLine($" šŸŽÆ {signal.Name} Signal Test"); // Create a test signal for this direction var testSignal = CreateTestSignal(Ticker.BTC, signal.Direction, currentBtcPrice); var result = await synthPredictionService.ValidateSignalAsync( signal: testSignal, currentPrice: currentBtcPrice, botConfig: botConfig, isBacktest: false, customThresholds: null); // No custom thresholds - use RiskManagement config testResults[configTest.Name][signal.Name] = result; Console.WriteLine($" šŸŽÆ Confidence: {result.Confidence}"); Console.WriteLine( $" šŸ“Š SL Risk: {result.StopLossProbability:P2} | TP Prob: {result.TakeProfitProbability:P2}"); Console.WriteLine( $" šŸŽ² TP/SL Ratio: {result.TpSlRatio:F2}x | Win/Loss: {result.WinLossRatio:F2}:1"); Console.WriteLine($" šŸ’° Expected Value: ${result.ExpectedMonetaryValue:F2}"); Console.WriteLine($" 🧮 Expected Utility: {result.ExpectedUtility:F4}"); Console.WriteLine( $" šŸŽÆ Kelly: {result.KellyFraction:P2} (Capped: {result.KellyCappedFraction:P2})"); Console.WriteLine($" šŸ“Š Kelly Assessment: {result.KellyAssessment}"); Console.WriteLine($" āœ… Kelly Favorable: {result.IsKellyFavorable(configTest.RiskConfig)}"); Console.WriteLine($" 🚫 Blocked: {result.IsBlocked}"); // Debug: Show actual probability values and threshold comparison var adverseThreshold = configTest.RiskConfig.AdverseProbabilityThreshold; Console.WriteLine( $" šŸ” DEBUG - SL: {result.StopLossProbability:F4} | TP: {result.TakeProfitProbability:F4} | Threshold: {adverseThreshold:F4}"); Console.WriteLine( $" šŸ” DEBUG - SL > Threshold: {result.StopLossProbability > adverseThreshold} | TP > SL: {result.TakeProfitProbability > result.StopLossProbability}"); // Assert that the method works with RiskManagement configuration Assert.True(Enum.IsDefined(typeof(Confidence), result.Confidence), $"{configTest.Name} - {signal.Name} signal should return a valid Confidence level"); // Assert that Kelly calculations were performed Assert.True(result.KellyFraction >= 0, "Kelly fraction should be non-negative"); Assert.True(result.KellyCappedFraction >= 0, "Capped Kelly fraction should be non-negative"); // Assert that Expected Utility calculations were performed Assert.True(result.TradingBalance > 0, "Trading balance should be set from bot config"); Assert.Equal(botConfig.BotTradingBalance, result.TradingBalance); } catch (Exception ex) { Console.WriteLine($" āŒ {signal.Name} signal test failed: {ex.Message}"); // Create a fallback result for error cases testResults[configTest.Name][signal.Name] = new SignalValidationResult { Confidence = Confidence.High, // Default to high confidence on error ValidationContext = $"Error: {ex.Message}" }; } } } // Display comprehensive results summary Console.WriteLine($"\nšŸ“ˆ Comprehensive RiskManagement Configuration Test Summary:"); Console.WriteLine($"Asset: BTC | Price: ${currentBtcPrice:N0} | Trading Balance: ${50000:N0}"); Console.WriteLine($"Stop Loss: 2.0% | Take Profit: 4.0% | Risk/Reward Ratio: 1:2.0"); _testOutputHelper.WriteLine($"\nšŸŽÆ Results Matrix:"); _testOutputHelper.WriteLine( $"{"Configuration",-20} {"LONG Confidence",-15} {"LONG Kelly",-12} {"SHORT Confidence",-16} {"SHORT Kelly",-12}"); _testOutputHelper.WriteLine(new string('-', 85)); foreach (var configTest in riskConfigs) { var longResult = testResults[configTest.Name].GetValueOrDefault("LONG"); var shortResult = testResults[configTest.Name].GetValueOrDefault("SHORT"); var longConf = longResult?.Confidence ?? Confidence.None; var shortConf = shortResult?.Confidence ?? Confidence.None; var longKelly = longResult?.KellyCappedFraction ?? 0m; var shortKelly = shortResult?.KellyCappedFraction ?? 0m; _testOutputHelper.WriteLine( $"{configTest.Name,-20} {GetConfidenceDisplay(longConf),-15} {longKelly,-12:P1} {GetConfidenceDisplay(shortConf),-16} {shortKelly,-12:P1}"); } // Display detailed ValidationContext for each configuration and direction Console.WriteLine($"\nšŸ“Š Detailed Analysis Results:"); Console.WriteLine(new string('=', 120)); foreach (var configTest in riskConfigs) { Console.WriteLine($"\nšŸ”§ {configTest.Name}"); Console.WriteLine(new string('-', 80)); var longResult = testResults[configTest.Name].GetValueOrDefault("LONG"); var shortResult = testResults[configTest.Name].GetValueOrDefault("SHORT"); if (longResult != null) { Console.WriteLine($"šŸ“ˆ LONG Signal Analysis:"); Console.WriteLine($" Context: {longResult.ValidationContext ?? "N/A"}"); Console.WriteLine( $" Confidence: {GetConfidenceDisplay(longResult.Confidence)} | Blocked: {longResult.IsBlocked}"); Console.WriteLine( $" Kelly Assessment: {longResult.KellyAssessment} | Kelly Favorable: {longResult.IsKellyFavorable(configTest.RiskConfig)}"); if (longResult.TradingBalance > 0) { Console.WriteLine( $" Trading Balance: ${longResult.TradingBalance:N0} | Risk Assessment: {longResult.GetUtilityRiskAssessment()}"); } } else { Console.WriteLine($"šŸ“ˆ LONG Signal Analysis: ERROR - No result available"); } Console.WriteLine(); // Empty line for separation if (shortResult != null) { Console.WriteLine($"šŸ“‰ SHORT Signal Analysis:"); Console.WriteLine($" Context: {shortResult.ValidationContext ?? "N/A"}"); Console.WriteLine( $" Confidence: {GetConfidenceDisplay(shortResult.Confidence)} | Blocked: {shortResult.IsBlocked}"); Console.WriteLine( $" Kelly Assessment: {shortResult.KellyAssessment} | Kelly Favorable: {shortResult.IsKellyFavorable(configTest.RiskConfig)}"); if (shortResult.TradingBalance > 0) { Console.WriteLine( $" Trading Balance: ${shortResult.TradingBalance:N0} | Risk Assessment: {shortResult.GetUtilityRiskAssessment()}"); } } else { Console.WriteLine($"šŸ“‰ SHORT Signal Analysis: ERROR - No result available"); } } Console.WriteLine($"\nšŸ“Š Risk Configuration Analysis:"); Console.WriteLine($"• Default: Balanced 20% adverse threshold, 1% Kelly minimum"); Console.WriteLine($"• Conservative: Strict 15% adverse, 2% Kelly min, half-Kelly multiplier"); Console.WriteLine($"• Aggressive: Permissive 30% adverse, 0.5% Kelly min, full Kelly"); Console.WriteLine($"• Custom Permissive: Very permissive 35% adverse, low barriers"); Console.WriteLine($"• Custom Strict: Very strict 10% adverse, high barriers, conservative sizing"); Console.WriteLine($"\nšŸ’” Key Insights:"); Console.WriteLine($"• Conservative configs should block more signals (lower confidence)"); Console.WriteLine($"• Aggressive configs should allow more signals (higher confidence)"); Console.WriteLine($"• Kelly fractions should vary based on risk tolerance settings"); Console.WriteLine($"• Expected Utility should reflect trading balance and risk aversion"); // Verify that we have results for all configurations and directions foreach (var configTest in riskConfigs) { foreach (var signal in signalDirections) { Assert.True(testResults.ContainsKey(configTest.Name) && testResults[configTest.Name].ContainsKey(signal.Name), $"Should have test result for {configTest.Name} - {signal.Name}"); var result = testResults[configTest.Name][signal.Name]; Assert.NotNull(result); Assert.True(result.TradingBalance > 0, "Trading balance should be populated from bot config"); } } Console.WriteLine("šŸ Comprehensive RiskManagement Configuration Test completed!"); } /// /// Helper method to display confidence levels with emojis /// /// Confidence level /// Formatted confidence display private static string GetConfidenceDisplay(Confidence confidence) { return confidence switch { Confidence.High => "🟢 HIGH", Confidence.Medium => "🟔 MEDIUM", Confidence.Low => "🟠 LOW", Confidence.None => "šŸ”“ NONE", _ => "ā“ UNKNOWN" }; } } // Simple test logger implementation public class TestLogger : ILogger { public IDisposable BeginScope(TState state) => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { // Silent logger for tests - output goes to Console.WriteLine } }