Files
maker/rebalance_test.go
zeekayandHanzo Dev b52cb4efc5 coherence: cross-venue reconciler + position rebalance + V4 discovery
Venue reconciler pegs every live venue (0x9999 book, V3 slot0, V4 via
state.json) to the Kraken oracle; drift past band => bounded corrective
trade with notional caps + inventory floors. Rebalance re-centers ops
range positions on oracle as price moves (rate-limited, idempotent). V4
pools auto-discovered from the migration's state.json (absent => book+V3
only). Tests: reconcile/rebalance/v4state all pass.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-04 20:48:56 -07:00

101 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package maker
import (
"math"
"testing"
"time"
)
// TestDecideRebalance drives the PURE recenter decision: inside-range holds, an
// edge/outside price recenters, and the rate-limit + min-drift gates suppress churn.
func TestDecideRebalance(t *testing.T) {
cfg := RebalanceConfig{
EdgeBps: 100, WidthBps: 500, MinInterval: time.Hour, MinDriftBps: 50, Execute: false,
}
const never = time.Duration(1 << 62)
cases := []struct {
name string
pool, low, high, ref, lastCenter float64
since time.Duration
wantRebalance bool
}{
// Comfortably inside [95,105] at 100: hold.
{"inside-holds", 100, 95, 105, 100, 0, never, false},
// Near the high edge (within 100 bps of 105): recenter.
{"near-high-recenters", 104.9, 95, 105, 100, 0, never, true},
// Outside the range (below low): recenter.
{"below-low-recenters", 94, 95, 105, 100, 0, never, true},
// At edge but recentered 1 minute ago: rate-limited hold.
{"rate-limited-holds", 104.9, 95, 105, 100, 100, time.Minute, false},
// At edge, enough time passed, but oracle barely moved from last center (10
// bps < 50): insufficient-drift hold.
{"low-drift-holds", 104.9, 95, 105, 100.1, 100, 2 * time.Hour, false},
// At edge, enough time, oracle drifted 200 bps from last center: recenter.
{"drifted-recenters", 104.9, 95, 105, 102, 100, 2 * time.Hour, true},
// Degenerate range: hold (never act on bad inputs).
{"bad-range-holds", 100, 105, 95, 100, 0, never, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
act := decideRebalance(c.pool, c.low, c.high, c.ref, c.lastCenter, c.since, cfg)
if act.Rebalance != c.wantRebalance {
t.Fatalf("rebalance = %v, want %v (reason: %s)", act.Rebalance, c.wantRebalance, act.Reason)
}
if act.Rebalance {
wantLow := c.ref * (1 - cfg.WidthBps/1e4)
wantHigh := c.ref * (1 + cfg.WidthBps/1e4)
if !closeF(act.NewLow, wantLow, 1e-9) || !closeF(act.NewHigh, wantHigh, 1e-9) {
t.Errorf("new range [%v,%v], want [%v,%v]", act.NewLow, act.NewHigh, wantLow, wantHigh)
}
}
})
}
}
// TestTickPriceRoundTrip proves priceToTick∘tickToPrice recovers a price to within
// one tick-spacing step, for both orientations (base==token0 and base==token1). This
// is the load-bearing conversion the live recenter's mint bounds depend on.
func TestTickPriceRoundTrip(t *testing.T) {
for _, baseIsToken0 := range []bool{true, false} {
for _, price := range []float64{0.5, 1.0, 21.75, 3800, 0.00061} {
tick := priceToTick(price, 18, 18, baseIsToken0, 1) // spacing 1 = finest grid
back := tickToPrice(tick, 18, 18, baseIsToken0)
// One tick is 1.0001×, i.e. 1 bp; allow a couple bps for rounding.
if rel := math.Abs(back-price) / price; rel > 3e-4 {
t.Errorf("base0=%v price %v -> tick %d -> %v (rel err %.6f)", baseIsToken0, price, tick, back, rel)
}
}
}
}
// TestTickPriceOrientation proves the orientation is honored: when base is token0 a
// higher tick means a higher quote-per-base price; when base is token1 it inverts.
func TestTickPriceOrientation(t *testing.T) {
// base==token0: monotone increasing in tick.
if !(tickToPrice(1000, 18, 18, true) > tickToPrice(0, 18, 18, true)) {
t.Error("base==token0: price must increase with tick")
}
// base==token1: monotone decreasing in tick.
if !(tickToPrice(1000, 18, 18, false) < tickToPrice(0, 18, 18, false)) {
t.Error("base==token1: price must decrease with tick")
}
}
// TestRebalanceStateRateLimit proves the state ledger reports "never" before any
// recenter (no rate-limit) and the elapsed time after a mark.
func TestRebalanceStateRateLimit(t *testing.T) {
st := newRebalanceState()
now := time.Date(2026, 7, 4, 12, 0, 0, 0, time.UTC)
if since, center := st.get("42", now); since < time.Hour*24*365 || center != 0 {
t.Fatalf("fresh position: since=%v center=%v, want large + 0", since, center)
}
st.mark("42", 100.0, now.Add(-30*time.Minute))
if since, center := st.get("42", now); since != 30*time.Minute || center != 100 {
t.Fatalf("after mark: since=%v center=%v, want 30m + 100", since, center)
}
}