diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index e320c471faf..8ddc32dd2b7 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -57,6 +57,7 @@ const ( bitfinexMarginClose = "funding/close" bitfinexLendbook = "lendbook/" bitfinexLends = "lends/" + bitfinexLeaderboard = "rankings" // Version 2 API endpoints bitfinexAPIVersion2 = "/v2/" @@ -506,11 +507,73 @@ func (b *Bitfinex) GetLiquidationFeed() error { return common.ErrNotYetImplemented } -// GetLeaderBoard returns leaderboard standings for unrealized -// profit (period delta), unrealized profit (inception), volume, and realized -// profit. -func (b *Bitfinex) GetLeaderBoard() error { - return common.ErrNotYetImplemented +// GetLeaderboard returns leaderboard standings for unrealized profit (period +// delta), unrealized profit (inception), volume, and realized profit. +// Allowed key values: "plu_diff" for unrealized profit (period delta), "plu" +// for unrealized profit (inception); "vol" for volume; "plr" for realized +// profit +// Allowed time frames are 3h, 1w and 1M +// Allowed symbols are trading pairs (e.g. tBTCUSD, tETHUSD and tGLOBAL:USD) +func (b *Bitfinex) GetLeaderboard(key, timeframe, symbol string, sort, limit int, start, end string) ([]LeaderboardEntry, error) { + validLeaderboardKey := func(input string) bool { + switch input { + case LeaderboardUnrealisedProfitPeriodDelta, + LeaderboardUnrealisedProfitInception, + LeaderboardVolume, + LeaderbookRealisedProfit: + return true + default: + return false + } + } + + if !validLeaderboardKey(key) { + return nil, errors.New("invalid leaderboard key") + } + + path := fmt.Sprintf("%s/%s:%s:%s/hist", b.API.Endpoints.URL+bitfinexAPIVersion2+bitfinexLeaderboard, + key, + timeframe, + symbol) + vals := url.Values{} + if sort != 0 { + vals.Set("sort", strconv.Itoa(sort)) + } + if limit != 0 { + vals.Set("limit", strconv.Itoa(limit)) + } + if start != "" { + vals.Set("start", start) + } + if end != "" { + vals.Set("end", end) + } + path = common.EncodeURLValues(path, vals) + var resp []interface{} + if err := b.SendHTTPRequest(path, &resp, leaderBoardReqRate); err != nil { + return nil, err + } + + parseTwitterHandle := func(i interface{}) string { + r, ok := i.(string) + if !ok { + return "" + } + return r + } + + var result []LeaderboardEntry + for x := range resp { + r := resp[x].([]interface{}) + result = append(result, LeaderboardEntry{ + Timestamp: time.Unix(0, int64(r[0].(float64))*int64(time.Millisecond)), + Username: r[2].(string), + Ranking: int(r[3].(float64)), + Value: r[6].(float64), + TwitterHandle: parseTwitterHandle(r[9]), + }) + } + return result, nil } // GetMarketAveragePrice calculates the average execution price for Trading or diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index c7f34250f9f..d5bb9e781fe 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -159,6 +159,41 @@ func TestGetCandles(t *testing.T) { } } +func TestGetLeaderboard(t *testing.T) { + t.Parallel() + // Test invalid key + _, err := b.GetLeaderboard("", "", "", 0, 0, "", "") + if err == nil { + t.Error("an error should have been thrown for an invalid key") + } + // Test default + _, err = b.GetLeaderboard(LeaderboardUnrealisedProfitInception, + "1M", + "tGLOBAL:USD", + 0, + 0, + "", + "") + if err != nil { + t.Fatal(err) + } + // Test params + var result []LeaderboardEntry + result, err = b.GetLeaderboard(LeaderboardUnrealisedProfitInception, + "1M", + "tGLOBAL:USD", + -1, + 1000, + "1582695181661", + "1583299981661") + if err != nil { + t.Fatal(err) + } + if len(result) == 0 { + t.Error("should have retrieved leaderboard data") + } +} + func TestGetAccountFees(t *testing.T) { if !areTestAPIKeysSet() { t.SkipNow() diff --git a/exchanges/bitfinex/bitfinex_types.go b/exchanges/bitfinex/bitfinex_types.go index 3d377c0c83b..2b4fe10fba9 100644 --- a/exchanges/bitfinex/bitfinex_types.go +++ b/exchanges/bitfinex/bitfinex_types.go @@ -1,5 +1,7 @@ package bitfinex +import "time" + // AcceptedOrderType defines the accepted market types, exchange strings denote // non-contract order types. var AcceptedOrderType = []string{"market", "limit", "stop", "trailing-stop", @@ -394,6 +396,23 @@ type Candle struct { Volume float64 } +// Leaderboard keys +const ( + LeaderboardUnrealisedProfitPeriodDelta = "plu_diff" + LeaderboardUnrealisedProfitInception = "plu" + LeaderboardVolume = "vol" + LeaderbookRealisedProfit = "plr" +) + +// LeaderboardEntry holds leaderboard data +type LeaderboardEntry struct { + Timestamp time.Time + Username string + Ranking int + Value float64 + TwitterHandle string +} + // WebsocketTicker holds ticker information type WebsocketTicker struct { Bid float64