H2: strategy-fingerprint metrics (conviction/sizing edge + category concentration)
Two deeper analyses, both pure from existing data (no new API cost): - ConvictionEdgePct: return% of the biggest-bet third minus the smallest-bet third of closed markets. Positive => sizing carries information (copy size-weighted); negative => overbets losers (red flag). CalculateMarketWinRates now emits per-market (invested, returnPct) pairs consumed by StrategyMetricsCalculator.ComputeConvictionEdge. - CategoryConcentration: Herfindahl index of category volume shares (specialist vs generalist), from the category-performance dict. Stored on TraderAnalytics, exposed on TraderDetailDto. Migration AddStrategyFingerprintMetrics. +7 unit tests (70 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
dcac62165e
commit
eaccdadddf
@@ -0,0 +1,74 @@
|
||||
using System.Collections.Generic;
|
||||
using Predictalytics.Application.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Predictalytics.Application.Tests.Services;
|
||||
|
||||
public class StrategyMetricsCalculatorTests
|
||||
{
|
||||
// ── Concentration (Herfindahl) ────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Concentration_SingleCategory_IsOne()
|
||||
{
|
||||
Assert.Equal(1.0m, StrategyMetricsCalculator.ComputeConcentration(new[] { 500m }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Concentration_TwoEqualCategories_IsHalf()
|
||||
{
|
||||
// shares 0.5, 0.5 -> 0.25 + 0.25 = 0.5
|
||||
Assert.Equal(0.5m, StrategyMetricsCalculator.ComputeConcentration(new[] { 100m, 100m }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Concentration_SpecialistScoresHigherThanGeneralist()
|
||||
{
|
||||
var specialist = StrategyMetricsCalculator.ComputeConcentration(new[] { 900m, 50m, 50m });
|
||||
var generalist = StrategyMetricsCalculator.ComputeConcentration(new[] { 100m, 100m, 100m, 100m });
|
||||
Assert.True(specialist > generalist);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Concentration_NoVolume_IsZero()
|
||||
{
|
||||
Assert.Equal(0m, StrategyMetricsCalculator.ComputeConcentration(new[] { 0m, 0m }));
|
||||
}
|
||||
|
||||
// ── Conviction / sizing edge ──────────────────────────────────────────────
|
||||
[Fact]
|
||||
public void Conviction_TooFewMarkets_IsNull()
|
||||
{
|
||||
var markets = new List<(decimal, decimal)> { (10m, 5m), (20m, 5m), (30m, 5m) };
|
||||
Assert.Null(StrategyMetricsCalculator.ComputeConvictionEdge(markets));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Conviction_BigBetsWinMore_IsPositive()
|
||||
{
|
||||
// small bets (invested 10-30) return ~0%, big bets (invested 100-120) return ~+40%
|
||||
var markets = new List<(decimal Invested, decimal ReturnPct)>
|
||||
{
|
||||
(10m, 0m), (20m, 2m), (30m, -2m),
|
||||
(50m, 5m), (60m, 3m), (70m, 4m),
|
||||
(100m, 40m), (110m, 38m), (120m, 42m),
|
||||
};
|
||||
var edge = StrategyMetricsCalculator.ComputeConvictionEdge(markets);
|
||||
Assert.NotNull(edge);
|
||||
Assert.True(edge > 30m); // big third avg ~40 minus small third avg ~0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Conviction_OverbetsLosers_IsNegative()
|
||||
{
|
||||
// big bets LOSE, small bets win -> negative conviction (a red flag)
|
||||
var markets = new List<(decimal Invested, decimal ReturnPct)>
|
||||
{
|
||||
(10m, 20m), (20m, 25m), (30m, 22m),
|
||||
(50m, 5m), (60m, 3m), (70m, 4m),
|
||||
(100m, -30m), (110m, -35m), (120m, -28m),
|
||||
};
|
||||
var edge = StrategyMetricsCalculator.ComputeConvictionEdge(markets);
|
||||
Assert.NotNull(edge);
|
||||
Assert.True(edge < 0m);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user