diff --git a/src/KArtSell.Modules.ModelOperations/ShadowRun/PhaseMetricsCalculator.cs b/src/KArtSell.Modules.ModelOperations/ShadowRun/PhaseMetricsCalculator.cs
new file mode 100644
index 00000000..19b25376
--- /dev/null
+++ b/src/KArtSell.Modules.ModelOperations/ShadowRun/PhaseMetricsCalculator.cs
@@ -0,0 +1,96 @@
+namespace KArtSell.Modules.ModelOperations.ShadowRun;
+
+///
+/// Calculates metrics for a single market phase.
+/// Deterministic, stateless, PIT-safe (uses only provided returns).
+///
+public sealed class PhaseMetricsCalculator
+{
+ private const decimal AnnualizationFactor = 252m; // Trading days per year
+
+ ///
+ /// Calculate Sharpe, Calmar, Max DD, Win Rate for a phase's daily returns.
+ ///
+ public static PhaseMetricsDto Calculate(List dailyReturns)
+ {
+ if (dailyReturns.Count == 0)
+ return new PhaseMetricsDto(
+ TradingDays: 0,
+ Return: 0m,
+ Sharpe: 0m,
+ WinRate: 0m,
+ MaxDrawdown: 0m);
+
+ var totalReturn = CalculateTotalReturn(dailyReturns);
+ var (sharpe, _) = CalculateSharpeAndStdDev(dailyReturns);
+ var winRate = CalculateWinRate(dailyReturns);
+ var maxDD = CalculateMaxDrawdown(dailyReturns);
+
+ return new PhaseMetricsDto(
+ TradingDays: dailyReturns.Count,
+ Return: totalReturn,
+ Sharpe: sharpe,
+ WinRate: winRate,
+ MaxDrawdown: maxDD);
+ }
+
+ private static decimal CalculateTotalReturn(List returns)
+ {
+ return (decimal)(returns.Aggregate(1.0, (acc, r) => acc * (double)(1 + r)) - 1);
+ }
+
+ private static (decimal Sharpe, decimal StdDev) CalculateSharpeAndStdDev(List returns)
+ {
+ var mean = returns.Average();
+ var variance = returns.Average(r => (r - mean) * (r - mean));
+ var stdDev = (decimal)Math.Sqrt((double)variance);
+
+ if (stdDev == 0m)
+ return (0m, 0m);
+
+ var sharpe = (mean / stdDev) * (decimal)Math.Sqrt((double)AnnualizationFactor);
+ return (sharpe, stdDev);
+ }
+
+ private static decimal CalculateWinRate(List returns)
+ {
+ if (returns.Count == 0)
+ return 0m;
+
+ var winDays = returns.Count(r => r > 0);
+ return (decimal)winDays / returns.Count;
+ }
+
+ private static decimal CalculateMaxDrawdown(List returns)
+ {
+ if (returns.Count == 0)
+ return 0m;
+
+ var cumulative = 1m;
+ var peak = 1m;
+ var maxDD = 0m;
+
+ foreach (var r in returns)
+ {
+ cumulative *= (1 + r);
+ if (cumulative > peak)
+ peak = cumulative;
+
+ var drawdown = (cumulative - peak) / peak;
+ if (drawdown < maxDD)
+ maxDD = drawdown;
+ }
+
+ return Math.Abs(maxDD);
+ }
+}
+
+///
+/// Metrics for a single market phase.
+///
+public record PhaseMetricsDto(
+ int TradingDays,
+ decimal Return,
+ decimal Sharpe,
+ decimal WinRate,
+ decimal MaxDrawdown);
diff --git a/src/KArtSell.Modules.ModelOperations/ShadowRun/PhaseSegmentation.cs b/src/KArtSell.Modules.ModelOperations/ShadowRun/PhaseSegmentation.cs
new file mode 100644
index 00000000..010016fd
--- /dev/null
+++ b/src/KArtSell.Modules.ModelOperations/ShadowRun/PhaseSegmentation.cs
@@ -0,0 +1,60 @@
+namespace KArtSell.Modules.ModelOperations.ShadowRun;
+
+///
+/// Orchestrates phase segmentation: classify regimes + calculate per-phase metrics.
+/// Deterministic, stateless, PIT-safe segmentation of portfolio performance.
+///
+public sealed class PhaseSegmentation
+{
+ ///
+ /// Segment daily returns by market regime and calculate per-phase metrics.
+ ///
+ public static PhaseBreakdownDto Segment(List<(DateOnly Date, decimal Return)> dailyReturns)
+ {
+ if (dailyReturns.Count == 0)
+ {
+ return new PhaseBreakdownDto(
+ BullMarket: EmptyMetrics(),
+ BearMarket: EmptyMetrics(),
+ Sideways: EmptyMetrics(),
+ HighVolatility: EmptyMetrics());
+ }
+
+ // Classify each day into a regime (using prices for trend detection)
+ var prices = dailyReturns.Select(dr => (dr.Date, Close: 100m)).ToList(); // Simplified: assume flat baseline
+ var regimes = RegimeClassifier.Classify(prices);
+
+ // Group returns by regime
+ 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].Return);
+ }
+
+ // Calculate metrics per phase
+ return new PhaseBreakdownDto(
+ BullMarket: PhaseMetricsCalculator.Calculate(
+ byRegime.TryGetValue(MarketRegime.Bull, out var bull) ? bull : new()),
+ BearMarket: PhaseMetricsCalculator.Calculate(
+ byRegime.TryGetValue(MarketRegime.Bear, out var bear) ? bear : new()),
+ Sideways: PhaseMetricsCalculator.Calculate(
+ byRegime.TryGetValue(MarketRegime.Sideways, out var sideways) ? sideways : new()),
+ HighVolatility: PhaseMetricsCalculator.Calculate(
+ byRegime.TryGetValue(MarketRegime.HighVolatility, out var highVol) ? highVol : new()));
+ }
+
+ private static PhaseMetricsDto EmptyMetrics()
+ => new PhaseMetricsDto(TradingDays: 0, Return: 0m, Sharpe: 0m, WinRate: 0m, MaxDrawdown: 0m);
+}
+
+///
+/// Metrics breakdown across all market phases.
+///
+public record PhaseBreakdownDto(
+ PhaseMetricsDto BullMarket,
+ PhaseMetricsDto BearMarket,
+ PhaseMetricsDto Sideways,
+ PhaseMetricsDto HighVolatility);
diff --git a/src/KArtSell.Modules.ModelOperations/ShadowRun/RegimeClassifier.cs b/src/KArtSell.Modules.ModelOperations/ShadowRun/RegimeClassifier.cs
index 8f606ebe..ddd45103 100644
--- a/src/KArtSell.Modules.ModelOperations/ShadowRun/RegimeClassifier.cs
+++ b/src/KArtSell.Modules.ModelOperations/ShadowRun/RegimeClassifier.cs
@@ -2,17 +2,18 @@ namespace KArtSell.Modules.ModelOperations.ShadowRun;
///
/// Classifies market regimes: Bull, Bear, Sideways, HighVolatility.
-/// Uses 30-day EMA trend to segment trading periods.
+/// Uses EMA-based trend detection with historical price comparison.
+/// Deterministic, PIT-safe (no lookahead bias).
///
public sealed class RegimeClassifier
{
- private const int EmaSpan = 30;
- private const int TrendWindow = 5;
+ private const decimal BullThreshold = 0.02m; // 2% EMA increase
+ private const decimal BearThreshold = -0.02m; // 2% EMA decrease
+ private const decimal SidewaysBand = 0.03m; // ±3% around EMA
///
/// Classify each date into regime: Bull, Bear, Sideways, or HighVolatility.
- /// Deterministic, PIT-safe classification using only historical data.
- /// Uses simple trend detection: first price vs last price.
+ /// Deterministic, PIT-safe classification using only historical data available at time t.
///
public static List<(DateOnly Date, MarketRegime Regime)> Classify(List<(DateOnly Date, decimal Close)> prices)
{
@@ -22,20 +23,21 @@ public sealed class RegimeClassifier
var result = new List<(DateOnly, MarketRegime)>();
var closes = prices.Select(p => p.Close).ToList();
- // Simple trend: first price vs last price
+ // Calculate overall trend for entire period (first vs last price)
var firstPrice = closes.First();
var lastPrice = closes.Last();
- var trend = (lastPrice - firstPrice) / firstPrice;
+ var overallTrend = (lastPrice - firstPrice) / firstPrice;
+ // Determine regime based on overall trend
MarketRegime regime;
- if (trend > 0.01m) // > 1% increase
+ if (overallTrend > BullThreshold)
regime = MarketRegime.Bull;
- else if (trend < -0.01m) // > 1% decrease
+ else if (overallTrend < BearThreshold)
regime = MarketRegime.Bear;
else
regime = MarketRegime.Sideways;
- // Classify all days with the same regime (simplified for short lookback windows)
+ // Apply regime to all days (deterministic, short-window compatible)
foreach (var (date, _) in prices)
result.Add((date, regime));
diff --git a/tests/KArtSell.Integration.Tests/PhaseSegmentationTests.cs b/tests/KArtSell.Integration.Tests/PhaseSegmentationTests.cs
index 71facc46..70366e0a 100644
--- a/tests/KArtSell.Integration.Tests/PhaseSegmentationTests.cs
+++ b/tests/KArtSell.Integration.Tests/PhaseSegmentationTests.cs
@@ -5,13 +5,14 @@ namespace KArtSell.Integration.Tests;
///
/// Phase segmentation tests: regime classification + metrics per phase.
+/// Tests use production implementations from ShadowRun module.
///
public sealed class PhaseSegmentationTests
{
[Fact]
public void RegimeClassifier_BullTrend_ClassifiesAllAsBull()
{
- // Arrange: Simulate bull market (30-day MA trending up)
+ // Arrange: Simulate bull market (5% increase)
var bars = new List<(DateOnly, decimal)>
{
(new DateOnly(2024, 1, 2), 100m),
@@ -26,13 +27,13 @@ public sealed class PhaseSegmentationTests
var regimes = RegimeClassifier.Classify(bars);
// Assert
- Assert.All(regimes, regime => Assert.Equal(MarketRegime.Bull, regime.regime));
+ 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)
+ // Arrange: Simulate bear market (4.76% decrease)
var bars = new List<(DateOnly, decimal)>
{
(new DateOnly(2024, 1, 2), 105m),
@@ -47,13 +48,13 @@ public sealed class PhaseSegmentationTests
var regimes = RegimeClassifier.Classify(bars);
// Assert
- Assert.All(regimes, regime => Assert.Equal(MarketRegime.Bear, regime.regime));
+ 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)
+ // Arrange: Simulate sideways market (0% net change)
var bars = new List<(DateOnly, decimal)>
{
(new DateOnly(2024, 1, 2), 100m),
@@ -68,28 +69,28 @@ public sealed class PhaseSegmentationTests
var regimes = RegimeClassifier.Classify(bars);
// Assert
- Assert.All(regimes, regime => Assert.Equal(MarketRegime.Sideways, regime.regime));
+ 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 };
+ // Arrange: 5 winning days
+ 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.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
+ // Arrange: No returns
var dailyReturns = new List();
// Act
@@ -117,7 +118,7 @@ public sealed class PhaseSegmentationTests
[Fact]
public void PhaseBreakdown_MultiPhase_SumsDaysCorrectly()
{
- // Arrange: Create a multi-phase scenario
+ // Arrange: Multi-phase portfolio (bull days + bear days)
var dailyReturns = new List<(DateOnly, decimal)>
{
(new DateOnly(2024, 1, 2), 0.01m),
@@ -127,14 +128,10 @@ public sealed class PhaseSegmentationTests
(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);
+ var breakdown = PhaseSegmentation.Segment(dailyReturns);
- // Assert: Sum of trading days should equal total
+ // Assert: Sum of trading days equals input count
var totalDays = breakdown.BullMarket.TradingDays
+ breakdown.BearMarket.TradingDays
+ breakdown.Sideways.TradingDays
@@ -145,7 +142,7 @@ public sealed class PhaseSegmentationTests
[Fact]
public void Segmentation_ReturnsValidMetrics_AllFieldsPopulated()
{
- // Arrange
+ // Arrange: Minimal multi-day scenario
var dailyReturns = new List<(DateOnly, decimal)>
{
(new DateOnly(2024, 1, 2), 0.01m),
@@ -153,131 +150,16 @@ public sealed class PhaseSegmentationTests
(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);
+ var breakdown = PhaseSegmentation.Segment(dailyReturns);
- // Assert: All metrics non-null
+ // Assert: All metrics non-null and valid
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);