Files
managing-apps/src/Managing.Infrastructure.Tests/SynthPredictionTests.cs
Oda 422fecea7b Postgres (#30)
* Add postgres

* Migrate users

* Migrate geneticRequest

* Try to fix Concurrent call

* Fix asyncawait

* Fix async and concurrent

* Migrate backtests

* Add cache for user by address

* Fix backtest migration

* Fix not open connection

* Fix backtest command error

* Fix concurrent

* Fix all concurrency

* Migrate TradingRepo

* Fix scenarios

* Migrate statistic repo

* Save botbackup

* Add settings et moneymanagement

* Add bot postgres

* fix a bit more backups

* Fix bot model

* Fix loading backup

* Remove cache market for read positions

* Add workers to postgre

* Fix workers api

* Reduce get Accounts for workers

* Migrate synth to postgre

* Fix backtest saved

* Remove mongodb

* botservice decorrelation

* Fix tradingbot scope call

* fix tradingbot

* fix concurrent

* Fix scope for genetics

* Fix account over requesting

* Fix bundle backtest worker

* fix a lot of things

* fix tab backtest

* Remove optimized moneymanagement

* Add light signal to not use User and too much property

* Make money management lighter

* insert indicators to awaitable

* Migrate add strategies to await

* Refactor scenario and indicator retrieval to use asynchronous methods throughout the application

* add more async await

* Add services

* Fix and clean

* Fix bot a bit

* Fix bot and add message for cooldown

* Remove fees

* Add script to deploy db

* Update dfeeploy script

* fix script

* Add idempotent script and backup

* finish script migration

* Fix did user and agent name on start bot
2025-07-27 20:42:17 +07:00

539 lines
25 KiB
C#

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;
}
/// <summary>
/// Helper method to create a test signal with realistic candle data
/// </summary>
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<SynthPredictionService>();
var synthApiClient = new SynthApiClient(httpClient, new TestLogger<SynthApiClient>());
var mockSynthRepository = new Mock<ISynthRepository>();
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<SynthPredictionService>();
var synthApiClient = new SynthApiClient(httpClient, new TestLogger<SynthApiClient>());
var mockSynthRepository = new Mock<ISynthRepository>();
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<string, Dictionary<string, SignalValidationResult>>();
// 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<string, SignalValidationResult>();
// 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!");
}
/// <summary>
/// Helper method to display confidence levels with emojis
/// </summary>
/// <param name="confidence">Confidence level</param>
/// <returns>Formatted confidence display</returns>
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<T> : ILogger<T>
{
public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
Func<TState, Exception, string> formatter)
{
// Silent logger for tests - output goes to Console.WriteLine
}
}