Fix Aggregated-tier mutable-bucket vs checkpoint bug

The Aggregated ingest tier bucketed trades by hour incl. the current, still-growing
hour, then upserted via ON DUPLICATE KEY UPDATE (mutable rows). The PnL engine
checkpoints positions by row Id, so a bucket that keeps growing after being applied
had its later growth silently skipped (Id <= LastAppliedTradeId).

Extract the duplicated aggregation logic from PollingWorker + TradeHistoryWorker into
TradeAggregation.AggregateCompletedHours, which only aggregates COMPLETED hours; the
current hour is deferred (re-fetched next cycle) so every persisted aggregate is
immutable. +3 unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-07-20 08:57:46 +02:00
co-authored by Claude Opus 4.8
parent 3ed0b4df27
commit be1b90b556
4 changed files with 154 additions and 55 deletions
@@ -258,34 +258,11 @@ public class TradeHistoryWorker : BackgroundService
if (trader.IngestMode == IngestMode.Aggregated && newTrades.Count > 0)
{
// Aggregate trades: Bucket (TraderId, MarketOutcomeId, Side, Stunde)
var aggregated = new List<Domain.Entities.Trade>();
foreach (var grp in newTrades.GroupBy(t => new { t.MarketOutcomeId, t.Side, Hour = t.ExecutedAt.ToString("yyyyMMddHH") }))
{
var first = grp.First();
var totalAmount = grp.Sum(t => t.Amount);
var totalSize = grp.Sum(t => t.Size);
var vwap = totalSize > 0 ? totalAmount / totalSize : first.Price;
var aggTrade = new Domain.Entities.Trade
{
PlatformTradeId = $"AGG_{trader.Id}_{grp.Key.MarketOutcomeId}_{grp.Key.Side}_{grp.Key.Hour}",
TraderId = trader.Id,
MarketOutcomeId = first.MarketOutcomeId,
DbMarketId = first.DbMarketId,
MarketId = first.MarketId,
AssetId = first.AssetId,
Outcome = first.Outcome,
Side = first.Side,
Price = vwap,
Amount = totalAmount,
Size = totalSize,
ExecutedAt = first.ExecutedAt,
AggregatedCount = grp.Count()
};
aggregated.Add(aggTrade);
}
newTrades = aggregated;
// Only completed hours are aggregated+persisted; the current (growing)
// hour is deferred so aggregate rows stay immutable for the checkpoint
// engine. See TradeAggregation.AggregateCompletedHours.
newTrades = Predictalytics.Application.Services.TradeAggregation
.AggregateCompletedHours(newTrades, trader.Id, DateTime.UtcNow);
}
if (isWeeklyBiopsy && newTrades.Count > 0)