@
#4 Copy-portfolio: diversified master mix instead of single-score ranking The leaderboard ranks single scores, but the top N can be three correlated weather bots. Suggest a de-clumped set instead: - Pure CopyPortfolioBuilder (Application): greedily picks high-scoring masters while enforcing a per-category cap and a max pairwise correlation; reports who was dropped for correlation vs the category cap. - GET /api/portfolio/suggest?size=8&maxPerCategory=2&maxSimilarity=0.6 builds the position-overlap similarity (signed market sets) among copy-relevant masters and runs the diversifier. New PortfolioEndpoints group. - UI: a "Copy-Portfolio" nav page — the diversified picks with category, score, copyability, PnL and the pick reason, plus a funnel summary (candidates / dropped for correlation / dropped for category cap). - Tests: top-score order, category cap, correlation drop, size limit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Predictalytics.Application.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Predictalytics.Application.Tests.Services;
|
||||
|
||||
public class CopyPortfolioBuilderTests
|
||||
{
|
||||
private static readonly System.Func<int, int, double> NoCorrelation = (_, _) => 0.0;
|
||||
|
||||
[Fact]
|
||||
public void PicksTopScoresFirst_WhenNoConstraintsBind()
|
||||
{
|
||||
var candidates = new List<PortfolioCandidate>
|
||||
{
|
||||
new(1, 90m, "Politics"),
|
||||
new(2, 80m, "Sports"),
|
||||
new(3, 70m, "Crypto"),
|
||||
};
|
||||
var r = CopyPortfolioBuilder.Build(candidates, NoCorrelation, size: 8, maxPerCategory: 2);
|
||||
Assert.Equal(new[] { 1, 2, 3 }, r.Picks.Select(p => p.TraderId).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnforcesCategoryCap()
|
||||
{
|
||||
// Three Politics masters, cap 2 -> the third (lowest) is dropped for the cap.
|
||||
var candidates = new List<PortfolioCandidate>
|
||||
{
|
||||
new(1, 90m, "Politics"),
|
||||
new(2, 85m, "Politics"),
|
||||
new(3, 80m, "Politics"),
|
||||
new(4, 70m, "Sports"),
|
||||
};
|
||||
var r = CopyPortfolioBuilder.Build(candidates, NoCorrelation, size: 8, maxPerCategory: 2);
|
||||
|
||||
Assert.Equal(new[] { 1, 2, 4 }, r.Picks.Select(p => p.TraderId).ToArray());
|
||||
Assert.Contains(3, r.DroppedForCategoryCap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropsHighlyCorrelatedCandidate()
|
||||
{
|
||||
var candidates = new List<PortfolioCandidate>
|
||||
{
|
||||
new(1, 90m, "Politics"),
|
||||
new(2, 85m, "Sports"), // highly correlated with #1 -> dropped
|
||||
new(3, 80m, "Crypto"),
|
||||
};
|
||||
// 1 and 2 move together; everyone else independent.
|
||||
double Sim(int a, int b) => (a == 1 && b == 2) || (a == 2 && b == 1) ? 0.9 : 0.0;
|
||||
|
||||
var r = CopyPortfolioBuilder.Build(candidates, Sim, size: 8, maxPerCategory: 2, maxSimilarity: 0.6);
|
||||
|
||||
Assert.Equal(new[] { 1, 3 }, r.Picks.Select(p => p.TraderId).ToArray());
|
||||
Assert.Contains(2, r.DroppedForCorrelation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RespectsSizeLimit()
|
||||
{
|
||||
var candidates = Enumerable.Range(1, 10)
|
||||
.Select(i => new PortfolioCandidate(i, 100m - i, $"Cat{i}")).ToList();
|
||||
var r = CopyPortfolioBuilder.Build(candidates, NoCorrelation, size: 3, maxPerCategory: 2);
|
||||
Assert.Equal(3, r.Picks.Count);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user