Files
maker/provider_kraken_test.go
zeekay 8aa4b5f780 maker: live Kraken CEX oracle + composite price feed
Add a Kraken public-market-data price source (provider_kraken.go), the same
ProviderSource shape as provider_oma.go / provider_v3.go, exposing the live CEX
mid the seeder tracks via UseFeed. NewKrakenFeed wraps it; CompositeMid (feed.go)
composes the CEX feed (listed majors) with the native V3 AMM feed (LUX/*) behind
the single feed the seeder consumes. Binary opt-in via MAKER_KRAKEN_PAIRS.

Read-only, unauthenticated, market-data only — the maker never trades on Kraken.
Tests hermetic (canned frames) + KRAKEN_LIVE gate; live-proven (real BTC/USD mid).

go.sum: refresh luxfi/[email protected] checksum (origin re-tag).
2026-06-28 17:13:31 -07:00

124 lines
4.2 KiB
Go

package maker
import (
"context"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
)
// krakenTickerJSON is a canned /Ticker frame for XBTUSD (result keyed by Kraken's
// normalized name XXBTZUSD, exactly as the live API returns it).
const krakenTickerJSON = `{"error":[],"result":{"XXBTZUSD":{
"a":["60010.5","1","1.000"],
"b":["59990.5","2","2.000"],
"c":["60000.0","0.01"],
"v":["100","200"],"p":["59950","59900"]}}}`
// krakenDepthJSON is a canned /Depth frame: asks ascending, bids descending, each
// row [price, volume, timestamp] with the timestamp as a bare number (mixed types).
const krakenDepthJSON = `{"error":[],"result":{"XXBTZUSD":{
"asks":[["60010.5","1.5",1700000000],["60020.0","2.0",1700000001]],
"bids":[["59990.5","1.0",1700000000],["59980.0","3.0",1700000001]]}}}`
func krakenStub(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch {
case strings.HasPrefix(r.URL.Path, "/Ticker"):
_, _ = w.Write([]byte(krakenTickerJSON))
case strings.HasPrefix(r.URL.Path, "/Depth"):
_, _ = w.Write([]byte(krakenDepthJSON))
default:
http.Error(w, "not found", http.StatusNotFound)
}
}))
}
// TestKrakenBBO parses a canned ticker into bid/ask/last.
func TestKrakenBBO(t *testing.T) {
srv := krakenStub(t)
defer srv.Close()
p := krakenProviderSourceWithBase("kraken", srv.URL, map[string]string{"BTC/USD": "XBTUSD"})
bid, ask, last, err := p.GetBBO(context.Background(), "BTC/USD")
if err != nil {
t.Fatalf("GetBBO: %v", err)
}
if !approx(bid, 59990.5) || !approx(ask, 60010.5) || !approx(last, 60000.0) {
t.Fatalf("BBO = bid %v ask %v last %v, want 59990.5/60010.5/60000.0", bid, ask, last)
}
}
// TestKrakenBook parses a canned depth frame and asserts the mid the aggregator
// will compute — best bid 59990.5, best ask 60010.5 → mid 60000.5.
func TestKrakenBook(t *testing.T) {
srv := krakenStub(t)
defer srv.Close()
p := krakenProviderSourceWithBase("kraken", srv.URL, map[string]string{"BTC/USD": "XBTUSD"})
bids, asks, err := p.GetBook(context.Background(), "BTC/USD")
if err != nil {
t.Fatalf("GetBook: %v", err)
}
if len(bids) != 2 || len(asks) != 2 {
t.Fatalf("levels = %d bids / %d asks, want 2/2", len(bids), len(asks))
}
for _, l := range append(append([]PriceLevel{}, bids...), asks...) {
if l.Source != "kraken" || !l.Phantom {
t.Fatalf("level not tagged phantom-kraken: %+v", l)
}
}
agg := NewAggregator([]ProviderSource{p}, quietLogger())
book, err := agg.GetAggregatedBook(context.Background(), "BTC/USD")
if err != nil {
t.Fatalf("aggregate: %v", err)
}
if !approx(book.MidPrice, 60000.5) {
t.Fatalf("mid = %v, want 60000.5", book.MidPrice)
}
}
// TestKrakenUnmappedSymbol confirms an unmapped symbol errors (so the aggregator
// skips it) rather than fabricating a price.
func TestKrakenUnmappedSymbol(t *testing.T) {
srv := krakenStub(t)
defer srv.Close()
p := krakenProviderSourceWithBase("kraken", srv.URL, map[string]string{"BTC/USD": "XBTUSD"})
if _, _, _, err := p.GetBBO(context.Background(), "DOGE/USD"); err == nil {
t.Fatal("expected error for unmapped symbol")
}
}
// TestNewKrakenFeedNilWhenEmpty confirms the feed is opt-in: an empty map yields a
// nil MidProvider (the seeder keeps its prior reference behavior).
func TestNewKrakenFeedNilWhenEmpty(t *testing.T) {
if NewKrakenFeed(nil, "kraken", nil) != nil {
t.Fatal("expected nil feed for empty pair map")
}
if NewKrakenFeed(map[string]string{"ETH/USD": "ETHUSD"}, "", nil) == nil {
t.Fatal("expected non-nil feed for a configured pair")
}
}
// TestKrakenLive hits the real public Kraken API. It is gated on KRAKEN_LIVE=1 so
// CI stays hermetic; run it to prove the live feed returns a sane BTC mid.
func TestKrakenLive(t *testing.T) {
if os.Getenv("KRAKEN_LIVE") != "1" {
t.Skip("set KRAKEN_LIVE=1 to hit the real Kraken API")
}
feed := NewKrakenFeed(map[string]string{"BTC/USD": "XBTUSD"}, "kraken", quietLogger())
mid, err := feed.GetMidPrice(context.Background(), "BTC/USD")
if err != nil {
t.Fatalf("live mid: %v", err)
}
if mid < 1000 || mid > 10_000_000 {
t.Fatalf("live BTC mid %.2f outside sane range", mid)
}
t.Logf("live Kraken BTC/USD mid = %.2f", mid)
}