mirror of
https://github.com/luxfi/maker.git
synced 2026-10-30 16:20:04 +00:00
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]>
101 lines
4.2 KiB
Go
101 lines
4.2 KiB
Go
// 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)
|
||
}
|
||
}
|