using Xunit; using KArtSell.Modules.ModelOperations.ShadowRun; namespace KArtSell.Integration.Tests; /// /// Phase segmentation tests: regime classification + metrics per phase. /// public sealed class PhaseSegmentationTests { [Fact] public void RegimeClassifier_BullTrend_ClassifiesAllAsBull() { // Arrange: Simulate bull market (30-day MA trending up) var bars = new List<(DateOnly, decimal)> { (new DateOnly(2024, 1, 2), 100m), (new DateOnly(2024, 1, 3), 101m), (new DateOnly(2024, 1, 4), 102m), (new DateOnly(2024, 1, 5), 103m), (new DateOnly(2024, 1, 8), 104m), (new DateOnly(2024, 1, 9), 105m), }; // Act var regimes = RegimeClassifier.Classify(bars); // Assert Assert.All(regimes, regime => Assert.Equal(MarketRegime.Bull, regime.regime)); } [Fact] public void RegimeClassifier_BearTrend_ClassifiesAllAsBear() { // Arrange: Simulate bear market (30-day MA trending down) var bars = new List<(DateOnly, decimal)> { (new DateOnly(2024, 1, 2), 105m), (new DateOnly(2024, 1, 3), 104m), (new DateOnly(2024, 1, 4), 103m), (new DateOnly(2024, 1, 5), 102m), (new DateOnly(2024, 1, 8), 101m), (new DateOnly(2024, 1, 9), 100m), }; // Act var regimes = RegimeClassifier.Classify(bars); // Assert Assert.All(regimes, regime => Assert.Equal(MarketRegime.Bear, regime.regime)); } [Fact] public void RegimeClassifier_Sideways_ClassifiesAllAsSideways() { // Arrange: Simulate sideways market (price oscillates ±5% around MA) var bars = new List<(DateOnly, decimal)> { (new DateOnly(2024, 1, 2), 100m), (new DateOnly(2024, 1, 3), 101m), (new DateOnly(2024, 1, 4), 99m), (new DateOnly(2024, 1, 5), 102m), (new DateOnly(2024, 1, 8), 98m), (new DateOnly(2024, 1, 9), 100m), }; // Act var regimes = RegimeClassifier.Classify(bars); // Assert Assert.All(regimes, regime => Assert.Equal(MarketRegime.Sideways, regime.regime)); } [Fact] public void PhaseMetrics_BullPhase_CalculatesCorrectMetrics() { // Arrange var dailyReturns = new List { 0.01m, 0.02m, 0.01m, -0.005m, 0.015m }; // Act var metrics = PhaseMetricsCalculator.Calculate(dailyReturns); // Assert Assert.True(metrics.TradingDays == 5); Assert.True(metrics.WinRate > 0 && metrics.WinRate <= 1, $"WinRate should be [0,1], got {metrics.WinRate}"); Assert.True(metrics.Return > 0, "Bull phase should have positive return"); } [Fact] public void PhaseMetrics_EmptyPhase_ReturnsZeros() { // Arrange var dailyReturns = new List(); // Act var metrics = PhaseMetricsCalculator.Calculate(dailyReturns); // Assert Assert.Equal(0, metrics.TradingDays); Assert.Equal(0m, metrics.Return); Assert.Equal(0m, metrics.Sharpe); } [Fact] public void PhaseMetrics_MixedReturns_CalculatesWinRate() { // Arrange: 3 wins, 2 losses var dailyReturns = new List { 0.01m, -0.005m, 0.02m, -0.01m, 0.015m }; // Act var metrics = PhaseMetricsCalculator.Calculate(dailyReturns); // Assert Assert.Equal(0.6m, metrics.WinRate); // 3/5 = 60% } [Fact] public void PhaseBreakdown_MultiPhase_SumsDaysCorrectly() { // Arrange: Create a multi-phase scenario var dailyReturns = new List<(DateOnly, decimal)> { (new DateOnly(2024, 1, 2), 0.01m), (new DateOnly(2024, 1, 3), 0.02m), (new DateOnly(2024, 1, 4), -0.005m), (new DateOnly(2024, 1, 5), 0.015m), (new DateOnly(2024, 1, 8), -0.01m), }; var classifier = new RegimeClassifier(); var metricsCalc = new PhaseMetricsCalculator(); var segmenter = new PhaseSegmentation(classifier, metricsCalc); // Act var breakdown = PhaseSegmentation.StaticSegment(dailyReturns, classifier, metricsCalc); // Assert: Sum of trading days should equal total var totalDays = breakdown.BullMarket.TradingDays + breakdown.BearMarket.TradingDays + breakdown.Sideways.TradingDays + breakdown.HighVolatility.TradingDays; Assert.Equal(dailyReturns.Count, totalDays); } [Fact] public void Segmentation_ReturnsValidMetrics_AllFieldsPopulated() { // Arrange var dailyReturns = new List<(DateOnly, decimal)> { (new DateOnly(2024, 1, 2), 0.01m), (new DateOnly(2024, 1, 3), 0.02m), (new DateOnly(2024, 1, 4), -0.005m), }; var classifier = new RegimeClassifier(); var metricsCalc = new PhaseMetricsCalculator(); var segmenter = new PhaseSegmentation(classifier, metricsCalc); // Act var breakdown = PhaseSegmentation.StaticSegment(dailyReturns, classifier, metricsCalc); // Assert: All metrics non-null Assert.NotNull(breakdown.BullMarket); Assert.NotNull(breakdown.BearMarket); Assert.NotNull(breakdown.Sideways); Assert.NotNull(breakdown.HighVolatility); // Assert: Metric fields valid Assert.True(breakdown.BullMarket.WinRate >= 0 && breakdown.BullMarket.WinRate <= 1); Assert.True(breakdown.BullMarket.Sharpe >= -5 && breakdown.BullMarket.Sharpe <= 5); } } /// /// Regime classifier: Bull, Bear, Sideways, HighVolatility /// public sealed class RegimeClassifier { public static List<(DateOnly Date, MarketRegime regime)> Classify(List<(DateOnly, decimal)> prices) { var result = new List<(DateOnly, MarketRegime)>(); if (prices.Count < 30) return prices.Select(p => (p.Item1, MarketRegime.Sideways)).ToList(); // Simplified: classify based on trend var avgPrice = prices.Average(p => p.Item2); var recentAvg = prices.TakeLast(5).Average(p => p.Item2); foreach (var (date, price) in prices) { var regime = recentAvg > avgPrice ? MarketRegime.Bull : recentAvg < avgPrice ? MarketRegime.Bear : MarketRegime.Sideways; result.Add((date, regime)); } return result; } } /// /// Calculate metrics for a single phase /// public sealed class PhaseMetricsCalculator { public static PhaseMetricsDto Calculate(List returns) { if (returns.Count == 0) return new PhaseMetricsDto(0, 0m, 0m, 0m, 0m); var totalReturn = (decimal)(returns.Aggregate(1.0, (acc, r) => acc * (double)(1 + r)) - 1); var winRate = (decimal)returns.Count(r => r > 0) / returns.Count; var mean = returns.Average(); var variance = returns.Average(r => (r - mean) * (r - mean)); var stdDev = (decimal)Math.Sqrt((double)variance); var sharpe = stdDev > 0 ? (mean / stdDev) * (decimal)Math.Sqrt(252) : 0m; // Simplified max drawdown var cumulative = 1m; var peak = 1m; var maxDD = 0m; foreach (var r in returns) { cumulative *= (1 + r); if (cumulative > peak) peak = cumulative; var dd = (cumulative - peak) / peak; if (dd < maxDD) maxDD = dd; } return new PhaseMetricsDto( TradingDays: returns.Count, Return: totalReturn, Sharpe: sharpe, WinRate: winRate, MaxDrawdown: Math.Abs(maxDD)); } } /// /// Orchestrates phase segmentation: classify regimes + calculate per-phase metrics /// public sealed class PhaseSegmentation { private readonly RegimeClassifier _classifier; private readonly PhaseMetricsCalculator _metricsCalc; public PhaseSegmentation(RegimeClassifier classifier, PhaseMetricsCalculator metricsCalc) { _classifier = classifier; _metricsCalc = metricsCalc; } public static PhaseBreakdownDto StaticSegment(List<(DateOnly, decimal)> dailyReturns, RegimeClassifier classifier, PhaseMetricsCalculator metricsCalc) { var regimes = RegimeClassifier.Classify(dailyReturns.Select(r => (r.Item1, (decimal)100)).ToList()); var byRegime = new Dictionary>(); for (int i = 0; i < dailyReturns.Count; i++) { var regime = regimes[i].regime; if (!byRegime.ContainsKey(regime)) byRegime[regime] = new List(); byRegime[regime].Add(dailyReturns[i].Item2); } return new PhaseBreakdownDto( BullMarket: PhaseMetricsCalculator.Calculate(byRegime.GetValueOrDefault(MarketRegime.Bull, new())), BearMarket: PhaseMetricsCalculator.Calculate(byRegime.GetValueOrDefault(MarketRegime.Bear, new())), Sideways: PhaseMetricsCalculator.Calculate(byRegime.GetValueOrDefault(MarketRegime.Sideways, new())), HighVolatility: PhaseMetricsCalculator.Calculate(byRegime.GetValueOrDefault(MarketRegime.HighVolatility, new()))); } } public enum MarketRegime { Bull, Bear, Sideways, HighVolatility } public record PhaseMetricsDto(int TradingDays, decimal Return, decimal Sharpe, decimal WinRate, decimal MaxDrawdown); public record PhaseBreakdownDto(PhaseMetricsDto BullMarket, PhaseMetricsDto BearMarket, PhaseMetricsDto Sideways, PhaseMetricsDto HighVolatility);