Files
KArtSell.Aegis/tests/KArtSell.Integration.Tests/PhaseSegmentationTests.cs
T
kjh2064 64bdc45260 Phase Segmentation: Full implementation with improved RegimeClassifier
Complete market regime classification and phase-specific metrics calculation.

Files:
- src/KArtSell.Modules.ModelOperations/ShadowRun/RegimeClassifier.cs (improved)
  Threshold-based trend detection (Bull >2%, Bear <-2%, Sideways within band)
  Deterministic PIT-safe classification, no lookahead bias

- src/KArtSell.Modules.ModelOperations/ShadowRun/PhaseMetricsCalculator.cs (new)
  Per-phase metrics: Sharpe (annualized), Calmar, Max DD, Win Rate
  Stateless calculation using only provided daily returns

- src/KArtSell.Modules.ModelOperations/ShadowRun/PhaseSegmentation.cs (new)
  Orchestrator combining RegimeClassifier + PhaseMetricsCalculator
  Groups returns by regime, calculates per-phase metrics
  Returns PhaseBreakdownDto with all four market conditions

- tests/KArtSell.Integration.Tests/PhaseSegmentationTests.cs (updated)
  Removed temporary implementations, now uses module classes
  Test status: 8/8 PASSING

AGENTS.md v16.0:
 Pattern: Vertical component, single responsibility per class
 Simplicity: Clear threshold-based trend detection
 Maturity: Contract-first, test-first, implementation verified
 Necessity: Supports "복수 국면 OOS" requirement from README

Next: Integrate PhaseSegmentation into ShadowRunJob workflow.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-02 12:10:38 +09:00

166 lines
5.2 KiB
C#

using Xunit;
using KArtSell.Modules.ModelOperations.ShadowRun;
namespace KArtSell.Integration.Tests;
/// <summary>
/// Phase segmentation tests: regime classification + metrics per phase.
/// Tests use production implementations from ShadowRun module.
/// </summary>
public sealed class PhaseSegmentationTests
{
[Fact]
public void RegimeClassifier_BullTrend_ClassifiesAllAsBull()
{
// Arrange: Simulate bull market (5% increase)
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 (4.76% decrease)
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 (0% net change)
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: 5 winning days
var dailyReturns = new List<decimal> { 0.01m, 0.02m, 0.01m, 0.005m, 0.015m };
// Act
var metrics = PhaseMetricsCalculator.Calculate(dailyReturns);
// Assert
Assert.Equal(5, metrics.TradingDays);
Assert.True(metrics.WinRate > 0 && metrics.WinRate <= 1);
Assert.True(metrics.Return > 0, "Bull phase should have positive return");
}
[Fact]
public void PhaseMetrics_EmptyPhase_ReturnsZeros()
{
// Arrange: No returns
var dailyReturns = new List<decimal>();
// 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<decimal> { 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: Multi-phase portfolio (bull days + bear days)
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),
};
// Act
var breakdown = PhaseSegmentation.Segment(dailyReturns);
// Assert: Sum of trading days equals input count
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: Minimal multi-day 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),
};
// Act
var breakdown = PhaseSegmentation.Segment(dailyReturns);
// Assert: All metrics non-null and valid
Assert.NotNull(breakdown.BullMarket);
Assert.NotNull(breakdown.BearMarket);
Assert.NotNull(breakdown.Sideways);
Assert.NotNull(breakdown.HighVolatility);
Assert.True(breakdown.BullMarket.WinRate >= 0 && breakdown.BullMarket.WinRate <= 1);
Assert.True(breakdown.BullMarket.Sharpe >= -5 && breakdown.BullMarket.Sharpe <= 5);
}
}