@
#1 Smart-money co-movement detection (discovery increment 1) The correlation endpoint was only pairwise + position-overlap based. Add timing- based, one-to-many co-movement discovery: for a seed trader, find the wallets that repeatedly enter the SAME outcomes within a time window — surfacing new informed traders rather than just ranking known ones. - Pure CoMovementCalculator (Application): ranks candidate wallets by shared co-entered markets; positive AvgLeadHours = the wallet tends to move BEFORE the seed (the informed-trader signal). - GET /api/traders/{id}/co-movement?windowHours=48&minShared=3 (bounds the seed to its last 500 buys) + CoMovingWalletDto. - UI: a co-movement card on the trader detail page listing the top related wallets with shared-market count and lead/lag (green when they move first). - Tests: min-shared threshold + window filtering, lead sign, ranking order. Next increment: cluster the co-movement graph + a visual; weight co-entries that precede significant price moves. Endpoint is on-demand per seed for now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Predictalytics.Application.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Predictalytics.Application.Tests.Services;
|
||||
|
||||
public class CoMovementCalculatorTests
|
||||
{
|
||||
private static readonly DateTime T0 = new(2026, 7, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void RanksCoMovers_AndRequiresMinSharedMarkets()
|
||||
{
|
||||
// Seed entered outcomes 1,2,3,4.
|
||||
var seed = new List<CoEntry>
|
||||
{
|
||||
new(1, T0), new(2, T0.AddDays(1)), new(3, T0.AddDays(2)), new(4, T0.AddDays(3))
|
||||
};
|
||||
var cands = new List<CandidateEntry>
|
||||
{
|
||||
// Trader 10 co-enters 1,2,3 within the window -> qualifies (>=3).
|
||||
new(10, 1, T0.AddHours(-2)),
|
||||
new(10, 2, T0.AddDays(1).AddHours(-1)),
|
||||
new(10, 3, T0.AddDays(2).AddHours(3)),
|
||||
// Trader 20 co-enters only 1,2 -> below minShared.
|
||||
new(20, 1, T0.AddHours(1)),
|
||||
new(20, 2, T0.AddDays(1)),
|
||||
// Trader 30 enters outcome 3 but far outside the window -> ignored.
|
||||
new(30, 3, T0.AddDays(10)),
|
||||
};
|
||||
|
||||
var ranked = CoMovementCalculator.Rank(seed, cands, windowHours: 48, minSharedMarkets: 3);
|
||||
|
||||
Assert.Single(ranked);
|
||||
Assert.Equal(10, ranked[0].TraderId);
|
||||
Assert.Equal(3, ranked[0].SharedMarkets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositiveLead_MeansCandidateMovedBeforeSeed()
|
||||
{
|
||||
var seed = new List<CoEntry> { new(1, T0), new(2, T0.AddDays(1)), new(3, T0.AddDays(2)) };
|
||||
// Candidate consistently enters 3 hours BEFORE the seed.
|
||||
var cands = new List<CandidateEntry>
|
||||
{
|
||||
new(10, 1, T0.AddHours(-3)),
|
||||
new(10, 2, T0.AddDays(1).AddHours(-3)),
|
||||
new(10, 3, T0.AddDays(2).AddHours(-3)),
|
||||
};
|
||||
|
||||
var ranked = CoMovementCalculator.Rank(seed, cands);
|
||||
|
||||
Assert.Single(ranked);
|
||||
Assert.True(ranked[0].AvgLeadHours > 0);
|
||||
Assert.Equal(3.0, ranked[0].AvgLeadHours, 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HigherSharedMarketCount_RanksFirst()
|
||||
{
|
||||
var seed = Enumerable.Range(1, 6).Select(i => new CoEntry(i, T0.AddHours(i))).ToList();
|
||||
var cands = new List<CandidateEntry>();
|
||||
// Trader 10 shares 5 markets, trader 20 shares 3.
|
||||
for (int i = 1; i <= 5; i++) cands.Add(new(10, i, T0.AddHours(i)));
|
||||
for (int i = 1; i <= 3; i++) cands.Add(new(20, i, T0.AddHours(i)));
|
||||
|
||||
var ranked = CoMovementCalculator.Rank(seed, cands, minSharedMarkets: 3);
|
||||
|
||||
Assert.Equal(2, ranked.Count);
|
||||
Assert.Equal(10, ranked[0].TraderId);
|
||||
Assert.Equal(20, ranked[1].TraderId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user