#5 Edge-freshness: is a master edge current or stale?

Compares the recent out-of-sample window (last 60d) against the older one
(day 60-180) — both already produced per run by the analytics worker, so no
worker changes and no schema change.

- Pure EdgeFreshnessCalculator (Application): Fresh / Stable / Fading /
  Insufficient + a 0-100 freshness score. Fading when return/market drops past a
  threshold or a once-strong profit factor collapses below break-even.
- GET /api/traders/{id}/edge-freshness reads the two windows and computes it.
- UI: a colored edge-freshness badge on the trader detail page
  (🟢 frisch / 🟡 stabil / 🔴 verblasst) with the return delta and market counts.
- Tests: 5 scenarios (insufficient, fresh, return collapse, PF collapse, stable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
This commit is contained in:
Richard
2026-07-24 11:44:23 +02:00
parent b7bee86ed7
commit e5ce69793b
6 changed files with 168 additions and 0 deletions
@@ -0,0 +1,54 @@
using Predictalytics.Application.Services;
using Predictalytics.Domain.Entities;
using Xunit;
namespace Predictalytics.Application.Tests.Services;
public class EdgeFreshnessCalculatorTests
{
private static TraderWindowMetrics Win(int closed, decimal avgReturn, decimal? pf = null)
=> new() { ClosedMarkets = closed, AvgReturnPct = avgReturn, ProfitFactor = pf };
[Fact]
public void TooFewMarkets_IsInsufficient()
{
var r = EdgeFreshnessCalculator.Compute(Win(3, 10m), Win(20, 8m));
Assert.Equal("Insufficient", r.Verdict);
}
[Fact]
public void HoldingEdge_IsFresh()
{
// Recent >= older and positive.
var r = EdgeFreshnessCalculator.Compute(older: Win(30, 6m), recent: Win(25, 9m));
Assert.Equal("Fresh", r.Verdict);
Assert.True(r.Score > 50m);
}
[Fact]
public void ReturnCollapse_IsFading()
{
// Return per market dropped well beyond the fade threshold.
var r = EdgeFreshnessCalculator.Compute(older: Win(30, 12m), recent: Win(25, 2m));
Assert.Equal("Fading", r.Verdict);
Assert.True(r.Score < 50m);
}
[Fact]
public void ProfitFactorCollapse_IsFading_EvenOnSmallReturnDrop()
{
// Return barely moves, but a once-strong profit factor fell below break-even.
var r = EdgeFreshnessCalculator.Compute(
older: Win(30, 5m, pf: 1.6m),
recent: Win(25, 4m, pf: 0.8m));
Assert.Equal("Fading", r.Verdict);
}
[Fact]
public void MildDecline_IsStable()
{
// Small drop (< fade threshold), not an improvement -> Stable.
var r = EdgeFreshnessCalculator.Compute(older: Win(30, 6m), recent: Win(25, 4m));
Assert.Equal("Stable", r.Verdict);
}
}