From f3a166400e0ab511aa296620837cb8c050fc14a5 Mon Sep 17 00:00:00 2001 From: parthod0x Date: Sun, 25 Oct 2026 08:50:54 +0000 Subject: [PATCH] test(oracle): cover price aggregation math (median, twap, deviation) The oracle price-aggregation helpers (calculateMedian, calculateTWAP, calculateDeviationBps) compute the aggregated AKT/USD price that gates the BME mint circuit breaker and the escrow AKT settlement fallback, but had no test coverage. Add internal (package keeper) unit tests pinning their behaviour: - calculateMedian: empty -> zero; odd -> middle; even -> average of the two middle values (unsorted input exercises the sort). - calculateDeviationBps: zero-min guard; (max-min)/min in basis points; no deviation -> 0. - calculateTWAP: empty -> error; single point -> that price; descending points time-weighted; identical timestamps fall back to the simple average. Test-only; the helpers are unchanged. Signed-off-by: parthod0x --- x/oracle/keeper/aggregation_math_test.go | 101 +++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 x/oracle/keeper/aggregation_math_test.go diff --git a/x/oracle/keeper/aggregation_math_test.go b/x/oracle/keeper/aggregation_math_test.go new file mode 100644 index 000000000..808ba979c --- /dev/null +++ b/x/oracle/keeper/aggregation_math_test.go @@ -0,0 +1,101 @@ +package keeper + +import ( + "testing" + "time" + + sdkmath "cosmossdk.io/math" + "github.com/stretchr/testify/require" + + types "pkg.akt.dev/go/node/oracle/v2" +) + +func pd(ts time.Time, price string) types.PriceData { + return types.PriceData{ + ID: types.PriceDataRecordID{Timestamp: ts}, + State: types.PriceDataState{Price: sdkmath.LegacyMustNewDecFromStr(price)}, + } +} + +func TestCalculateMedian(t *testing.T) { + t.Run("empty returns zero", func(t *testing.T) { + require.True(t, calculateMedian(nil).IsZero()) + }) + + t.Run("odd count returns middle value", func(t *testing.T) { + now := time.Now() + // deliberately unsorted input to exercise the internal sort + in := []types.PriceData{pd(now, "30"), pd(now, "10"), pd(now, "20")} + require.True(t, sdkmath.LegacyNewDec(20).Equal(calculateMedian(in)), + "median of {10,20,30} must be 20, got %s", calculateMedian(in)) + }) + + t.Run("even count averages the two middle values", func(t *testing.T) { + now := time.Now() + in := []types.PriceData{pd(now, "10"), pd(now, "40"), pd(now, "20"), pd(now, "30")} + // sorted: 10,20,30,40 -> (20+30)/2 = 25 + require.True(t, sdkmath.LegacyNewDec(25).Equal(calculateMedian(in)), + "median of {10,20,30,40} must be 25, got %s", calculateMedian(in)) + }) +} + +func TestCalculateDeviationBps(t *testing.T) { + t.Run("zero min returns zero", func(t *testing.T) { + require.Equal(t, uint64(0), + calculateDeviationBps(sdkmath.LegacyZeroDec(), sdkmath.LegacyNewDec(100))) + }) + + t.Run("computes (max-min)/min in basis points", func(t *testing.T) { + // (110-100)/100 * 10000 = 1000 bps + require.Equal(t, uint64(1000), + calculateDeviationBps(sdkmath.LegacyNewDec(100), sdkmath.LegacyNewDec(110))) + }) + + t.Run("no deviation returns zero", func(t *testing.T) { + require.Equal(t, uint64(0), + calculateDeviationBps(sdkmath.LegacyNewDec(100), sdkmath.LegacyNewDec(100))) + }) +} + +func TestCalculateTWAP(t *testing.T) { + base := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) + + t.Run("empty returns error", func(t *testing.T) { + _, err := calculateTWAP(base, nil) + require.Error(t, err) + }) + + t.Run("single point weighted by elapsed time returns that price", func(t *testing.T) { + now := base.Add(30 * time.Second) + in := []types.PriceData{pd(base.Add(20*time.Second), "100")} + twap, err := calculateTWAP(now, in) + require.NoError(t, err) + require.True(t, sdkmath.LegacyNewDec(100).Equal(twap), "got %s", twap) + }) + + t.Run("descending points weighted by time each price prevails", func(t *testing.T) { + // now = :30 + // newest @ :20 price 100 -> weight (now - :20) = 10s + // older @ :00 price 220 -> weight (:20 - :00) = 20s + // TWAP = (100*10 + 220*20) / 30 = 5400 / 30 = 180 + now := base.Add(30 * time.Second) + in := []types.PriceData{ + pd(base.Add(20*time.Second), "100"), + pd(base, "220"), + } + twap, err := calculateTWAP(now, in) + require.NoError(t, err) + require.True(t, sdkmath.LegacyNewDec(180).Equal(twap), + "time-weighted average must be 180, got %s", twap) + }) + + t.Run("identical timestamps fall back to simple average", func(t *testing.T) { + // all points share the block time -> totalWeight == 0 -> simple average + now := base + in := []types.PriceData{pd(base, "100"), pd(base, "200")} + twap, err := calculateTWAP(now, in) + require.NoError(t, err) + require.True(t, sdkmath.LegacyNewDec(150).Equal(twap), + "fallback average of {100,200} must be 150, got %s", twap) + }) +}