197 lines
9.7 KiB
C#
197 lines
9.7 KiB
C#
using Predictalytics.Domain.Entities;
|
|
using Predictalytics.Domain.Enums;
|
|
using Predictalytics.Domain.Interfaces;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Predictalytics.Infrastructure.Data.Repositories;
|
|
|
|
public class TradeRepository : ITradeRepository
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly Microsoft.Extensions.Logging.ILogger<TradeRepository> _logger;
|
|
|
|
public TradeRepository(AppDbContext db, Microsoft.Extensions.Logging.ILogger<TradeRepository> logger)
|
|
{
|
|
_db = db;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<Trade?> GetByPlatformTradeIdAsync(PlatformType platform, string platformTradeId, CancellationToken ct = default)
|
|
=> await _db.Trades.FirstOrDefaultAsync(t => t.Platform == platform && t.PlatformTradeId == platformTradeId, ct);
|
|
|
|
public async Task<IReadOnlyList<Trade>> GetByTraderIdAsync(int traderId, int skip = 0, int take = 50, CancellationToken ct = default)
|
|
=> await _db.Trades.Include(t => t.Trader).Include(t => t.DbMarket).Where(t => t.TraderId == traderId && t.DbMarketId != null)
|
|
.OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct);
|
|
|
|
public async Task<IReadOnlyList<Trade>> GetByDbMarketIdAsync(int dbMarketId, int skip = 0, int take = 50, CancellationToken ct = default)
|
|
=> await _db.Trades.Include(t => t.Trader).Include(t => t.DbMarket).Where(t => t.DbMarketId == dbMarketId)
|
|
.OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct);
|
|
|
|
public async Task<IReadOnlyList<Trade>> GetByMarketIdAsync(string platformMarketId, int skip = 0, int take = 50, CancellationToken ct = default)
|
|
=> await _db.Trades.Include(t => t.Trader).Include(t => t.DbMarket).Where(t => t.MarketId == platformMarketId && t.DbMarketId != null)
|
|
.OrderByDescending(t => t.ExecutedAt).Skip(skip).Take(take).ToListAsync(ct);
|
|
|
|
public async Task<IReadOnlyList<Trade>> GetRecentAsync(int count = 50, PlatformType? platform = null, CancellationToken ct = default)
|
|
{
|
|
var q = _db.Trades.Include(t => t.Trader).Include(t => t.DbMarket).Where(t => t.DbMarketId != null).AsQueryable();
|
|
if (platform.HasValue) q = q.Where(t => t.Platform == platform.Value);
|
|
return await q.OrderByDescending(t => t.ExecutedAt).Take(count).ToListAsync(ct);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<Trade>> GetLargestAsync(int count = 5, DateTime? since = null, CancellationToken ct = default)
|
|
{
|
|
var q = _db.Trades.Include(t => t.Trader).Include(t => t.DbMarket).Where(t => t.DbMarketId != null).AsQueryable();
|
|
if (since.HasValue) q = q.Where(t => t.ExecutedAt >= since.Value);
|
|
return await q.OrderByDescending(t => t.Amount).Take(count).ToListAsync(ct);
|
|
}
|
|
|
|
public async Task<int> GetCountAsync(int? traderId = null, CancellationToken ct = default)
|
|
{
|
|
var q = _db.Trades.AsQueryable();
|
|
if (traderId.HasValue) q = q.Where(t => t.TraderId == traderId.Value);
|
|
return await q.CountAsync(ct);
|
|
}
|
|
|
|
public async Task AddRangeAsync(IEnumerable<Trade> trades, CancellationToken ct = default)
|
|
{
|
|
var tradeList = trades.ToList();
|
|
if (tradeList.Count == 0) return;
|
|
|
|
foreach (var t in tradeList)
|
|
{
|
|
t.Outcome = StringHelper.Truncate(t.Outcome, 128) ?? "";
|
|
t.PlatformTradeId = StringHelper.Truncate(t.PlatformTradeId, 256) ?? "";
|
|
t.MarketId = StringHelper.Truncate(t.MarketId, 66) ?? "";
|
|
t.AssetId = StringHelper.Truncate(t.AssetId, 80) ?? "";
|
|
if (t.TransactionHash != null)
|
|
t.TransactionHash = StringHelper.Truncate(t.TransactionHash, 66);
|
|
}
|
|
|
|
foreach (var chunk in tradeList.Chunk(500))
|
|
{
|
|
var sb = new System.Text.StringBuilder("INSERT INTO Trades (PlatformTradeId, MarketId, AssetId, Outcome, Side, Price, Size, Amount, ExecutedAt, TransactionHash, TraderId, MarketOutcomeId, DbMarketId, Platform, IsContextEnriched, AggregatedCount, UsdcSize, OutcomeIndex) VALUES ");
|
|
var parameters = new List<object>();
|
|
|
|
for (int i = 0; i < chunk.Length; i++)
|
|
{
|
|
var t = chunk[i];
|
|
int pIdx = i * 18;
|
|
sb.Append($"({{{pIdx}}}, {{{pIdx + 1}}}, {{{pIdx + 2}}}, {{{pIdx + 3}}}, {{{pIdx + 4}}}, {{{pIdx + 5}}}, {{{pIdx + 6}}}, {{{pIdx + 7}}}, {{{pIdx + 8}}}, {{{pIdx + 9}}}, {{{pIdx + 10}}}, {{{pIdx + 11}}}, {{{pIdx + 12}}}, {{{pIdx + 13}}}, {{{pIdx + 14}}}, {{{pIdx + 15}}}, {{{pIdx + 16}}}, {{{pIdx + 17}}})");
|
|
|
|
if (i < chunk.Length - 1)
|
|
sb.Append(", ");
|
|
|
|
parameters.Add(t.PlatformTradeId);
|
|
parameters.Add(t.MarketId);
|
|
parameters.Add(t.AssetId);
|
|
parameters.Add(t.Outcome);
|
|
parameters.Add((int)t.Side);
|
|
parameters.Add(t.Price);
|
|
parameters.Add(t.Size);
|
|
parameters.Add(t.Amount);
|
|
parameters.Add(t.ExecutedAt);
|
|
parameters.Add(t.TransactionHash ?? (object?)null);
|
|
parameters.Add(t.TraderId);
|
|
parameters.Add(t.MarketOutcomeId ?? (object?)null);
|
|
parameters.Add(t.DbMarketId ?? (object?)null);
|
|
parameters.Add((int)t.Platform);
|
|
parameters.Add(t.IsContextEnriched);
|
|
parameters.Add(t.AggregatedCount ?? (object?)null);
|
|
parameters.Add(t.UsdcSize ?? (object?)null);
|
|
parameters.Add(t.OutcomeIndex ?? (object?)null);
|
|
}
|
|
|
|
// For Aggregated Trades, we want UPSERT logic to update size, amount and VWAP
|
|
sb.Append(" ON DUPLICATE KEY UPDATE Price=VALUES(Price), Size=VALUES(Size), Amount=VALUES(Amount), AggregatedCount=VALUES(AggregatedCount), UsdcSize=VALUES(UsdcSize), OutcomeIndex=VALUES(OutcomeIndex);");
|
|
|
|
int maxRetries = 3;
|
|
var backoffs = new[] { 250, 500, 1000 };
|
|
for (int retry = 0; retry <= maxRetries; retry++)
|
|
{
|
|
try
|
|
{
|
|
var rowsInserted = await _db.Database.ExecuteSqlRawAsync(sb.ToString(), parameters.ToArray(), ct);
|
|
_logger.LogInformation("Inserted {RowsInserted} trades into the database.", rowsInserted);
|
|
break;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var mysqlEx = ex as MySqlConnector.MySqlException ?? ex.InnerException as MySqlConnector.MySqlException;
|
|
if (mysqlEx != null && (mysqlEx.Number == 1213 || mysqlEx.Number == 1205))
|
|
{
|
|
if (retry == maxRetries)
|
|
{
|
|
_logger.LogError(ex, "Failed to insert {Count} trades after {Retries} retries due to deadlocks.", chunk.Length, maxRetries);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("Deadlock detected during trade insertion. Retrying in {Delay}ms... (Attempt {Attempt}/{Max})", backoffs[retry], retry + 1, maxRetries);
|
|
await Task.Delay(backoffs[retry], ct);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task<decimal> GetTotalVolumeAsync(DateTime? since = null, CancellationToken ct = default)
|
|
{
|
|
var q = _db.Trades.AsQueryable();
|
|
if (since.HasValue) q = q.Where(t => t.ExecutedAt >= since.Value);
|
|
return await q.SumAsync(t => t.Amount, ct);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<Trade>> GetOrphanedTradesAsync(int limit, CancellationToken ct = default)
|
|
{
|
|
return await _db.Trades
|
|
.Where(t => t.MarketOutcomeId == null && !string.IsNullOrEmpty(t.AssetId))
|
|
.OrderByDescending(t => t.ExecutedAt)
|
|
.Take(limit)
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
public async Task<HashSet<string>> GetKnownPlatformTradeIdsAsync(PlatformType platform, int traderId, IEnumerable<string> platformTradeIds, CancellationToken ct = default)
|
|
{
|
|
var idList = platformTradeIds.ToList();
|
|
if (idList.Count == 0) return new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var ids = await _db.Trades
|
|
.Where(t => t.Platform == platform && idList.Contains(t.PlatformTradeId))
|
|
.Select(t => t.PlatformTradeId)
|
|
.ToListAsync(ct);
|
|
return new HashSet<string>(ids, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public async Task UpdateAsync(Trade trade, CancellationToken ct = default)
|
|
{
|
|
trade.Outcome = StringHelper.Truncate(trade.Outcome, 128) ?? "";
|
|
trade.PlatformTradeId = StringHelper.Truncate(trade.PlatformTradeId, 256) ?? "";
|
|
trade.MarketId = StringHelper.Truncate(trade.MarketId, 66) ?? "";
|
|
trade.AssetId = StringHelper.Truncate(trade.AssetId, 80) ?? "";
|
|
|
|
_db.Trades.Update(trade);
|
|
await _db.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task<IReadOnlyList<Trade>> GetTradesForContextEnrichmentAsync(int limit, CancellationToken ct = default)
|
|
{
|
|
return await _db.Trades
|
|
.Include(t => t.Trader)
|
|
.Include(t => t.Trader.CurrentScore)
|
|
.Include(t => t.Trader.WatchlistEntries)
|
|
.Include(t => t.Trader.Analytics)
|
|
.Where(t => !t.IsContextEnriched
|
|
&& t.Platform == PlatformType.Polymarket
|
|
&& t.AssetId != "")
|
|
.Where(t => t.Trader.WatchlistEntries.Any() || (t.Trader.Analytics != null && t.Trader.Analytics.CopytradingScore > 50))
|
|
.OrderByDescending(t => t.ExecutedAt)
|
|
.Take(limit)
|
|
.ToListAsync(ct);
|
|
}
|
|
}
|