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:
co-authored by
Claude Opus 4.8
parent
3ed0b4df27
commit
be1b90b556
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Predictalytics.Application.Services;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Predictalytics.Application.Tests.Services;
|
||||
|
||||
public class TradeAggregationTests
|
||||
{
|
||||
private static Trade Raw(int outcomeId, TradeSide side, decimal price, decimal size, DateTime at)
|
||||
=> new()
|
||||
{
|
||||
MarketOutcomeId = outcomeId, DbMarketId = 10, MarketId = "cond", AssetId = "asset",
|
||||
Outcome = "Yes", Side = side, Price = price, Size = size, Amount = price * size, ExecutedAt = at
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void AggregateCompletedHours_SumsCompletedHourWithVwap()
|
||||
{
|
||||
var now = new DateTime(2026, 07, 13, 15, 30, 0, DateTimeKind.Utc);
|
||||
var completedHour = new DateTime(2026, 07, 13, 10, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
var trades = new List<Trade>
|
||||
{
|
||||
Raw(100, TradeSide.Buy, 0.40m, 50m, completedHour.AddMinutes(5)),
|
||||
Raw(100, TradeSide.Buy, 0.60m, 50m, completedHour.AddMinutes(45)),
|
||||
};
|
||||
|
||||
var result = TradeAggregation.AggregateCompletedHours(trades, traderId: 7, nowUtc: now);
|
||||
|
||||
var agg = Assert.Single(result);
|
||||
Assert.Equal("AGG_7_100_Buy_2026071310", agg.PlatformTradeId);
|
||||
Assert.Equal(100m, agg.Size); // 50 + 50
|
||||
Assert.Equal(50m, agg.Amount); // 0.40*50 + 0.60*50 = 20 + 30
|
||||
Assert.Equal(0.50m, agg.Price); // VWAP = 50/100
|
||||
Assert.Equal(2, agg.AggregatedCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AggregateCompletedHours_DefersCurrentHour()
|
||||
{
|
||||
// The bug this guards against: the current, still-growing hour must NOT be aggregated,
|
||||
// because its aggregate row is mutable-by-key while the PnL engine checkpoints by Id.
|
||||
var now = new DateTime(2026, 07, 13, 15, 30, 0, DateTimeKind.Utc);
|
||||
|
||||
var trades = new List<Trade>
|
||||
{
|
||||
// completed hour -> aggregated
|
||||
Raw(100, TradeSide.Buy, 0.50m, 40m, new DateTime(2026, 07, 13, 14, 10, 0, DateTimeKind.Utc)),
|
||||
// current hour (15:xx) -> deferred
|
||||
Raw(100, TradeSide.Buy, 0.50m, 99m, new DateTime(2026, 07, 13, 15, 05, 0, DateTimeKind.Utc)),
|
||||
};
|
||||
|
||||
var result = TradeAggregation.AggregateCompletedHours(trades, traderId: 7, nowUtc: now);
|
||||
|
||||
Assert.Single(result); // only the 14:00 bucket
|
||||
Assert.EndsWith("_2026071314", result[0].PlatformTradeId);
|
||||
Assert.Equal(40m, result[0].Size); // current-hour 99 shares NOT included
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AggregateCompletedHours_SeparatesOutcomeAndSide()
|
||||
{
|
||||
var now = new DateTime(2026, 07, 13, 15, 0, 0, DateTimeKind.Utc);
|
||||
var h = new DateTime(2026, 07, 13, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
var trades = new List<Trade>
|
||||
{
|
||||
Raw(100, TradeSide.Buy, 0.50m, 10m, h),
|
||||
Raw(100, TradeSide.Sell, 0.50m, 10m, h),
|
||||
Raw(101, TradeSide.Buy, 0.50m, 10m, h),
|
||||
};
|
||||
|
||||
var result = TradeAggregation.AggregateCompletedHours(trades, traderId: 7, nowUtc: now);
|
||||
|
||||
Assert.Equal(3, result.Count); // (100,Buy), (100,Sell), (101,Buy) are distinct buckets
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user