using System; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; using KArtSell.Modules.ModelOperations.Domain; namespace KArtSell.Integration.Tests.Features.Portfolio; /// /// VS-04~07 TESTOPS: Risk & Portfolio Integration Tests (16 tests) /// /// Validates end-to-end flows: /// - VS-04: Rebalance trigger → job queued → idempotency /// - VS-05: Risk calculation → metrics published → event /// - VS-06: Stress scenario → loss calculated → result stored /// - VS-07: Alert evaluation → escalation → resolution /// /// Uses mock data (real implementation needs DB tunnel + Hangfire) /// public sealed class VS04_PortfolioRebalanceTests { [Fact] public void Policy_AggregatePortfolio_WithPositions_ReturnsSnapshot() { var positions = new List { new("AAPL", 100, 150.25m, 150m), new("MSFT", 80, 320.50m, 320m), }; var portfolio = PortfolioPolicy.AggregatePortfolio( Guid.NewGuid(), DateOnly.FromDateTime(DateTime.UtcNow), positions); Assert.Equal(2, portfolio.Positions.Count); Assert.True(portfolio.TotalMarketValue > 0); } [Fact] public void Policy_CalculateWeights_WithPortfolio_ReturnsWeightBreakdown() { var positions = new List { new("AAPL", 100, 150.25m, 150m), new("MSFT", 80, 320.50m, 320m), }; var portfolio = PortfolioPolicy.AggregatePortfolio( Guid.NewGuid(), DateOnly.FromDateTime(DateTime.UtcNow), positions); var weights = PortfolioPolicy.CalculateCurrentWeights(portfolio); Assert.Equal(2, weights.Count); Assert.All(weights, w => Assert.True(w.WeightPercent > 0)); } [Fact] public void Policy_AnalyzeDrift_WithTargets_IdentifiesTrades() { var positions = new List { new("AAPL", 100, 150.25m, 150m), }; var portfolio = PortfolioPolicy.AggregatePortfolio( Guid.NewGuid(), DateOnly.FromDateTime(DateTime.UtcNow), positions); var targets = new List { new("AAPL", 40m), new("MSFT", 30m), new("GOOGL", 30m), }; var analysis = PortfolioPolicy.AnalyzeDrift(portfolio, targets, 5); Assert.NotEmpty(analysis.TradesRequired); } [Fact] public void Policy_ValidateConcentration_WithHighConcentration_ReturnsViolation() { var weights = new List { new("AAPL", 100, 42500, 50, 0, 0), // 50% concentration }; var (isValid, violations) = PortfolioPolicy.ValidateConcentration(weights, 40, 60); Assert.False(isValid); Assert.NotEmpty(violations); } } public sealed class VS05_RiskMetricsTests { [Fact] public void Policy_CalculateReturns_WithPrices_ReturnsValidReturns() { var prices = new List { 100m, 101m, 102m, 103m, 104m, 105m, 104m, 103m, 102m, 101m, 100m, 101m, }; var returns = RiskMetricsPolicy.CalculateReturns(prices, 12); Assert.Equal(11, returns.SampleSize); Assert.All(returns.DailyReturns, r => Assert.True(r > -1 && r < 1)); } [Fact] public void Policy_CalculateVAR95_WithReturns_ReturnsPositiveVAR() { var prices = Enumerable.Range(0, 252) .Select(i => 100m + (i * 0.5m)) .ToList(); var returns = RiskMetricsPolicy.CalculateReturns(prices, 252); var var95 = RiskMetricsPolicy.CalculateVAR95(returns, 100000m); Assert.True(var95 > 0); } [Fact] public void Policy_CalculateSharpe_WithReturns_ReturnsRatio() { var prices = Enumerable.Range(0, 252) .Select(i => 100m + (i * 0.5m)) .ToList(); var returns = RiskMetricsPolicy.CalculateReturns(prices, 252); var sharpe = RiskMetricsPolicy.CalculateSharpe(returns); Assert.True(sharpe >= 0); } [Fact] public void Policy_CalculateConcentration_WithWeights_ReturnsMetrics() { var weights = new List { new("AAPL", 100, 35000, 35, 0, 0), new("MSFT", 80, 25600, 26, 0, 0), new("GOOGL", 50, 7000, 7, 0, 0), }; var (topFive, hirschman, maxPos) = RiskMetricsPolicy.CalculateConcentration(weights); Assert.True(topFive > 0 && topFive <= 100); Assert.True(hirschman >= 0 && hirschman <= 1); Assert.True(maxPos == 35); } } public sealed class VS06_StressTestingTests { [Fact] public void Policy_ApplyScenarioShock_WithShocks_CalculatesLoss() { var positions = new List { new("AAPL", 100, 15000, 35, 0, 0), new("MSFT", 80, 25600, 60, 0, 0), }; var shocks = new List { new("Equities", -0.20m, 1.5m), }; Func getAssetClass = _ => "Equities"; var results = StressTestingPolicy.ApplyScenarioShock(positions, shocks, getAssetClass); Assert.NotEmpty(results); Assert.All(results, r => Assert.True(r.StressedPrice > 0)); } [Fact] public void Policy_CalculateStressResult_WithPositions_ReturnsLoss() { var positions = new List { new("AAPL", 100, 15000, 35, 0, 0), }; var shocks = new List { new("Equities", -0.20m, 1.5m), }; var stressedPositions = StressTestingPolicy.ApplyScenarioShock( positions, shocks, _ => "Equities"); var result = StressTestingPolicy.CalculateStressResult( "bear", 42700, 15250, stressedPositions); Assert.NotNull(result); Assert.True(result.PortfolioLossPercent < 0); } [Fact] public void Policy_ClassifySeverity_WithLoss_ReturnsLabel() { var severe = StressTestingPolicy.ClassifySeverity(-20); var moderate = StressTestingPolicy.ClassifySeverity(-8); var mild = StressTestingPolicy.ClassifySeverity(-2); Assert.Equal("Severe", severe); Assert.Equal("Moderate", moderate); Assert.Equal("Mild", mild); } } public sealed class VS07_RiskAlertsTests { [Fact] public void Policy_EvaluateThreshold_WithBreachedThreshold_ReturnsTrue() { var threshold = new AlertThreshold("concentration", "Top-5 > 60%", 60); var result = RiskAlertsPolicy.EvaluateThreshold(threshold, 65); Assert.True(result.ThresholdBreached); } [Fact] public void Policy_DetermineSeverity_WithTimeElapsed_ReturnsEscalatedStatus() { var threshold = new AlertThreshold("concentration", "Test", 60, 2, 5); var triggeredAt = DateTime.UtcNow.AddMinutes(-3); var severity = RiskAlertsPolicy.DetermineSeverity(threshold, triggeredAt, DateTime.UtcNow); Assert.Equal(AlertSeverity.Warning, severity); } [Fact] public void Policy_EvaluateEscalation_WithTimeThreshold_ReturnsEscalation() { var threshold = new AlertThreshold("concentration", "Test", 60, 2, 5); var triggeredAt = DateTime.UtcNow.AddMinutes(-3); var decision = RiskAlertsPolicy.EvaluateEscalation( threshold, AlertSeverity.Initial, triggeredAt, DateTime.UtcNow, thresholdStillBreached: true); Assert.True(decision.ShouldEscalate); Assert.Equal(AlertSeverity.Warning, decision.ToSeverity); } [Fact] public void Policy_EvaluateResolution_WhenThresholdSafe_ReturnsResolve() { var threshold = new AlertThreshold("concentration", "Test", 60); var triggeredAt = DateTime.UtcNow.AddMinutes(-5); var decision = RiskAlertsPolicy.EvaluateResolution(threshold, 55, triggeredAt, DateTime.UtcNow); Assert.True(decision.ShouldResolve); Assert.Equal("threshold_back_to_safe", decision.ResolutionType); } [Fact] public void Policy_ValidateThreshold_WithInvalidConfig_ReturnsIssues() { var threshold = new AlertThreshold("test", "Test", -10, 5, 2); // Critical < Warn is invalid var (isValid, issues) = RiskAlertsPolicy.ValidateThreshold(threshold); Assert.False(isValid); Assert.NotEmpty(issues); } } /// /// Mock data structures (real implementation uses DB entities) /// public record Position(string Symbol, decimal Quantity, decimal MarketPrice, decimal CostBasisPerUnit); public class AlertThreshold { public string ThresholdType { get; set; } public string ThresholdName { get; set; } public decimal ThresholdValue { get; set; } public int WarnAtMinutes { get; set; } public int CriticalAtMinutes { get; set; } public AlertThreshold(string type, string name, decimal value, int warn = 2, int critical = 5) { ThresholdType = type; ThresholdName = name; ThresholdValue = value; WarnAtMinutes = warn; CriticalAtMinutes = critical; } }