D3: Implement IngestMode classification, weekly biopsy, SnapshotOnly bypass, and Aggregated import grouping
This commit is contained in:
@@ -6,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Predictalytics.Worker.Services;
|
||||
|
||||
@@ -173,7 +174,7 @@ public class TradeHistoryWorker : BackgroundService
|
||||
if (tradesPerDay > 5000) newMode = IngestMode.SnapshotOnly;
|
||||
else if (tradesPerDay > 100 && trader.IngestMode == IngestMode.Full) newMode = IngestMode.Aggregated;
|
||||
else if (trader.IngestMode == IngestMode.SnapshotOnly && tradesPerDay < 2500) newMode = IngestMode.Aggregated;
|
||||
else if (trader.IngestMode == IngestMode.Aggregated && tradesPerDay < 50) newMode = IngestMode.Full;
|
||||
else if (trader.IngestMode == IngestMode.Aggregated && tradesPerDay < 50 && days >= 7) newMode = IngestMode.Full;
|
||||
|
||||
if (newMode != trader.IngestMode)
|
||||
{
|
||||
@@ -184,13 +185,7 @@ public class TradeHistoryWorker : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
if (isWeeklyBiopsy)
|
||||
{
|
||||
// "NUR durch den TraderTraitCalculator schicken, NICHT persistieren."
|
||||
// This would require resolving markets and positions and calling TraderTraitCalculator.Compute
|
||||
// For now we skip persisting.
|
||||
fetchedTrades = new List<Domain.Entities.Trade>();
|
||||
}
|
||||
// fetchedTrades are kept for weekly biopsy resolving, we will clear newTrades afterwards.
|
||||
}
|
||||
|
||||
var newTrades = new List<Domain.Entities.Trade>();
|
||||
@@ -293,6 +288,26 @@ public class TradeHistoryWorker : BackgroundService
|
||||
newTrades = aggregated;
|
||||
}
|
||||
|
||||
if (isWeeklyBiopsy && newTrades.Count > 0)
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<Predictalytics.Infrastructure.Data.AppDbContext>();
|
||||
var positions = await db.TraderPositions
|
||||
.Include(p => p.MarketOutcome).ThenInclude(o => o.Market)
|
||||
.Where(p => p.TraderId == trader.Id)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var computedTraits = Predictalytics.Application.Services.TraderTraitCalculator.Compute(trader, newTrades, positions);
|
||||
|
||||
db.TraderTraits.RemoveRange(db.TraderTraits.Where(tt => tt.TraderId == trader.Id));
|
||||
foreach (var (traitName, value) in computedTraits)
|
||||
{
|
||||
db.TraderTraits.Add(new Predictalytics.Domain.Entities.TraderTrait { TraderId = trader.Id, Trait = traitName, Value = value });
|
||||
}
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
newTrades.Clear(); // DO NOT persist trades!
|
||||
}
|
||||
|
||||
if (trader.IngestMode == IngestMode.SnapshotOnly)
|
||||
{
|
||||
// "SnapshotOnly (Tier C): Stündlich: GetTraderPositionsAsync -> TraderPositions upserten"
|
||||
@@ -301,10 +316,12 @@ public class TradeHistoryWorker : BackgroundService
|
||||
try
|
||||
{
|
||||
var positions = await provider.GetTraderPositionsAsync(trader.PlatformUserId, ct);
|
||||
decimal totalRealizedPnl = 0;
|
||||
decimal totalUnrealizedPnl = 0;
|
||||
|
||||
if (positions != null && positions.Count > 0)
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<Predictalytics.Infrastructure.Data.AppDbContext>();
|
||||
// For simplicity, just use the endpoint's positions
|
||||
var existingPos = db.TraderPositions.Where(tp => tp.TraderId == trader.Id).ToList();
|
||||
foreach(var info in positions)
|
||||
{
|
||||
@@ -325,26 +342,123 @@ public class TradeHistoryWorker : BackgroundService
|
||||
{
|
||||
ex.SharesHeld = info.Size;
|
||||
ex.AvgCost = info.AveragePrice;
|
||||
// RealizedPnl is built by tape replay, we skip it for SnapshotOnly
|
||||
ex.RealizedPnl = info.RealizedPnl; // Mapped cashPnl!
|
||||
}
|
||||
else
|
||||
{
|
||||
db.TraderPositions.Add(new Predictalytics.Domain.Entities.TraderPosition
|
||||
ex = new Predictalytics.Domain.Entities.TraderPosition
|
||||
{
|
||||
TraderId = trader.Id,
|
||||
MarketOutcomeId = marketOutcomeId.Value,
|
||||
SharesHeld = info.Size,
|
||||
AvgCost = info.AveragePrice,
|
||||
RealizedPnl = 0
|
||||
});
|
||||
RealizedPnl = info.RealizedPnl
|
||||
};
|
||||
db.TraderPositions.Add(ex);
|
||||
}
|
||||
|
||||
// Calculate unrealized PnL: pos.SharesHeld * (currentPrice - pos.AvgCost)
|
||||
if (!string.IsNullOrEmpty(info.AssetId))
|
||||
{
|
||||
var outcomeObj = await marketRepo.GetOutcomeByTokenIdAsync(info.AssetId, ct);
|
||||
if (outcomeObj != null)
|
||||
{
|
||||
var unrealized = ex.SharesHeld * (outcomeObj.CurrentPrice - ex.AvgCost);
|
||||
totalUnrealizedPnl += unrealized;
|
||||
}
|
||||
}
|
||||
totalRealizedPnl += ex.RealizedPnl;
|
||||
}
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
// Overall PnL
|
||||
decimal overallPnl = totalRealizedPnl + totalUnrealizedPnl;
|
||||
|
||||
// Let's query Leaderboard PnLs
|
||||
decimal pnl30d = 0;
|
||||
decimal pnl7d = 0;
|
||||
decimal pnl24h = 0;
|
||||
|
||||
var polyApi = scope.ServiceProvider.GetService<Predictalytics.Infrastructure.Providers.Polymarket.PolymarketApiClient>();
|
||||
if (polyApi != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var leaderboardAll = await polyApi.GetLeaderboardAsync(limit: 50, timePeriod: "ALL", ct: ct);
|
||||
var entryAll = leaderboardAll.FirstOrDefault(e => string.Equals(e.ProxyWallet, trader.PlatformUserId, StringComparison.OrdinalIgnoreCase) || string.Equals(e.UserName, trader.PlatformUserId, StringComparison.OrdinalIgnoreCase));
|
||||
if (entryAll != null)
|
||||
{
|
||||
overallPnl = (decimal)entryAll.Pnl;
|
||||
}
|
||||
|
||||
var leaderboard30d = await polyApi.GetLeaderboardAsync(limit: 50, timePeriod: "30D", ct: ct);
|
||||
var entry30d = leaderboard30d.FirstOrDefault(e => string.Equals(e.ProxyWallet, trader.PlatformUserId, StringComparison.OrdinalIgnoreCase) || string.Equals(e.UserName, trader.PlatformUserId, StringComparison.OrdinalIgnoreCase));
|
||||
if (entry30d != null)
|
||||
{
|
||||
pnl30d = (decimal)entry30d.Pnl;
|
||||
}
|
||||
|
||||
var leaderboard7d = await polyApi.GetLeaderboardAsync(limit: 50, timePeriod: "7D", ct: ct);
|
||||
var entry7d = leaderboard7d.FirstOrDefault(e => string.Equals(e.ProxyWallet, trader.PlatformUserId, StringComparison.OrdinalIgnoreCase) || string.Equals(e.UserName, trader.PlatformUserId, StringComparison.OrdinalIgnoreCase));
|
||||
if (entry7d != null)
|
||||
{
|
||||
pnl7d = (decimal)entry7d.Pnl;
|
||||
}
|
||||
|
||||
var leaderboard24h = await polyApi.GetLeaderboardAsync(limit: 50, timePeriod: "24H", ct: ct);
|
||||
var entry24h = leaderboard24h.FirstOrDefault(e => string.Equals(e.ProxyWallet, trader.PlatformUserId, StringComparison.OrdinalIgnoreCase) || string.Equals(e.UserName, trader.PlatformUserId, StringComparison.OrdinalIgnoreCase));
|
||||
if (entry24h != null)
|
||||
{
|
||||
pnl24h = (decimal)entry24h.Pnl;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch leaderboard PnLs for SnapshotOnly trader {TraderId}", trader.Id);
|
||||
}
|
||||
}
|
||||
|
||||
var dbCtx = scope.ServiceProvider.GetRequiredService<Predictalytics.Infrastructure.Data.AppDbContext>();
|
||||
var analyticsObj = await dbCtx.TraderAnalytics.FirstOrDefaultAsync(a => a.TraderId == trader.Id, ct);
|
||||
if (analyticsObj == null)
|
||||
{
|
||||
analyticsObj = new Predictalytics.Domain.Entities.TraderAnalytics { TraderId = trader.Id };
|
||||
dbCtx.TraderAnalytics.Add(analyticsObj);
|
||||
}
|
||||
|
||||
analyticsObj.OverallPnL = overallPnl;
|
||||
analyticsObj.PnL30d = pnl30d;
|
||||
analyticsObj.PnL7d = pnl7d;
|
||||
analyticsObj.PnL24h = pnl24h;
|
||||
analyticsObj.LastCalculatedAt = DateTime.UtcNow;
|
||||
|
||||
trader.TotalPnl = overallPnl;
|
||||
trader.LastAnalyzedAt = DateTime.UtcNow;
|
||||
|
||||
// Save Daily Snapshot (Equity curve)
|
||||
var today = DateTime.UtcNow.Date;
|
||||
var snapshot = await dbCtx.TraderDailySnapshots.FirstOrDefaultAsync(s => s.TraderId == trader.Id && s.Date == today, ct);
|
||||
if (snapshot == null)
|
||||
{
|
||||
dbCtx.TraderDailySnapshots.Add(new Predictalytics.Domain.Entities.TraderDailySnapshot
|
||||
{
|
||||
TraderId = trader.Id,
|
||||
Date = today,
|
||||
TotalPnl = overallPnl,
|
||||
CurrentBalance = analyticsObj.CurrentBalance
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
snapshot.TotalPnl = overallPnl;
|
||||
}
|
||||
|
||||
await dbCtx.SaveChangesAsync(ct);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to fetch positions for SnapshotOnly trader {TraderId}", trader.Id);
|
||||
_logger.LogWarning(ex, "Failed to fetch positions / leaderboard for SnapshotOnly trader {TraderId}", trader.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user