package book import ( "fmt" "testing" "github.com/edwardtay/injective-booklab/dec" ) var D = dec.MustParse func lv(p, q string) Level { return Level{D(p), D(q)} } // Snapshot shape taken from INJ/USDT on mainnet, 2026-11-07. func sample() *Book { return &Book{ Seq: 73443633, Bids: []Level{lv("8.105", "53.846"), lv("8.084", "13.785"), lv("8.057", "17.81")}, Asks: []Level{lv("8.27", "52.766"), lv("8.291", "13.306"), lv("8.318", "17.034")}, } } func TestValidate(t *testing.T) { if err := sample().Validate(); err != nil { t.Fatal(err) } b := sample() b.Bids[0].Price = D("8.3") if err := b.Validate(); err == nil { t.Error("crossed or unsorted book accepted") } b = sample() b.Asks[1], b.Asks[2] = b.Asks[2], b.Asks[1] if err := b.Validate(); err == nil { t.Error("unsorted asks accepted") } } func TestMidSpread(t *testing.T) { b := sample() mid, _ := b.Mid() if mid.String() != "8.1875" { t.Errorf("mid %s", mid) } bps, _ := b.SpreadBps() if got := bps.Round(4).String(); got != "201.5267" { t.Errorf("spread bps %s", got) } } func TestSimulateTaker(t *testing.T) { cases := []struct { name string side Side qty, limit string filled, unfilled, worst string levels int }{ {"inside top level", Buy, "10", "0", "10", "0", "8.27", 1}, {"walks two levels", Buy, "60", "0", "60", "0", "8.291", 2}, {"exhausts book", Buy, "100", "0", "83.106", "16.894", "8.318", 3}, {"limit stops walk", Buy, "100", "8.291", "66.072", "33.928", "8.291", 2}, {"sell side", Sell, "60", "0", "60", "0", "8.084", 2}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { r := SimulateTaker(sample(), c.side, D(c.qty), D(c.limit), D("0.0005")) if r.Filled.String() != c.filled || r.Unfilled.String() != c.unfilled || r.WorstPrice.String() != c.worst || r.LevelsUsed != c.levels { t.Fatalf("got filled=%s unfilled=%s worst=%s levels=%d", r.Filled, r.Unfilled, r.WorstPrice, r.LevelsUsed) } if r.SlippageBps.IsNegative() { t.Errorf("negative slippage %s", r.SlippageBps) } }) } r := SimulateTaker(sample(), Buy, D("60"), dec.Zero(), D("0.0005")) // 52.766*8.27 + 7.234*8.291 = 436.37482 + 59.977094 = 496.351914 if r.Notional.String() != "496.351914" || r.Fee.String() != "0.248175957" { t.Errorf("notional %s fee %s", r.Notional, r.Fee) } } func TestCheckOrder(t *testing.T) { m := Market{PriceTick: D("0.001"), QtyTick: D("0.001"), MinNotional: D("1")} if errs := m.CheckOrder(D("8.105"), D("1")); len(errs) != 0 { t.Errorf("valid order rejected: %v", errs) } if errs := m.CheckOrder(D("8.1055"), D("0.0005")); len(errs) != 3 { t.Errorf("want 3 violations (price tick, qty tick, notional), got %v", errs) } } func TestDiff(t *testing.T) { a := sample() b := sample() b.Bids = []Level{lv("8.11", "1"), lv("8.105", "50"), lv("8.057", "17.81")} // add, change, remove ch := Diff(a, b) if len(ch) != 3 { t.Fatalf("want 3 changes, got %d: %+v", len(ch), ch) } if !ch[0].Price.Equal(D("8.11")) || !ch[0].Old.IsZero() { t.Errorf("first change should be the new 8.11 level: %+v", ch[0]) } if len(Diff(a, sample())) != 0 { t.Error("identical books should diff empty") } } func deepBook(n int) *Book { b := &Book{} for i := 0; i < n; i++ { b.Bids = append(b.Bids, lv(fmt.Sprintf("%d.5", 10000-i), "1.25")) b.Asks = append(b.Asks, lv(fmt.Sprintf("%d.5", 10001+i), "1.25")) } return b } func BenchmarkSimulateTaker500Levels(b *testing.B) { bk := deepBook(500) q := D("400") b.ReportAllocs() for i := 0; i < b.N; i++ { SimulateTaker(bk, Buy, q, dec.Zero(), D("0.0005")) } } func BenchmarkDiff500Levels(b *testing.B) { x, y := deepBook(500), deepBook(500) y.Bids[10].Qty = D("2") b.ReportAllocs() for i := 0; i < b.N; i++ { Diff(x, y) } }