H1: risk-adjusted return metrics (equity-curve smoothness = copyability)
New RiskMetricsCalculator (pure) derives max drawdown (USD), daily-PnL volatility and longest losing streak from the trader's TraderDailySnapshot equity curve; the PnL engine computes them each recalc and stores them on TraderAnalytics (+ computed ReturnOverMaxDrawdown, Calmar-like). Two traders with identical final PnL but a smoother path are very differently copyable — this captures that. Exposed on TraderDetailDto. Migration AddRiskAdjustedMetrics. +5 unit tests (63 total, 1 skip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
be1b90b556
commit
dcac62165e
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Predictalytics.Application.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Predictalytics.Application.Tests.Services;
|
||||
|
||||
public class RiskMetricsCalculatorTests
|
||||
{
|
||||
private static List<(DateTime, decimal)> Curve(params decimal[] pnls)
|
||||
{
|
||||
var list = new List<(DateTime, decimal)>();
|
||||
var d = new DateTime(2026, 01, 01, 0, 0, 0, DateTimeKind.Utc);
|
||||
foreach (var p in pnls) { list.Add((d, p)); d = d.AddDays(1); }
|
||||
return list;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_ReturnsZeros()
|
||||
{
|
||||
var m = RiskMetricsCalculator.Compute(new List<(DateTime, decimal)>());
|
||||
Assert.Equal(0m, m.MaxDrawdownUsd);
|
||||
Assert.Equal(0m, m.PnlVolatilityUsd);
|
||||
Assert.Equal(0, m.LongestLosingStreakDays);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MonotonicUp_HasNoDrawdownNoStreak()
|
||||
{
|
||||
// Steady climb 0 -> 10 -> 20 -> 30: the ideal, most-copyable curve.
|
||||
var m = RiskMetricsCalculator.Compute(Curve(0, 10, 20, 30));
|
||||
Assert.Equal(0m, m.MaxDrawdownUsd);
|
||||
Assert.Equal(0, m.LongestLosingStreakDays);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DipFromPeak_MeasuresPeakToTroughDrawdown()
|
||||
{
|
||||
// Peak 100, trough 40 -> max drawdown 60. Ends back at 90.
|
||||
var m = RiskMetricsCalculator.Compute(Curve(0, 100, 70, 40, 90));
|
||||
Assert.Equal(60m, m.MaxDrawdownUsd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LongestLosingStreak_CountsConsecutiveDownDays()
|
||||
{
|
||||
// deltas: +100, -30, -30, -30, +50 -> 3 consecutive losing days
|
||||
var m = RiskMetricsCalculator.Compute(Curve(0, 100, 70, 40, 10, 60));
|
||||
Assert.Equal(3, m.LongestLosingStreakDays);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Volatility_IsZeroForConstantDailyGain_PositiveForBumpyPath()
|
||||
{
|
||||
// Constant +10/day -> zero volatility of daily returns.
|
||||
var smooth = RiskMetricsCalculator.Compute(Curve(0, 10, 20, 30, 40));
|
||||
Assert.Equal(0m, smooth.PnlVolatilityUsd);
|
||||
|
||||
// Same endpoint (+40) but a bumpy path -> positive volatility.
|
||||
var bumpy = RiskMetricsCalculator.Compute(Curve(0, 50, 10, 60, 40));
|
||||
Assert.True(bumpy.PnlVolatilityUsd > 0m);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user