@
#1 increment 2: cluster the co-movement graph + network visualization Completes the smart-money discovery lever beyond the per-seed list: - Pure CoMovementGraphBuilder (Application): links wallets by shared timely co-entries and clusters the graph via union-find (connected components). - GET /api/co-movement/graph builds the graph over copy-relevant + insider wallets and weights each node by an "informed share" — how often the wallet entered before a big favorable price move (from stored price snapshots, graceful when absent). This is the "co-move before price moves" signal. - UI: a new "Netzwerk" page rendering an SVG cluster graph (node size = score, color = cluster, gold ring = insider, green ring = informed leader); click a node opens the trader. - Tests: min-edge-weight, window filtering, connected-component clustering. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Predictalytics.Application.Services;
|
||||
using Xunit;
|
||||
|
||||
namespace Predictalytics.Application.Tests.Services;
|
||||
|
||||
public class CoMovementGraphBuilderTests
|
||||
{
|
||||
private static readonly DateTime T0 = new(2026, 7, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
// Two traders co-entering `count` shared outcomes within the window.
|
||||
private static IEnumerable<CandidateEntry> CoEnter(int a, int b, int count, int outcomeBase = 0)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var oid = outcomeBase + i;
|
||||
yield return new CandidateEntry(a, oid, T0.AddDays(i));
|
||||
yield return new CandidateEntry(b, oid, T0.AddDays(i).AddHours(2));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeFormsOnlyAtOrAboveMinWeight()
|
||||
{
|
||||
// 10 & 20 share 4 outcomes; 30 & 40 share only 2 -> below minEdge=3.
|
||||
var entries = CoEnter(10, 20, 4, outcomeBase: 0)
|
||||
.Concat(CoEnter(30, 40, 2, outcomeBase: 100))
|
||||
.ToList();
|
||||
|
||||
var g = CoMovementGraphBuilder.Build(entries, windowHours: 48, minEdgeWeight: 3);
|
||||
|
||||
Assert.Single(g.Edges);
|
||||
Assert.Equal(4, g.Edges[0].Weight);
|
||||
Assert.Contains(10, g.ClusterByTrader.Keys);
|
||||
Assert.DoesNotContain(30, g.ClusterByTrader.Keys); // no qualifying edge -> not in graph
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutsideWindowDoesNotCount()
|
||||
{
|
||||
var entries = new List<CandidateEntry>
|
||||
{
|
||||
new(1, 1, T0), new(2, 1, T0.AddDays(5)), // 5 days apart -> outside 48h
|
||||
new(1, 2, T0), new(2, 2, T0.AddDays(5)),
|
||||
new(1, 3, T0), new(2, 3, T0.AddDays(5)),
|
||||
};
|
||||
var g = CoMovementGraphBuilder.Build(entries, windowHours: 48, minEdgeWeight: 3);
|
||||
Assert.Empty(g.Edges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectedComponentsFormClusters()
|
||||
{
|
||||
// 1-2-3 chained into one cluster; 4-5 a separate cluster.
|
||||
var entries = CoEnter(1, 2, 3, 0)
|
||||
.Concat(CoEnter(2, 3, 3, 50))
|
||||
.Concat(CoEnter(4, 5, 3, 100))
|
||||
.ToList();
|
||||
|
||||
var g = CoMovementGraphBuilder.Build(entries, minEdgeWeight: 3);
|
||||
|
||||
Assert.Equal(g.ClusterByTrader[1], g.ClusterByTrader[2]);
|
||||
Assert.Equal(g.ClusterByTrader[2], g.ClusterByTrader[3]);
|
||||
Assert.Equal(g.ClusterByTrader[4], g.ClusterByTrader[5]);
|
||||
Assert.NotEqual(g.ClusterByTrader[1], g.ClusterByTrader[4]);
|
||||
Assert.Equal(2, g.ClusterByTrader.Values.Distinct().Count());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user