feat: Implement copytrading analytics and update WebUI
- Add v4 Tape-based CopytradingEstimator (Clustering, MAE, Tape Fills) - Add CopytradingBacktestHarness (Walk-Forward testing, Shrinkage, LCB ranking) - Add EF Core Migrations for FollowerFillPrice and Scoring fields - Update WebUI: Fix Trader Detail tab layout blowout - Update WebUI: Redesign Trader Detail menubar - Update WebUI: Display separate Quality and Copyability metric cards - Update WebUI: Add 'Highly Copyable' filter to Trader List
This commit is contained in:
@@ -38,8 +38,10 @@ public class TradeContextEnrichmentWorker : BackgroundService
|
||||
var polymarketClient = scope.ServiceProvider.GetRequiredService<PolymarketApiClient>();
|
||||
var rateLimiter = scope.ServiceProvider.GetRequiredService<IRateLimiter>();
|
||||
|
||||
var estimator = scope.ServiceProvider.GetRequiredService<ICopytradingEstimator>();
|
||||
|
||||
// Fetch a batch of unenriched trades
|
||||
var unenrichedTrades = await tradeRepo.GetTradesForContextEnrichmentAsync(500, stoppingToken);
|
||||
var unenrichedTrades = await tradeRepo.GetTradesForContextEnrichmentAsync(50, stoppingToken);
|
||||
|
||||
if (unenrichedTrades.Count == 0)
|
||||
{
|
||||
@@ -48,7 +50,7 @@ public class TradeContextEnrichmentWorker : BackgroundService
|
||||
continue;
|
||||
}
|
||||
|
||||
// Group by AssetId to minimize API calls (1 call per asset fetches the whole 1m history)
|
||||
// Group by AssetId to minimize API calls for the history endpoint (if we still use it)
|
||||
var tradesByAsset = unenrichedTrades.GroupBy(t => t.AssetId).ToList();
|
||||
_logger.LogInformation("Enriching {TradeCount} trades across {AssetCount} assets...", unenrichedTrades.Count, tradesByAsset.Count);
|
||||
|
||||
@@ -64,22 +66,9 @@ public class TradeContextEnrichmentWorker : BackgroundService
|
||||
// Wait for rate limiter to respect global limits
|
||||
await rateLimiter.WaitAsync(Predictalytics.Domain.Enums.PlatformType.Polymarket, stoppingToken);
|
||||
|
||||
// Fetch the 10-minute history for the entire market (using "max" since "1m" is invalid for full history)
|
||||
// We still fetch history for PriceBefore1m
|
||||
var history = await polymarketClient.GetPricesHistoryAsync(assetId, "max", stoppingToken);
|
||||
if (history == null || history.Count == 0)
|
||||
{
|
||||
// If history is not available, mark as enriched to prevent infinite loops,
|
||||
// but prices remain null.
|
||||
foreach (var trade in group)
|
||||
{
|
||||
trade.IsContextEnriched = true;
|
||||
await tradeRepo.UpdateAsync(trade, stoppingToken);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Order history chronologically for safe binary search / LINQ
|
||||
var orderedHistory = history.OrderBy(h => h.Timestamp).ToList();
|
||||
var orderedHistory = history?.OrderBy(h => h.Timestamp).ToList() ?? new List<PriceHistoryEntry>();
|
||||
|
||||
foreach (var trade in group)
|
||||
{
|
||||
@@ -89,7 +78,7 @@ public class TradeContextEnrichmentWorker : BackgroundService
|
||||
var prePoint = orderedHistory
|
||||
.LastOrDefault(h => h.Timestamp < tradeTimeUnix);
|
||||
|
||||
// Find the closest point AFTER the trade (approx 1 min after)
|
||||
// Find the closest point AFTER the trade (approx 1 min after) - old logic
|
||||
var postPoint = orderedHistory
|
||||
.FirstOrDefault(h => h.Timestamp > tradeTimeUnix);
|
||||
|
||||
@@ -98,12 +87,18 @@ public class TradeContextEnrichmentWorker : BackgroundService
|
||||
trade.PostTradePrice1m = postPoint != null ? (decimal?)postPoint.Price : null;
|
||||
trade.IsContextEnriched = true;
|
||||
|
||||
// NEW: Calculate exact follower fill prices from Trade Tape
|
||||
var followerFill10s = await estimator.EstimateFollowerFillPriceAsync(trade, 10, stoppingToken);
|
||||
var followerFill60s = await estimator.EstimateFollowerFillPriceAsync(trade, 60, stoppingToken);
|
||||
|
||||
// Populate new high-res TradeContext
|
||||
trade.Context = new TradeContext
|
||||
{
|
||||
TradeId = trade.Id,
|
||||
PriceBefore1m = prePrice,
|
||||
PriceAfter1m = trade.PostTradePrice1m,
|
||||
FollowerFillPrice10s = followerFill10s,
|
||||
FollowerFillPrice60s = followerFill60s,
|
||||
EstimatedSlippage = prePrice.HasValue ? Math.Abs(trade.Price - prePrice.Value) : null,
|
||||
EstimatedOrderType = DetermineOrderType(trade, prePrice)
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user