From 7baa07738a394ed25d441d6c6adbc938f918e954 Mon Sep 17 00:00:00 2001 From: wqqqqqq Date: Sat, 20 Dec 2025 01:12:50 +0800 Subject: [PATCH] feat: implement coinank openapi rank interface (#1250) - implement VisualScreener Rank - implement More Instrument Agg Rank - rank interface can help user to choose a coin --- .../coinank_enum/instrument_agg_sort_by.go | 85 ++++++ provider/coinank/coinank_enum/sort_type.go | 8 + provider/coinank/coinank_http.go | 12 +- provider/coinank/instrument_agg_rank.go | 255 ++++++++++++++++++ provider/coinank/instrument_agg_rank_test.go | 101 +++++++ 5 files changed, 460 insertions(+), 1 deletion(-) create mode 100644 provider/coinank/coinank_enum/instrument_agg_sort_by.go create mode 100644 provider/coinank/coinank_enum/sort_type.go create mode 100644 provider/coinank/instrument_agg_rank.go create mode 100644 provider/coinank/instrument_agg_rank_test.go diff --git a/provider/coinank/coinank_enum/instrument_agg_sort_by.go b/provider/coinank/coinank_enum/instrument_agg_sort_by.go new file mode 100644 index 00000000..fb6d7d07 --- /dev/null +++ b/provider/coinank/coinank_enum/instrument_agg_sort_by.go @@ -0,0 +1,85 @@ +package coinank_enum + +type InstrumentAggSortBy string + +const ( + OpenInterest InstrumentAggSortBy = "openInterest" + Price InstrumentAggSortBy = "price" + PriceChangeM5 InstrumentAggSortBy = "priceChangeM5" + PriceChangeM15 InstrumentAggSortBy = "priceChangeM15" + PriceChangeM30 InstrumentAggSortBy = "priceChangeM30" + PriceChangeH4 InstrumentAggSortBy = "priceChangeH4" + PriceChangeH24 InstrumentAggSortBy = "priceChangeH24" + PriceChangeH1 InstrumentAggSortBy = "priceChangeH1" + PriceChangeH6 InstrumentAggSortBy = "priceChangeH6" + PriceChangeH12 InstrumentAggSortBy = "priceChangeH12" + OpenInterestChM15 InstrumentAggSortBy = "openInterestChM15" + OpenInterestChM5 InstrumentAggSortBy = "openInterestChM5" + OpenInterestChM30 InstrumentAggSortBy = "openInterestChM30" + OpenInterestCh1 InstrumentAggSortBy = "openInterestCh1" + OpenInterestCh4 InstrumentAggSortBy = "openInterestCh4" + OpenInterestCh24 InstrumentAggSortBy = "openInterestCh24" + OpenInterestCh2D InstrumentAggSortBy = "openInterestCh2D" + OpenInterestCh3D InstrumentAggSortBy = "openInterestCh3D" + OpenInterestCh7D InstrumentAggSortBy = "openInterestCh7D" + LiquidationH1 InstrumentAggSortBy = "liquidationH1" + LiquidationH1Long InstrumentAggSortBy = "liquidationH1Long" + LiquidationH1Short InstrumentAggSortBy = "liquidationH1Short" + LiquidationH4Long InstrumentAggSortBy = "liquidationH4Long" + LiquidationH4Short InstrumentAggSortBy = "liquidationH4Short" + LiquidationH24Short InstrumentAggSortBy = "liquidationH24Short" + LiquidationH24Long InstrumentAggSortBy = "liquidationH24Long" + LiquidationH4 InstrumentAggSortBy = "liquidationH4" + FundingRate InstrumentAggSortBy = "fundingRate" + LiquidationH12 InstrumentAggSortBy = "liquidationH12" + LiquidationH12Long InstrumentAggSortBy = "liquidationH12Long" + LiquidationH12Short InstrumentAggSortBy = "liquidationH12Short" + LiquidationH24 InstrumentAggSortBy = "liquidationH24" + Turnover24h InstrumentAggSortBy = "turnover24h" + TurnoverChg24h InstrumentAggSortBy = "turnoverChg24h" + LongShortAccount InstrumentAggSortBy = "longShortAccount" + LsPersonChg5m InstrumentAggSortBy = "lsPersonChg5m" + LsPersonChg15m InstrumentAggSortBy = "lsPersonChg15m" + LsPersonChg30m InstrumentAggSortBy = "lsPersonChg30m" + LsPersonChg1h InstrumentAggSortBy = "lsPersonChg1h" + LsPersonChg4h InstrumentAggSortBy = "lsPersonChg4h" + LongShortPerson InstrumentAggSortBy = "longShortPerson" + LongShortPosition InstrumentAggSortBy = "longShortPosition" + LongShortRatio InstrumentAggSortBy = "longShortRatio" + MarketCap InstrumentAggSortBy = "marketCap" + MarketCapChange24H InstrumentAggSortBy = "marketCapChange24H" + MarketCapChangeValue24H InstrumentAggSortBy = "marketCapChangeValue24H" + TotalSupply InstrumentAggSortBy = "totalSupply" + MaxSupply InstrumentAggSortBy = "maxSupply" + CirculatingSupply InstrumentAggSortBy = "circulatingSupply" + Buy5m InstrumentAggSortBy = "buy5m" + Sell5m InstrumentAggSortBy = "sell5m" + Buy15m InstrumentAggSortBy = "buy15m" + Sell15m InstrumentAggSortBy = "sell15m" + Buy30m InstrumentAggSortBy = "buy30m" + Sell30m InstrumentAggSortBy = "sell30m" + Buy1h InstrumentAggSortBy = "buy1h" + Sell1h InstrumentAggSortBy = "sell1h" + Buy2h InstrumentAggSortBy = "buy2h" + Sell2h InstrumentAggSortBy = "sell2h" + Buy4h InstrumentAggSortBy = "buy4h" + Sell4h InstrumentAggSortBy = "sell4h" + Buy6h InstrumentAggSortBy = "buy6h" + Sell6h InstrumentAggSortBy = "sell6h" + Buy8h InstrumentAggSortBy = "buy8h" + Sell8h InstrumentAggSortBy = "sell8h" + Buy12h InstrumentAggSortBy = "buy12h" + Sell12h InstrumentAggSortBy = "sell12h" + Buy24h InstrumentAggSortBy = "buy24h" + Sell24h InstrumentAggSortBy = "sell24h" + TurnoverChg15m InstrumentAggSortBy = "turnoverChg15m" + TurnoverChg30m InstrumentAggSortBy = "turnoverChg30m" + TurnoverChg1h InstrumentAggSortBy = "turnoverChg1h" + TurnoverChg4h InstrumentAggSortBy = "turnoverChg4h" + VolatilityM5 InstrumentAggSortBy = "volatilityM5" + VolatilityM15 InstrumentAggSortBy = "volatilityM15" + VolatilityH1 InstrumentAggSortBy = "volatilityH1" + OiVsMar InstrumentAggSortBy = "oiVsMar" + OiVsVol InstrumentAggSortBy = "oiVsVol" + VolVsMar InstrumentAggSortBy = "volVsMar" +) diff --git a/provider/coinank/coinank_enum/sort_type.go b/provider/coinank/coinank_enum/sort_type.go new file mode 100644 index 00000000..7b741cad --- /dev/null +++ b/provider/coinank/coinank_enum/sort_type.go @@ -0,0 +1,8 @@ +package coinank_enum + +type SortType string + +const ( + Asc SortType = "asc" + Desc SortType = "desc" +) diff --git a/provider/coinank/coinank_http.go b/provider/coinank/coinank_http.go index ab5ae6d3..d06b0349 100644 --- a/provider/coinank/coinank_http.go +++ b/provider/coinank/coinank_http.go @@ -18,13 +18,23 @@ type CoinankClient struct { Apikey string } -// CoinankResponse coinank openapi common respones +// CoinankResponse coinank openapi common response type CoinankResponse[T any] struct { Success bool `json:"success"` Code string `json:"code"` Data T `json:"data"` } +// PageData coinank openapi pageData in response +type PageData[T any] struct { + List []T `json:"list"` + Pagination struct { + Current int `json:"current"` + Total int `json:"total"` + PageSize int `json:"pageSize"` + } `json:"pagination"` +} + var HttpError error = errors.New("http client error") // NewCoinankClient new coinank http client for coinank openapi diff --git a/provider/coinank/instrument_agg_rank.go b/provider/coinank/instrument_agg_rank.go new file mode 100644 index 00000000..1a9a87e9 --- /dev/null +++ b/provider/coinank/instrument_agg_rank.go @@ -0,0 +1,255 @@ +package coinank + +import ( + "context" + "encoding/json" + "nofx/provider/coinank/coinank_enum" + "strconv" +) + +// VisualScreener Visual Screener +func (c *CoinankClient) VisualScreener(ctx context.Context, interval coinank_enum.Interval) ([]VisualScreenerResponse, error) { + paramsMap := make(map[string]string, 1) + paramsMap["interval"] = string(interval) + resp, err := c.Get(ctx, "/api/instruments/visualScreener", paramsMap) + if err != nil { + return nil, err + } + var result CoinankResponse[[]VisualScreenerResponse] + err = json.Unmarshal([]byte(resp), &result) + if err != nil { + return nil, err + } + if !result.Success { + return nil, HttpError + } + return result.Data, nil +} + +// OiRank Open Interest Ranking +func (c *CoinankClient) OiRank(ctx context.Context, sortBy coinank_enum.InstrumentAggSortBy, + sortType coinank_enum.SortType, page int, size int) ([]OiRankResponse, error) { + resp, err := c.Get(ctx, "/api/instruments/oiRank", c.rankParam(sortBy, sortType, page, size)) + if err != nil { + return nil, err + } + var result CoinankResponse[CoinankResponse[PageData[OiRankResponse]]] + err = json.Unmarshal([]byte(resp), &result) + if err != nil { + return nil, err + } + if !result.Success { + return nil, HttpError + } + if !result.Data.Success { + return nil, HttpError + } + return result.Data.Data.List, nil +} + +// LongShortRank longShortRatio Ranking +func (c *CoinankClient) LongShortRank(ctx context.Context, sortBy coinank_enum.InstrumentAggSortBy, + sortType coinank_enum.SortType, page int, size int) ([]LongShortRankResponse, error) { + resp, err := c.Get(ctx, "/api/instruments/longShortRank", c.rankParam(sortBy, sortType, page, size)) + if err != nil { + return nil, err + } + var result CoinankResponse[CoinankResponse[PageData[LongShortRankResponse]]] + err = json.Unmarshal([]byte(resp), &result) + if err != nil { + return nil, err + } + if !result.Success { + return nil, HttpError + } + if !result.Data.Success { + return nil, HttpError + } + return result.Data.Data.List, nil +} + +// LiquidationRank Liquidation Ranking +func (c *CoinankClient) LiquidationRank(ctx context.Context, sortBy coinank_enum.InstrumentAggSortBy, + sortType coinank_enum.SortType, page int, size int) ([]LiquidationRankResponse, error) { + resp, err := c.Get(ctx, "/api/instruments/liquidationRank", c.rankParam(sortBy, sortType, page, size)) + if err != nil { + return nil, err + } + var result CoinankResponse[CoinankResponse[PageData[LiquidationRankResponse]]] + err = json.Unmarshal([]byte(resp), &result) + if err != nil { + return nil, err + } + if !result.Success { + return nil, HttpError + } + if !result.Data.Success { + return nil, HttpError + } + return result.Data.Data.List, nil +} + +// PriceRank PriceChg Ranking +func (c *CoinankClient) PriceRank(ctx context.Context, sortBy coinank_enum.InstrumentAggSortBy, + sortType coinank_enum.SortType, page int, size int) ([]PriceRankResponse, error) { + resp, err := c.Get(ctx, "/api/instruments/priceRank", c.rankParam(sortBy, sortType, page, size)) + if err != nil { + return nil, err + } + var result CoinankResponse[CoinankResponse[PageData[PriceRankResponse]]] + err = json.Unmarshal([]byte(resp), &result) + if err != nil { + return nil, err + } + if !result.Success { + return nil, HttpError + } + if !result.Data.Success { + return nil, HttpError + } + return result.Data.Data.List, nil +} + +// VolumeRank VolumeChg Ranking +func (c *CoinankClient) VolumeRank(ctx context.Context, sortBy coinank_enum.InstrumentAggSortBy, + sortType coinank_enum.SortType, page int, size int) ([]VolumeRankResponse, error) { + resp, err := c.Get(ctx, "/api/instruments/volumeRank", c.rankParam(sortBy, sortType, page, size)) + if err != nil { + return nil, err + } + var result CoinankResponse[CoinankResponse[PageData[VolumeRankResponse]]] + err = json.Unmarshal([]byte(resp), &result) + if err != nil { + return nil, err + } + if !result.Success { + return nil, HttpError + } + if !result.Data.Success { + return nil, HttpError + } + return result.Data.Data.List, nil +} + +func (c *CoinankClient) rankParam(sortBy coinank_enum.InstrumentAggSortBy, + sortType coinank_enum.SortType, page int, size int) map[string]string { + paramsMap := make(map[string]string, 4) + if page < 1 { + page = 1 + } + if size < 1 { + size = 10 + } + if sortBy == "" { + sortBy = coinank_enum.OpenInterest + } + if sortType == "" { + sortType = coinank_enum.Desc + } + paramsMap["page"] = strconv.Itoa(page) + paramsMap["size"] = strconv.Itoa(size) + paramsMap["sortBy"] = string(sortBy) + paramsMap["sortType"] = string(sortType) + return paramsMap +} + +type VisualScreenerResponse struct { + BaseCoin string `json:"baseCoin"` + PriceChg float64 `json:"priceChg"` + OiChg float64 `json:"oiChg"` + VoChg float64 `json:"voChg"` +} + +type LongShortRankResponse struct { + BaseCoin string `json:"baseCoin"` + CoinImage string `json:"coinImage"` + Price float64 `json:"price"` + LongShortPerson float64 `json:"longShortPerson"` + LsPersonChg5M float64 `json:"lsPersonChg5m"` + LsPersonChg15M float64 `json:"lsPersonChg15m"` + LsPersonChg30M float64 `json:"lsPersonChg30m"` + LsPersonChg1H float64 `json:"lsPersonChg1h"` + LsPersonChg4H float64 `json:"lsPersonChg4h"` + CirculatingSupply int `json:"circulatingSupply"` + Symbol string `json:"symbol"` + ExchangeName string `json:"exchangeName"` + SupportContract bool `json:"supportContract"` +} + +type OiRankResponse struct { + BaseCoin string `json:"baseCoin"` + CoinImage string `json:"coinImage"` + Price float64 `json:"price"` + OpenInterest float64 `json:"openInterest"` + OpenInterestChM5 float64 `json:"openInterestChM5"` + OpenInterestChM15 float64 `json:"openInterestChM15"` + OpenInterestChM30 float64 `json:"openInterestChM30"` + OpenInterestCh1 float64 `json:"openInterestCh1"` + OpenInterestCh4 float64 `json:"openInterestCh4"` + OpenInterestCh24 float64 `json:"openInterestCh24"` + OpenInterestCh2D float64 `json:"openInterestCh2D"` + OpenInterestCh3D float64 `json:"openInterestCh3D"` + OpenInterestCh7D float64 `json:"openInterestCh7D"` + CirculatingSupply int `json:"circulatingSupply"` + Symbol string `json:"symbol"` + ExchangeName string `json:"exchangeName"` + SupportContract bool `json:"supportContract"` + Follow bool `json:"follow"` +} + +type LiquidationRankResponse struct { + BaseCoin string `json:"baseCoin"` + CoinImage string `json:"coinImage"` + Price float64 `json:"price"` + PriceChangeH24 float64 `json:"priceChangeH24"` + LiquidationH1 float64 `json:"liquidationH1"` + LiquidationH1Long float64 `json:"liquidationH1Long"` + LiquidationH1Short float64 `json:"liquidationH1Short"` + LiquidationH4 float64 `json:"liquidationH4"` + LiquidationH4Long float64 `json:"liquidationH4Long"` + LiquidationH4Short float64 `json:"liquidationH4Short"` + LiquidationH12 float64 `json:"liquidationH12"` + LiquidationH12Long float64 `json:"liquidationH12Long"` + LiquidationH12Short float64 `json:"liquidationH12Short"` + LiquidationH24 float64 `json:"liquidationH24"` + LiquidationH24Long float64 `json:"liquidationH24Long"` + LiquidationH24Short float64 `json:"liquidationH24Short"` + CirculatingSupply int `json:"circulatingSupply"` + SupportContract bool `json:"supportContract"` +} + +type PriceRankResponse struct { + BaseCoin string `json:"baseCoin"` + CoinImage string `json:"coinImage"` + Price float64 `json:"price"` + PriceChangeH24 float64 `json:"priceChangeH24"` + PriceChangeM5 float64 `json:"priceChangeM5"` + PriceChangeM15 float64 `json:"priceChangeM15"` + PriceChangeM30 float64 `json:"priceChangeM30"` + PriceChangeH1 float64 `json:"priceChangeH1"` + PriceChangeH2 float64 `json:"priceChangeH2"` + PriceChangeH4 float64 `json:"priceChangeH4"` + PriceChangeH6 float64 `json:"priceChangeH6"` + PriceChangeH8 float64 `json:"priceChangeH8"` + PriceChangeH12 float64 `json:"priceChangeH12"` + CirculatingSupply int `json:"circulatingSupply"` + Symbol string `json:"symbol"` + ExchangeName string `json:"exchangeName"` + SupportContract bool `json:"supportContract"` +} + +type VolumeRankResponse struct { + BaseCoin string `json:"baseCoin"` + CoinImage string `json:"coinImage"` + Price float64 `json:"price"` + Turnover24H float64 `json:"turnover24h"` + TurnoverChg24H float64 `json:"turnoverChg24h"` + TurnoverChg4H float64 `json:"turnoverChg4h"` + TurnoverChg1H float64 `json:"turnoverChg1h"` + TurnoverChg30M float64 `json:"turnoverChg30m"` + TurnoverChg15M float64 `json:"turnoverChg15m"` + CirculatingSupply int `json:"circulatingSupply"` + Symbol string `json:"symbol"` + ExchangeName string `json:"exchangeName"` + SupportContract bool `json:"supportContract"` +} diff --git a/provider/coinank/instrument_agg_rank_test.go b/provider/coinank/instrument_agg_rank_test.go new file mode 100644 index 00000000..8a008342 --- /dev/null +++ b/provider/coinank/instrument_agg_rank_test.go @@ -0,0 +1,101 @@ +package coinank + +import ( + "context" + "encoding/json" + "nofx/provider/coinank/coinank_enum" + "testing" +) + +func TestVisualScreener(t *testing.T) { + client := NewCoinankClient(coinank_enum.MainUrl, TestApikey) + resp, err := client.VisualScreener(context.TODO(), coinank_enum.Minute15) + if err != nil { + t.Error(err) + } + res, err := json.Marshal(resp) + if err != nil { + t.Error(err) + } + t.Logf("%s", res) +} + +func TestOiRank(t *testing.T) { + client := NewCoinankClient(coinank_enum.MainUrl, TestApikey) + resp, err := client.OiRank(context.TODO(), coinank_enum.OpenInterest, coinank_enum.Desc, 1, 10) + if err != nil { + t.Error(err) + } + if resp[0].BaseCoin != "BTC" { + t.Error("oi first not BTC") + } + res, err := json.Marshal(resp) + if err != nil { + t.Error(err) + } + t.Logf("%s", res) +} + +func TestLongShortRank(t *testing.T) { + client := NewCoinankClient(coinank_enum.MainUrl, TestApikey) + resp, err := client.LongShortRank(context.TODO(), coinank_enum.LongShortRatio, coinank_enum.Desc, 1, 10) + if err != nil { + t.Error(err) + } + if resp[0].BaseCoin == "" { + t.Error("baseCoin is empty") + } + res, err := json.Marshal(resp) + if err != nil { + t.Error(err) + } + t.Logf("%s", res) +} + +func TestLiquidationRank(t *testing.T) { + client := NewCoinankClient(coinank_enum.MainUrl, TestApikey) + resp, err := client.LiquidationRank(context.TODO(), coinank_enum.LiquidationH1, coinank_enum.Desc, 1, 10) + if err != nil { + t.Error(err) + } + if resp[0].BaseCoin == "" { + t.Error("baseCoin is empty") + } + res, err := json.Marshal(resp) + if err != nil { + t.Error(err) + } + t.Logf("%s", res) +} + +func TestPriceRank(t *testing.T) { + client := NewCoinankClient(coinank_enum.MainUrl, TestApikey) + resp, err := client.PriceRank(context.TODO(), coinank_enum.Price, coinank_enum.Desc, 1, 10) + if err != nil { + t.Error(err) + } + if resp[0].BaseCoin == "" { + t.Error("baseCoin is empty") + } + res, err := json.Marshal(resp) + if err != nil { + t.Error(err) + } + t.Logf("%s", res) +} + +func TestVolumeRank(t *testing.T) { + client := NewCoinankClient(coinank_enum.MainUrl, TestApikey) + resp, err := client.VolumeRank(context.TODO(), coinank_enum.Turnover24h, coinank_enum.Desc, 1, 10) + if err != nil { + t.Error(err) + } + if resp[0].BaseCoin == "" { + t.Error("baseCoin is empty") + } + res, err := json.Marshal(resp) + if err != nil { + t.Error(err) + } + t.Logf("%s", res) +}