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