64bdc45260
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>
166 lines
5.2 KiB
C#
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);
|
|
}
|
|
}
|