152 lines
7.1 KiB
C#
152 lines
7.1 KiB
C#
using Predictalytics.Domain.Entities;
|
|
using Predictalytics.Domain.Interfaces;
|
|
using Predictalytics.Infrastructure.Providers.Polymarket;
|
|
using Predictalytics.Application.Interfaces;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Predictalytics.Worker.Services;
|
|
|
|
/// <summary>
|
|
/// Retroactively enriches trades of top/watchlisted traders with high-resolution
|
|
/// 1-minute price contexts immediately before and after execution.
|
|
/// Avoids burdening the live PollingWorker.
|
|
/// </summary>
|
|
public class TradeContextEnrichmentWorker : BackgroundService
|
|
{
|
|
private readonly IServiceProvider _services;
|
|
private readonly ILogger<TradeContextEnrichmentWorker> _logger;
|
|
|
|
public TradeContextEnrichmentWorker(IServiceProvider services, ILogger<TradeContextEnrichmentWorker> logger)
|
|
{
|
|
_services = services;
|
|
_logger = logger;
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
_logger.LogInformation("🧠 TradeContextEnrichmentWorker started");
|
|
await Task.Delay(10000, stoppingToken); // Wait for app startup
|
|
|
|
while (!stoppingToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
using var scope = _services.CreateScope();
|
|
var tradeRepo = scope.ServiceProvider.GetRequiredService<ITradeRepository>();
|
|
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(50, stoppingToken);
|
|
|
|
if (unenrichedTrades.Count == 0)
|
|
{
|
|
// No work to do, sleep longer
|
|
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
|
continue;
|
|
}
|
|
|
|
// 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);
|
|
|
|
int updatedCount = 0;
|
|
|
|
foreach (var group in tradesByAsset)
|
|
{
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
var assetId = group.Key;
|
|
|
|
try
|
|
{
|
|
// Wait for rate limiter to respect global limits
|
|
await rateLimiter.WaitAsync(Predictalytics.Domain.Enums.PlatformType.Polymarket, stoppingToken);
|
|
|
|
// We still fetch history for PriceBefore1m
|
|
var history = await polymarketClient.GetPricesHistoryAsync(assetId, "max", stoppingToken);
|
|
var orderedHistory = history?.OrderBy(h => h.Timestamp).ToList() ?? new List<PriceHistoryEntry>();
|
|
|
|
foreach (var trade in group)
|
|
{
|
|
var tradeTimeUnix = ((DateTimeOffset)trade.ExecutedAt).ToUnixTimeSeconds();
|
|
|
|
// Find the closest point BEFORE the trade (approx 1 min before)
|
|
var prePoint = orderedHistory
|
|
.LastOrDefault(h => h.Timestamp < tradeTimeUnix);
|
|
|
|
// Find the closest point AFTER the trade (approx 1 min after) - old logic
|
|
var postPoint = orderedHistory
|
|
.FirstOrDefault(h => h.Timestamp > tradeTimeUnix);
|
|
|
|
var prePrice = prePoint != null ? (decimal?)prePoint.Price : null;
|
|
trade.PreTradePrice1m = prePrice;
|
|
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)
|
|
};
|
|
|
|
await tradeRepo.UpdateAsync(trade, stoppingToken);
|
|
updatedCount++;
|
|
}
|
|
}
|
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
|
{
|
|
_logger.LogInformation("Cancellation requested during enrichment, stopping batch.");
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to enrich asset {AssetId}", assetId);
|
|
// Do NOT mark as enriched on failure, try again later
|
|
}
|
|
}
|
|
|
|
_logger.LogInformation("✅ Enriched {UpdatedCount} trades in this cycle.", updatedCount);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error in TradeContextEnrichmentWorker loop");
|
|
}
|
|
|
|
// Sleep briefly before next batch
|
|
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
|
|
}
|
|
|
|
_logger.LogInformation("🧠 TradeContextEnrichmentWorker stopped");
|
|
}
|
|
|
|
private static Predictalytics.Domain.Enums.OrderType DetermineOrderType(Trade trade, decimal? priceBefore)
|
|
{
|
|
if (priceBefore == null) return Predictalytics.Domain.Enums.OrderType.Unknown;
|
|
|
|
if (trade.Side == Predictalytics.Domain.Enums.TradeSide.Buy)
|
|
{
|
|
return trade.Price <= priceBefore.Value ? Predictalytics.Domain.Enums.OrderType.Maker : Predictalytics.Domain.Enums.OrderType.Taker;
|
|
}
|
|
else if (trade.Side == Predictalytics.Domain.Enums.TradeSide.Sell)
|
|
{
|
|
return trade.Price >= priceBefore.Value ? Predictalytics.Domain.Enums.OrderType.Maker : Predictalytics.Domain.Enums.OrderType.Taker;
|
|
}
|
|
|
|
return Predictalytics.Domain.Enums.OrderType.Unknown;
|
|
}
|
|
}
|