From 4e1cf674d78a60807d7065ba26cac2cdbf1e2e5b Mon Sep 17 00:00:00 2001 From: murlux Date: Thu, 6 May 2021 22:29:03 +0100 Subject: [PATCH] FTX REST bootstrapping, websocket is missing orderbook channel and example ftx.ws client is working. --- .../ccip---ccex-improvement-proposal.md | 19 ++ .../ISSUE_TEMPLATES/issues---bug-reports.md | 31 +++ builder/builder.go | 21 +- exchanges.go => builder/exchanges.go | 2 +- builder/ftx.go | 48 ---- bybit/errors.go | 2 +- common/market.go | 2 +- common/order.go | 4 +- config/config.go | 9 +- config/config_test.go | 4 +- examples/README.md | 57 ----- examples/ftx/main.go | 75 ++++-- exchange.go | 22 -- exchange/README.md | 3 + account.go => exchange/account.go | 3 +- exchange/conversion.go | 13 + exchange/exchange.go | 49 ++++ exchange/fills.go | 13 + exchange/funding.go | 13 + exchange/futures.go | 45 ++++ exchange/leveraged_tokens.go | 19 ++ exchange/margin.go | 27 ++ exchange/markets.go | 39 +++ exchange/options.go | 6 + exchange/orders.go | 42 ++++ exchange/spot.go | 6 + exchange/wallet.go | 57 +++++ websocket.go => exchange/websocket.go | 23 +- ftx/account.go | 2 +- ftx/client.go | 1 - ftx/conversion.go | 33 +++ ftx/errors.go | 3 +- ftx/fills.go | 27 ++ ftx/ftx.go | 97 ++++++++ ftx/funding.go | 50 ++++ ftx/futures.go | 51 ++++ ftx/leveraged_tokens.go | 19 ++ ftx/margin.go | 62 +++++ ftx/markets.go | 43 ++++ ftx/options.go | 19 ++ ftx/orders.go | 58 ++++- ftx/rest/authentication.go | 6 +- ftx/rest/client.go | 78 ++++-- ftx/rest/constants.go | 5 +- ftx/rest/errors.go | 5 + ftx/rest/models/account.go | 8 +- ftx/rest/models/common.go | 4 +- ftx/rest/models/responses.go | 6 - ftx/spot.go | 19 ++ ftx/wallet.go | 53 +++- ftx/websocket/client.go | 232 ++++++++++-------- ftx/websocket/constants.go | 16 +- ftx/websocket/models/requests.go | 8 +- ftx/websocket/models/responses.go | 52 ++-- internal/util/util.go | 2 +- internal/websocket/client.go | 106 ++++---- orders.go | 8 - wallet.go | 8 - 58 files changed, 1308 insertions(+), 427 deletions(-) create mode 100644 .github/ISSUE_TEMPLATES/ccip---ccex-improvement-proposal.md create mode 100644 .github/ISSUE_TEMPLATES/issues---bug-reports.md rename exchanges.go => builder/exchanges.go (94%) delete mode 100644 builder/ftx.go delete mode 100644 exchange.go create mode 100644 exchange/README.md rename account.go => exchange/account.go (98%) create mode 100644 exchange/conversion.go create mode 100644 exchange/exchange.go create mode 100644 exchange/fills.go create mode 100644 exchange/funding.go create mode 100644 exchange/futures.go create mode 100644 exchange/leveraged_tokens.go create mode 100644 exchange/margin.go create mode 100644 exchange/markets.go create mode 100644 exchange/options.go create mode 100644 exchange/orders.go create mode 100644 exchange/spot.go create mode 100644 exchange/wallet.go rename websocket.go => exchange/websocket.go (73%) delete mode 100644 ftx/client.go create mode 100644 ftx/conversion.go create mode 100644 ftx/fills.go create mode 100644 ftx/ftx.go create mode 100644 ftx/funding.go create mode 100644 ftx/futures.go create mode 100644 ftx/leveraged_tokens.go create mode 100644 ftx/margin.go create mode 100644 ftx/markets.go create mode 100644 ftx/options.go create mode 100644 ftx/rest/errors.go create mode 100644 ftx/spot.go delete mode 100644 orders.go delete mode 100644 wallet.go diff --git a/.github/ISSUE_TEMPLATES/ccip---ccex-improvement-proposal.md b/.github/ISSUE_TEMPLATES/ccip---ccex-improvement-proposal.md new file mode 100644 index 0000000..9b6b56c --- /dev/null +++ b/.github/ISSUE_TEMPLATES/ccip---ccex-improvement-proposal.md @@ -0,0 +1,19 @@ +--- +name: CCIP - CCEX Improvement Proposal +about: Suggest an idea for this project +title: "[CCIP]" +labels: '' +assignees: murlokito +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATES/issues---bug-reports.md b/.github/ISSUE_TEMPLATES/issues---bug-reports.md new file mode 100644 index 0000000..b48e317 --- /dev/null +++ b/.github/ISSUE_TEMPLATES/issues---bug-reports.md @@ -0,0 +1,31 @@ +--- +name: Issues / Bug Reports +about: Create a report to help us improve +title: "[Issue]" +labels: '' +assignees: murlokito +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/builder/builder.go b/builder/builder.go index 7e7beec..b2a1a91 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -2,28 +2,29 @@ package builder import ( "fmt" - . "github.com/murlokito/ccex" - + "github.com/murlokito/ccex" "github.com/murlokito/ccex/config" + "github.com/murlokito/ccex/exchange" + "github.com/murlokito/ccex/ftx" ) // NewExchange returns a configured client with the passed config. -func NewExchange(exchange Exchange, config *config.Configuration) (*ExchangeClient, error) { +func NewExchange(exchange exchange.Exchange, config *config.Configuration) (*exchange.ExchangeClient, error) { switch exchange { case Binance: - return nil, ErrExchangeNotImplemented + return nil, ccex.ErrExchangeNotImplemented case BinanceUS: - return nil, ErrExchangeNotImplemented + return nil, ccex.ErrExchangeNotImplemented case BitMEX: - return nil, ErrExchangeNotImplemented + return nil, ccex.ErrExchangeNotImplemented case Bybit: - return nil, ErrExchangeNotImplemented + return nil, ccex.ErrExchangeNotImplemented case Deribit: - return nil, ErrExchangeNotImplemented + return nil, ccex.ErrExchangeNotImplemented case FTX: - return NewFTXClient(config) + return ftx.NewFTXClient(config) case FTXUS: - return nil, ErrExchangeNotImplemented + return nil, ccex.ErrExchangeNotImplemented default: return nil, fmt.Errorf("new clients error [%v]", Exchanges[exchange]) } diff --git a/exchanges.go b/builder/exchanges.go similarity index 94% rename from exchanges.go rename to builder/exchanges.go index 46cacda..ccf0d9d 100644 --- a/exchanges.go +++ b/builder/exchanges.go @@ -1,4 +1,4 @@ -package ccex +package builder const ( Binance = iota diff --git a/builder/ftx.go b/builder/ftx.go deleted file mode 100644 index 5cc1c94..0000000 --- a/builder/ftx.go +++ /dev/null @@ -1,48 +0,0 @@ -package builder - -import ( - "github.com/murlokito/ccex" - "github.com/murlokito/ccex/config" - "github.com/murlokito/ccex/ftx" - "github.com/murlokito/ccex/ftx/rest" - "github.com/murlokito/ccex/ftx/websocket" -) - -// NewFTXClient returns a new configured client for FTX, to be used with the agnostic builder. -func NewFTXClient(config *config.Configuration) (*ccex.ExchangeClient, error){ - - // Initialize the base http client that takes care of authentication and rate limiting - client, err := rest.NewClient(config) - if err != nil { - return nil, err - } - - // Initialize the clients for the specific API segments - accountClient, err := ftx.NewAccountClient(client) - if err != nil { - return nil, err - } - - walletClient, err := ftx.NewWalletClient(client) - if err != nil { - return nil, err - } - - ordersClient, err := ftx.NewOrdersClient(client) - if err != nil { - return nil, err - } - - // Initialize the websocket client - wsClient, err := websocket.NewClient(config) - if err != nil { - return nil, err - } - - return &ccex.ExchangeClient{ - Account: accountClient, - Wallet: walletClient, - Orders: ordersClient, - Websocket: wsClient, - }, nil -} diff --git a/bybit/errors.go b/bybit/errors.go index 70f3e3d..f5a939a 100644 --- a/bybit/errors.go +++ b/bybit/errors.go @@ -4,5 +4,5 @@ import "fmt" var ( ErrMethodNotImplemented = fmt.Errorf("bybit: method not implemented") - ErrNotAuthenticated = fmt.Errorf("bybit: not authenticated") + ErrNotAuthenticated = fmt.Errorf("bybit: not authenticated") ) diff --git a/common/market.go b/common/market.go index 408937c..3170973 100644 --- a/common/market.go +++ b/common/market.go @@ -7,4 +7,4 @@ const ( Spot = iota Futures Options -) \ No newline at end of file +) diff --git a/common/order.go b/common/order.go index ae6f827..5cdd942 100644 --- a/common/order.go +++ b/common/order.go @@ -7,6 +7,4 @@ const ( LimitOrder = iota MarketOrder TriggerOrder - ) - - +) diff --git a/config/config.go b/config/config.go index b3b01d2..a1eb86f 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,9 @@ package config -import "github.com/murlokito/ccex/auth" +import ( + "fmt" + "github.com/murlokito/ccex/auth" +) // Configuration holds everything necessary to type Configuration struct { @@ -8,6 +11,10 @@ type Configuration struct { SubAccount string } +func (c Configuration) String() string { + return fmt.Sprintf("k: %v s: %v sub-account: %v", c.Auth.GetKey(), c.Auth.GetSecret(), c.SubAccount) +} + // GetAuth retrieves the configuration's authentication func (c *Configuration) GetAuth() *auth.Authentication { return c.Auth diff --git a/config/config_test.go b/config/config_test.go index 76c2bae..a25acd9 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -25,7 +25,7 @@ func TestConfig(t *testing.T) { func TestSetConfig(t *testing.T) { config := Configuration{ - Auth: nil, + Auth: nil, SubAccount: "none", } authInfo := &auth.Authentication{ @@ -40,4 +40,4 @@ func TestSetConfig(t *testing.T) { } }) -} \ No newline at end of file +} diff --git a/examples/README.md b/examples/README.md index e825ee6..78ed6f6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,60 +5,3 @@ This package holds examples on how to use `ccex` in the following ways: - exchange-agnostic using the builder package - exchange-specific using any of the supported exchanges -## Preview - -### Exchange agnostic - -```go -package main - -import ( - "fmt" - "net/http" - - "github.com/murlokito/ccex" - builder "github.com/murlokito/ccex/builder" -) - -func main() { - - var ( - exchanges []ccex.Exchange - ) - - clients := ccex.{ - ccex.FTX, - ccex.Binance, - } - - - params := &ccex.Parameters{ - Debug: false, - HttpClient: &http.Client{}, - ProxyURL: "", - AccessKey: "access-key", - SecretKey: "access-secret", - } - - exchange, err := builder.NewExchangeFromParameters(ccex.FTX, params) - if err != nil { - fmt.Println(err) - } - - exchange. -} - -``` - -### FTX - -```go -package main - -import "github.com/murlokito/ccex" - -func main(){ - -} - -``` \ No newline at end of file diff --git a/examples/ftx/main.go b/examples/ftx/main.go index c787c69..5edf3fe 100644 --- a/examples/ftx/main.go +++ b/examples/ftx/main.go @@ -1,41 +1,78 @@ -package ftx +package main import ( "fmt" - "github.com/murlokito/ccex/auth" - "github.com/murlokito/ccex/builder" - "github.com/murlokito/ccex/config" + "github.com/murlokito/ccex/ftx" "github.com/murlokito/ccex/ftx/websocket" "github.com/murlokito/ccex/ftx/websocket/models" + "time" ) +type Data struct { + Trades map[string][]models.TradeData + Ticker map[string]models.TickerData +} + func main() { - cfg := &config.Configuration{ - Auth: &auth.Authentication{ - Key: "some-key", - Secret: "some-secret", - }, - SubAccount: "some-sub-account", + markets := []string{ + "BTC-PERP", "ETH-PERP", } - ftxClient, err := builder.NewFTXClient(cfg) - if err != nil { - fmt.Printf("err: %v", err) + data := Data{ + Trades: map[string][]models.TradeData{}, + Ticker: map[string]models.TickerData{}, } - handler := func(message models.TickerMessage) { - fmt.Printf("bid: %v ask: %v last: %v", message.Data.Bid, message.Data.Ask, message.Data.Last) + tickerHandler := func(message models.TickerMessage) { + //fmt.Printf("bid: %v ask: %v last: %v\n", message.Data.Bid, message.Data.Ask, message.Data.Last) + data.Ticker[message.Market] = message.Data } - err = ftxClient.Websocket.TickerHandler(handler) - if err != nil { - fmt.Printf("err: %v", err) + tradeHandler := func(message models.TradeMessage) { + //complete := fmt.Sprintf("num trades: %v", len(message.Data)) + for _, trade := range message.Data { + data.Trades[message.Market] = append(data.Trades[message.Market], trade) + //str := fmt.Sprintf("price: %v size: %v side: %v liq: %v\n", trade.Price, trade.Price, trade.Side, trade.Liquidation) + //complete += str + tradeVol := trade.Size * trade.Price + if tradeVol > 100000 { + fmt.Printf("{%v} {%v} Volume: $%.2f Price: $%v Liquidation: %v\n", message.Market, trade.Side, tradeVol, trade.Price, trade.Liquidation) + } + } + //fmt.Println(complete) } - err = ftxClient.Websocket.Subscribe(websocket.Ticker, "BTC-PERP") + ftxClient, err := ftx.NewFTXClient(nil, nil, tickerHandler, tradeHandler, nil) if err != nil { fmt.Printf("err: %v", err) } + ftxClient.Websocket.Connect() + + for _, market := range markets { + err = ftxClient.Websocket.Subscribe(websocket.Ticker, market) + if err != nil { + fmt.Printf("err: %v", err) + } + + err = ftxClient.Websocket.Subscribe(websocket.Trades, market) + if err != nil { + fmt.Printf("err: %v", err) + } + } + + for { + if ftxClient.Websocket.Connected() { + fmt.Println(fmt.Sprintf("client is connected - active subs %v", ftxClient.Websocket.Subscriptions())) + } + for k, v := range data.Trades { + fmt.Println(fmt.Sprintf("number of trades for %v - %v", k, len(v))) + } + for k, v := range data.Ticker { + fmt.Println(fmt.Sprintf("latest ticker for %v - %v", k, v.Last)) + } + time.Sleep(15 * time.Second) + } + } diff --git a/exchange.go b/exchange.go deleted file mode 100644 index 0c10642..0000000 --- a/exchange.go +++ /dev/null @@ -1,22 +0,0 @@ -package ccex - -type ( - // Exchange represents the exchange name. - Exchange int - - /* - ExchangeClient exposes a unified API to interact with the exchange. - In certain cases some methods will not be implemented and thus will - return an error accordingly. - */ - ExchangeClient struct { - Account Account - - Wallet Wallet - - Orders Orders - - Websocket Websocket - } - -) diff --git a/exchange/README.md b/exchange/README.md new file mode 100644 index 0000000..405b32d --- /dev/null +++ b/exchange/README.md @@ -0,0 +1,3 @@ +# exchange + +This package specifies library-wide interfaces and constants. \ No newline at end of file diff --git a/account.go b/exchange/account.go similarity index 98% rename from account.go rename to exchange/account.go index aec0a99..2b3d10c 100644 --- a/account.go +++ b/exchange/account.go @@ -1,4 +1,4 @@ -package ccex +package exchange import "github.com/murlokito/ccex/common" @@ -34,6 +34,5 @@ type ( // PostFuturesAccountLeverageChange is used to change the futures account's maximum leverage to the amount specified by `leverage` on the market specified by `symbol`. PostFuturesAccountLeverageChange(symbol string, leverage int) (common.Response, error) - } ) diff --git a/exchange/conversion.go b/exchange/conversion.go new file mode 100644 index 0000000..4385efa --- /dev/null +++ b/exchange/conversion.go @@ -0,0 +1,13 @@ +package exchange + +import "github.com/murlokito/ccex/common" + +type ( + Conversion interface { + PostConversionQuoteRequest(fromCoin, toCoin string, size float32) (common.Response, error) + + GetConversionQuoteStatus(quoteId int, market string) (common.Response, error) + + PostConversionQuoteAcceptance(quoteId int) (common.Response, error) + } +) diff --git a/exchange/exchange.go b/exchange/exchange.go new file mode 100644 index 0000000..21740b3 --- /dev/null +++ b/exchange/exchange.go @@ -0,0 +1,49 @@ +package exchange + +type ( + // Exchange represents the exchange name. + Exchange int + + /* + ExchangeClient exposes a unified API to interact with the exchange. + In certain cases some methods will not be implemented and thus will + return an error accordingly. + */ + ExchangeClient struct { + // Account interface which interacts with account generic endpoints + Account Account + + // Wallet interface which interacts with wallet-specific endpoints + Wallet Wallet + + // Conversion interface which interacts with conversion-specific endpoints + Conversion Conversion + + // Orders interface which interacts with order-specific endpoints + Orders Orders + + // Markets interface which interacts with market generic endpoints + Markets Markets + + // Fills interface which interacts with order fill specific endpoints + Fills Fills + + // Funding interface which interacts with funding specific endpoints + Funding Funding + + // Spot interface which interacts with spot-specific endpoints + Spot Spot + + // Futures interface which interacts with future-specific endpoints + Futures Futures + + // Margin interface which interacts with spot margin-specific endpoints + Margin Margin + + // Options interface which interacts with options-specific endpoints + Options Options + + // Websocket interface which interacts with the websocket + Websocket Websocket + } +) diff --git a/exchange/fills.go b/exchange/fills.go new file mode 100644 index 0000000..f169494 --- /dev/null +++ b/exchange/fills.go @@ -0,0 +1,13 @@ +package exchange + +import ( + "time" + + "github.com/murlokito/ccex/common" +) + +type ( + Fills interface { + GetFills(market, order string, orderId, limit int, start, end time.Time) (common.Response, error) + } +) diff --git a/exchange/funding.go b/exchange/funding.go new file mode 100644 index 0000000..40d62ad --- /dev/null +++ b/exchange/funding.go @@ -0,0 +1,13 @@ +package exchange + +import ( + "time" + + "github.com/murlokito/ccex/common" +) + +type ( + Funding interface { + GetFundingPayments(future string, start, end time.Time) (common.Response, error) + } +) diff --git a/exchange/futures.go b/exchange/futures.go new file mode 100644 index 0000000..c0acabb --- /dev/null +++ b/exchange/futures.go @@ -0,0 +1,45 @@ +package exchange + +import ( + "github.com/murlokito/ccex/common" + "time" +) + +type ( + Futures interface { + /* + GetFutures fetches all the available futures markets. + In certain cases this method and the GetMarkets method have the same behavior, + it happens when it is strictly a futures exchange or there are no distinct endpoints for markets and futures. + It is so the interface is implemented. + */ + GetFutures() (common.Response, error) + + /* + GetFuture is used to fetch information related to the futures market specified by `future`. + In certain cases this method and the GetMarket method have the same behavior, + it happens when it is strictly a futures exchange or there are no distinct endpoints for markets and futures. + It is so the interface is implemented. + */ + GetFuture(future string) (common.Response, error) + + // GetFutureStats fetches the stats associated with the future specified by `future`. + GetFutureStats(future string) (common.Response, error) + + // GetFundingRate fetches the funding rates for the future specified by `future`. + GetFundingRate(future string, start, end time.Time) (common.Response, error) + + // GetIndexWeights fetches the weights of the index specified by `index`. + GetIndexWeights(index string) (common.Response, error) + + // GetExpiredFutures fetches futures that have expired. + GetExpiredFutures() (common.Response, error) + + /* + GetHistoricalIndex fetches OHLC data for the index specified by `index` + with a maximum specified by `limit` for the passed `symbol` with the specified `resolution`. + Optionally provide `start` and `end` to request a specific period. + */ + GetHistoricalIndex(index string, resolution, limit int, start, end time.Time) (common.Response, error) + } +) diff --git a/exchange/leveraged_tokens.go b/exchange/leveraged_tokens.go new file mode 100644 index 0000000..af71931 --- /dev/null +++ b/exchange/leveraged_tokens.go @@ -0,0 +1,19 @@ +package exchange + +import "github.com/murlokito/ccex/common" + +type ( + LeveragedTokens interface { + GetLeveragedTokens() (common.Response, error) + + GetLeveragedTokenInfo(token string) (common.Response, error) + + GetLeveragedTokenBalances() (common.Response, error) + + PostLeveragedTokenCreationRequest(token string, size float32) (common.Response, error) + + GetLeveragedTokenRedemptionRequests() (common.Response, error) + + PostLeveragedTokenRedemptionRequest(token string, size float32) (common.Response, error) + } +) diff --git a/exchange/margin.go b/exchange/margin.go new file mode 100644 index 0000000..190dff7 --- /dev/null +++ b/exchange/margin.go @@ -0,0 +1,27 @@ +package exchange + +import "github.com/murlokito/ccex/common" + +type ( + Margin interface { + GetLendingHistory() (common.Response, error) + + GetBorrowRates() (common.Response, error) + + GetLendingRates() (common.Response, error) + + GetDailyBorrowedAmounts() (common.Response, error) + + GetSpotMarginMarketInfo() (common.Response, error) + + GetMyBorrowHistory() (common.Response, error) + + GetMyLendingHistory() (common.Response, error) + + GetLendingOffers() (common.Response, error) + + GetLendingInfo() (common.Response, error) + + PostLendingOffer(coin string, size, rate float32) (common.Response, error) + } +) diff --git a/exchange/markets.go b/exchange/markets.go new file mode 100644 index 0000000..ce38b35 --- /dev/null +++ b/exchange/markets.go @@ -0,0 +1,39 @@ +package exchange + +import ( + "github.com/murlokito/ccex/common" + "time" +) + +type ( + Markets interface { + /* + GetMarkets fetches all the available markets. + In certain cases this method and the GetFutures method have the same behavior, + it happens when it is strictly a futures exchange or there are no distinct endpoints for markets and futures. + It is so the interface is implemented. + */ + GetMarkets() (common.Response, error) + + /* + GetMarket is used to fetch information related to the market specified by `symbol`. + In certain cases this method and the GetFuture method have the same behavior, + it happens when it is strictly a futures exchange or there are no distinct endpoints for markets and futures. + It is so the interface is implemented. + */ + GetMarket(symbol string) (common.Response, error) + + // GetOrderBook fetches the order book for the passed `symbol`. + GetOrderBook(symbol string) (common.Response, error) + + // GetTrades fetches the trades for the passed `symbol`. + GetTrades(symbol string) (common.Response, error) + + /* + GetCandles fetches OHLC data for the market specified by `symbol` + with a maximum specified by `limit` for the passed `symbol` with the specified `resolution`. + Optionally provide `start` and `end` to request a specific period. + */ + GetCandles(symbol string, resolution, limit int, start, end time.Time) (common.Response, error) + } +) diff --git a/exchange/options.go b/exchange/options.go new file mode 100644 index 0000000..baec374 --- /dev/null +++ b/exchange/options.go @@ -0,0 +1,6 @@ +package exchange + +type ( + Options interface { + } +) diff --git a/exchange/orders.go b/exchange/orders.go new file mode 100644 index 0000000..d0b4803 --- /dev/null +++ b/exchange/orders.go @@ -0,0 +1,42 @@ +package exchange + +import ( + "github.com/murlokito/ccex/common" + "time" +) + +type ( + // Orders specifies functionality for the orders API + Orders interface { + // GetOpenOrders fetches the open orders. Optionally pass `market` to only fetch orders from a single market. + GetOpenOrders(market string) (common.Response, error) + + /* + GetOrderHistory fetches the orders history. + Optionally pass `market` to only fetch orders from a single market and `start` and `end` for orders within a period. + */ + GetOrderHistory(market string, limit int, start, end time.Time) (common.Response, error) + + GetOpenTriggerOrders(market, triggerOrderType string) (common.Response, error) + + GetTriggerOrderHistory(market, side, triggerOrderType, orderType string, limit int, start, end time.Time) (common.Response, error) + + GetTriggerOrderTriggers() (common.Response, error) + + PostOrder(market, side, orderType, clientId string, price, size float32, reduceOnly, postOnly, ioc bool) (common.Response, error) + + PostTriggerOrder(market, side, triggerOrderType string, size, triggerPrice, orderPrice, trailValue float32, reduceOnly, retryUntilFilled bool) (common.Response, error) + + PostModifyOrder(orderId int, price, size float32, clientId string, byClientId bool) (common.Response, error) + + PostModifyTriggerOrder(orderId int, size, triggerPrice, orderPrice, trailValue float32) (common.Response, error) + + GetOrderStatus(orderId, clientId int, byClientId bool) (common.Response, error) + + DeleteOrder(orderId, clientId int, byClientId bool) (common.Response, error) + + DeleteTriggerOrder(orderId int) (common.Response, error) + + DeleteAllOrders(market string, conditionalOrdersOnly, limitOrdersOnly bool) (common.Response, error) + } +) diff --git a/exchange/spot.go b/exchange/spot.go new file mode 100644 index 0000000..621915a --- /dev/null +++ b/exchange/spot.go @@ -0,0 +1,6 @@ +package exchange + +type ( + Spot interface { + } +) diff --git a/exchange/wallet.go b/exchange/wallet.go new file mode 100644 index 0000000..bfc651b --- /dev/null +++ b/exchange/wallet.go @@ -0,0 +1,57 @@ +package exchange + +import ( + "github.com/murlokito/ccex/common" + "time" +) + +type ( + // Wallet specifies functionality for the wallet API + Wallet interface { + + // GetWalletCoins is used to fetch coins held in the account wallet. + GetWalletCoins() (common.Response, error) + + // GetWalletBalances is used to fetch balances of account holdings. + GetWalletBalances() (common.Response, error) + + // GetAllWalletBalances is used to fetch all balances of account holdings. + GetAllWalletBalances() (common.Response, error) + + /* + GetDepositAddress is used to fetch a deposit address for the currency specified by `coin` using `method`. + For FTX, `method` can be one of: + For ERC20 tokens: method=erc20 + For TRC20 tokens: method=trx + For SPL tokens: method=sol + For Omni tokens: method=omni + For BEP2 tokens: method=bep2 + */ + GetDepositAddress(coin, method string) (common.Response, error) + + // GetWalletDepositHistory is used to fetch the wallet deposit history. + GetWalletDepositHistory(limit int, start, end time.Time) (common.Response, error) + + // GetWalletWithdrawalHistory is used to fetch the wallet withdrawal history. + GetWalletWithdrawalHistory(limit int, start, end time.Time) (common.Response, error) + + // GetWalletAirdropHistory is used to fetch the wallet airdrop history. + GetWalletAirdropHistory(limit int, start, end time.Time) (common.Response, error) + + // GetSavedAddresses is used to fetch saved addresses for currency specified by `coin`. + GetSavedAddresses(coin string) (common.Response, error) + + // PostCreateSavedAddress is used to fetch saved addresses for currency specified by `coin`. + PostCreateSavedAddress(coin, address, addressName, tag string, isPrimeTrust bool) (common.Response, error) + + // DeleteSavedAddress is used to delete a saved address specified by `addressId`. + DeleteSavedAddress(addressId int) (common.Response, error) + + /* + PostWalletWithdrawal is used to request a withdrawal of the coin specified by `coin` with amount specified by `size` + to the address specified by `address`, if withdrawal password and/or 2FA is active then `password` and `code` + are necessary to proceed with the withdrawal. The parameter `tag` is optional. + */ + PostWalletWithdrawal(coin, address, tag, password, code string, size int) (common.Response, error) + } +) diff --git a/websocket.go b/exchange/websocket.go similarity index 73% rename from websocket.go rename to exchange/websocket.go index f43647f..660745e 100644 --- a/websocket.go +++ b/exchange/websocket.go @@ -1,10 +1,8 @@ -package ccex +package exchange import "github.com/murlokito/ccex/ftx/websocket/models" type ( - - // OnMarketsHandler is called whenever the websocket client receives a market message. OnMarketsHandler func(data models.MarketMessage) @@ -17,23 +15,26 @@ type ( // OnTradeHandler is called whenever the websocket client receives a trade message. OnTradeHandler func(data models.TradeMessage) - // OnMessageHandler is a type defined to represent a handler called for a certain channel and market combination + // OnMessageHandler is a type defined to represent a handler called for a certain channel and market combination. OnMessageHandler func(message interface{}) - // MessageDispatcher represents a subscription with a personal handlerr + // MessageDispatcher represents a subscription with a personal handler. MessageDispatcher struct { Channel string - Market string + Market string Handler OnMessageHandler } // Websocket specifies functionality to interact with the websocket API. Websocket interface { + Connect() + Connected() bool + Reconnect() error + Subscriptions() map[string][]string Subscribe(channel string, market string) error - SubscribeWithHandler(channel string, market string, handler OnMessageHandler) error - MarketHandler(handler OnMarketsHandler) error - OrderBookHandler(handler OnOrderBookHandler) error - TradeHandler(handler OnTradeHandler) error - TickerHandler(handler OnTickerHandler) error + OnMarketHandler(handler OnMarketsHandler) + OnOrderBookHandler(handler OnOrderBookHandler) + OnTradesHandler(handler OnTradeHandler) + OnTickerHandler(handler OnTickerHandler) } ) diff --git a/ftx/account.go b/ftx/account.go index 6554c36..ffca285 100644 --- a/ftx/account.go +++ b/ftx/account.go @@ -46,7 +46,7 @@ func (a AccountClient) PostFuturesAccountLeverageChange(symbol string, leverage } // NewAccountClient returns a new configured account client -func NewAccountClient(client *rest.Client) (*AccountClient, error){ +func NewAccountClient(client *rest.Client) (*AccountClient, error) { return &AccountClient{ client: client, }, nil diff --git a/ftx/client.go b/ftx/client.go deleted file mode 100644 index 0b15d23..0000000 --- a/ftx/client.go +++ /dev/null @@ -1 +0,0 @@ -package ftx diff --git a/ftx/conversion.go b/ftx/conversion.go new file mode 100644 index 0000000..be0e583 --- /dev/null +++ b/ftx/conversion.go @@ -0,0 +1,33 @@ +package ftx + +import ( + "github.com/murlokito/ccex/common" + "github.com/murlokito/ccex/ftx/rest" +) + +type ( + + // ConversionClient represents the client for the FTX Conversion API. + ConversionClient struct { + client *rest.Client + } +) + +func (c ConversionClient) PostConversionQuoteRequest(fromCoin, toCoin string, size float32) (common.Response, error) { + panic("implement me") +} + +func (c ConversionClient) GetConversionQuoteStatus(quoteId int, market string) (common.Response, error) { + panic("implement me") +} + +func (c ConversionClient) PostConversionQuoteAcceptance(quoteId int) (common.Response, error) { + panic("implement me") +} + +// NewConversionClient returns a new configured account client +func NewConversionClient(client *rest.Client) (*ConversionClient, error) { + return &ConversionClient{ + client: client, + }, nil +} diff --git a/ftx/errors.go b/ftx/errors.go index 12a616e..ef5bc5a 100644 --- a/ftx/errors.go +++ b/ftx/errors.go @@ -1,7 +1,6 @@ package ftx const ( - ErrRateLimited = "ftx: rate limited, should wait %d ms" ErrMethodNotImplemented = "ftx: method not implemented" - ErrNotAuthenticated = "ftx: not authenticated" + ErrNotAuthenticated = "ftx: not authenticated" ) diff --git a/ftx/fills.go b/ftx/fills.go new file mode 100644 index 0000000..d6cc8b3 --- /dev/null +++ b/ftx/fills.go @@ -0,0 +1,27 @@ +package ftx + +import ( + "github.com/murlokito/ccex/common" + "github.com/murlokito/ccex/ftx/rest" + "time" +) + +type ( + + // FillsClient represents the client for the FTX Fills API. + FillsClient struct { + client *rest.Client + } +) + +func (f FillsClient) GetFills(market, order string, orderId, limit int, start, end time.Time) (common.Response, error) { + panic("implement me") +} + +// NewFillsClient returns a new configured account client +func NewFillsClient(client *rest.Client) (*FillsClient, error) { + return &FillsClient{ + client: client, + }, nil +} + diff --git a/ftx/ftx.go b/ftx/ftx.go new file mode 100644 index 0000000..f43aed2 --- /dev/null +++ b/ftx/ftx.go @@ -0,0 +1,97 @@ +package ftx + +import ( + "github.com/murlokito/ccex/config" + "github.com/murlokito/ccex/exchange" + "github.com/murlokito/ccex/ftx/rest" + "github.com/murlokito/ccex/ftx/websocket" +) + +// NewFTXClient returns a new configured client for FTX, to be used with the agnostic builder. +func NewFTXClient(config *config.Configuration, marketsHandler exchange.OnMarketsHandler, + tickerHandler exchange.OnTickerHandler, tradesHandler exchange.OnTradeHandler, + orderBookHandler exchange.OnOrderBookHandler) (*exchange.ExchangeClient, error) { + + // Initialize the base http client that takes care of authentication and rate limiting + client, err := rest.NewClient(config) + if err != nil { + return nil, err + } + + // Initialize the clients for the specific API segments + accountClient, err := NewAccountClient(client) + if err != nil { + return nil, err + } + + walletClient, err := NewWalletClient(client) + if err != nil { + return nil, err + } + + ordersClient, err := NewOrdersClient(client) + if err != nil { + return nil, err + } + + conversionClient, err := NewConversionClient(client) + if err != nil { + return nil, err + } + + marginClient, err := NewMarginClient(client) + if err != nil { + return nil, err + } + + marketsClient, err := NewMarketsClient(client) + if err != nil { + return nil, err + } + + fillsClient, err := NewFillsClient(client) + if err != nil { + return nil, err + } + + fundingClient, err := NewFundingClient(client) + if err != nil { + return nil, err + } + + spotClient, err := NewSpotClient(client) + if err != nil { + return nil, err + } + + futuresClient, err := NewFuturesClient(client) + if err != nil { + return nil, err + } + + optionsClient, err := NewOptionsClient(client) + if err != nil { + return nil, err + } + + // Initialize the websocket client + wsClient, err := websocket.NewClient(config, marketsHandler, tickerHandler, tradesHandler, orderBookHandler) + if err != nil { + return nil, err + } + + return &exchange.ExchangeClient{ + Account: accountClient, + Wallet: walletClient, + Conversion: conversionClient, + Orders: ordersClient, + Markets: marketsClient, + Fills: fillsClient, + Funding: fundingClient, + Spot: spotClient, + Futures: futuresClient, + Margin: marginClient, + Options: optionsClient, + Websocket: wsClient, + }, nil +} diff --git a/ftx/funding.go b/ftx/funding.go new file mode 100644 index 0000000..21bddf5 --- /dev/null +++ b/ftx/funding.go @@ -0,0 +1,50 @@ +package ftx + +import ( + "encoding/json" + "fmt" + "github.com/murlokito/ccex/common" + "github.com/murlokito/ccex/ftx/rest" + "github.com/murlokito/ccex/ftx/rest/models" + "time" +) + +type ( + + // FundingClient represents the client for the FTX Funding API. + FundingClient struct { + client *rest.Client + } +) + +func (f FundingClient) GetFundingPayments(future string, start, end time.Time) (common.Response, error) { + var url string + + if future != "" { + if (start != time.Time{}) && (end != time.Time{}) { + url = fmt.Sprintf(rest.FundingPaymentsFutureStartEndEndpoint, future, start.Unix(), end.Unix()) + }else { + url = fmt.Sprintf(rest.FundingPaymentsFutureEndpoint, future) + } + } + + res, err := f.client.Get(url) + if err != nil { + return nil, err + } + var model models.ResponseForFundingRates + err = json.Unmarshal(res, &model) + if err != nil { + return nil, err + } + + return &model, nil +} + +// NewFundingClient returns a new configured account client +func NewFundingClient(client *rest.Client) (*FundingClient, error) { + return &FundingClient{ + client: client, + }, nil +} + diff --git a/ftx/futures.go b/ftx/futures.go new file mode 100644 index 0000000..b624c89 --- /dev/null +++ b/ftx/futures.go @@ -0,0 +1,51 @@ +package ftx + +import ( + "github.com/murlokito/ccex/common" + "github.com/murlokito/ccex/ftx/rest" + "time" +) + +type ( + + // FuturesClient represents the client for the FTX Futures API. + FuturesClient struct { + client *rest.Client + } +) + +func (f FuturesClient) GetFutures() (common.Response, error) { + panic("implement me") +} + +func (f FuturesClient) GetFuture(future string) (common.Response, error) { + panic("implement me") +} + +func (f FuturesClient) GetFutureStats(future string) (common.Response, error) { + panic("implement me") +} + +func (f FuturesClient) GetFundingRate(future string, start, end time.Time) (common.Response, error) { + panic("implement me") +} + +func (f FuturesClient) GetIndexWeights(index string) (common.Response, error) { + panic("implement me") +} + +func (f FuturesClient) GetExpiredFutures() (common.Response, error) { + panic("implement me") +} + +func (f FuturesClient) GetHistoricalIndex(index string, resolution, limit int, start, end time.Time) (common.Response, error) { + panic("implement me") +} + +// NewFuturesClient returns a new configured account client +func NewFuturesClient(client *rest.Client) (*FuturesClient, error) { + return &FuturesClient{ + client: client, + }, nil +} + diff --git a/ftx/leveraged_tokens.go b/ftx/leveraged_tokens.go new file mode 100644 index 0000000..a866359 --- /dev/null +++ b/ftx/leveraged_tokens.go @@ -0,0 +1,19 @@ +package ftx + +import "github.com/murlokito/ccex/ftx/rest" + +type ( + + // LeveragedTokensClient represents the client for the FTX Leveraged Tokens API. + LeveragedTokensClient struct { + client *rest.Client + } +) + +// NewLeveragedTokensClient returns a new configured account client +func NewLeveragedTokensClient(client *rest.Client) (*LeveragedTokensClient, error) { + return &LeveragedTokensClient{ + client: client, + }, nil +} + diff --git a/ftx/margin.go b/ftx/margin.go new file mode 100644 index 0000000..5059b87 --- /dev/null +++ b/ftx/margin.go @@ -0,0 +1,62 @@ +package ftx + +import ( + "github.com/murlokito/ccex/common" + "github.com/murlokito/ccex/ftx/rest" +) + +type ( + + // MarginClient represents the client for the FTX Margin API. + MarginClient struct { + client *rest.Client + } +) + +func (m MarginClient) GetLendingHistory() (common.Response, error) { + panic("implement me") +} + +func (m MarginClient) GetBorrowRates() (common.Response, error) { + panic("implement me") +} + +func (m MarginClient) GetLendingRates() (common.Response, error) { + panic("implement me") +} + +func (m MarginClient) GetDailyBorrowedAmounts() (common.Response, error) { + panic("implement me") +} + +func (m MarginClient) GetSpotMarginMarketInfo() (common.Response, error) { + panic("implement me") +} + +func (m MarginClient) GetMyBorrowHistory() (common.Response, error) { + panic("implement me") +} + +func (m MarginClient) GetMyLendingHistory() (common.Response, error) { + panic("implement me") +} + +func (m MarginClient) GetLendingOffers() (common.Response, error) { + panic("implement me") +} + +func (m MarginClient) GetLendingInfo() (common.Response, error) { + panic("implement me") +} + +func (m MarginClient) PostLendingOffer(coin string, size, rate float32) (common.Response, error) { + panic("implement me") +} + +// NewMarginClient returns a new configured account client +func NewMarginClient(client *rest.Client) (*MarginClient, error) { + return &MarginClient{ + client: client, + }, nil +} + diff --git a/ftx/markets.go b/ftx/markets.go new file mode 100644 index 0000000..6906eda --- /dev/null +++ b/ftx/markets.go @@ -0,0 +1,43 @@ +package ftx + +import ( + "github.com/murlokito/ccex/common" + "github.com/murlokito/ccex/ftx/rest" + "time" +) + +type ( + + // MarketsClient represents the client for the FTX Markets API. + MarketsClient struct { + client *rest.Client + } +) + +func (m MarketsClient) GetMarkets() (common.Response, error) { + panic("implement me") +} + +func (m MarketsClient) GetMarket(symbol string) (common.Response, error) { + panic("implement me") +} + +func (m MarketsClient) GetOrderBook(symbol string) (common.Response, error) { + panic("implement me") +} + +func (m MarketsClient) GetTrades(symbol string) (common.Response, error) { + panic("implement me") +} + +func (m MarketsClient) GetCandles(symbol string, resolution, limit int, start, end time.Time) (common.Response, error) { + panic("implement me") +} + +// NewMarketsClient returns a new configured account client +func NewMarketsClient(client *rest.Client) (*MarketsClient, error) { + return &MarketsClient{ + client: client, + }, nil +} + diff --git a/ftx/options.go b/ftx/options.go new file mode 100644 index 0000000..639a89c --- /dev/null +++ b/ftx/options.go @@ -0,0 +1,19 @@ +package ftx + +import "github.com/murlokito/ccex/ftx/rest" + +type ( + + // OptionsClient represents the client for the FTX Options API. + OptionsClient struct { + client *rest.Client + } +) + +// NewOptionsClient returns a new configured account client +func NewOptionsClient(client *rest.Client) (*OptionsClient, error) { + return &OptionsClient{ + client: client, + }, nil +} + diff --git a/ftx/orders.go b/ftx/orders.go index 4e3e926..702feef 100644 --- a/ftx/orders.go +++ b/ftx/orders.go @@ -1,6 +1,10 @@ package ftx -import "github.com/murlokito/ccex/ftx/rest" +import ( + "github.com/murlokito/ccex/common" + "github.com/murlokito/ccex/ftx/rest" + "time" +) type ( @@ -10,6 +14,58 @@ type ( } ) +func (o OrdersClient) GetOpenOrders(market string) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) GetOrderHistory(market string, limit int, start, end time.Time) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) GetOpenTriggerOrders(market, triggerOrderType string) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) GetTriggerOrderHistory(market, side, triggerOrderType, orderType string, limit int, start, end time.Time) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) GetTriggerOrderTriggers() (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) PostOrder(market, side, orderType, clientId string, price, size float32, reduceOnly, postOnly, ioc bool) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) PostTriggerOrder(market, side, triggerOrderType string, size, triggerPrice, orderPrice, trailValue float32, reduceOnly, retryUntilFilled bool) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) PostModifyOrder(orderId int, price, size float32, clientId string, byClientId bool) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) PostModifyTriggerOrder(orderId int, size, triggerPrice, orderPrice, trailValue float32) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) GetOrderStatus(orderId, clientId int, byClientId bool) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) DeleteOrder(orderId, clientId int, byClientId bool) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) DeleteTriggerOrder(orderId int) (common.Response, error) { + panic("implement me") +} + +func (o OrdersClient) DeleteAllOrders(market string, conditionalOrdersOnly, limitOrdersOnly bool) (common.Response, error) { + panic("implement me") +} + func NewOrdersClient(client *rest.Client) (*OrdersClient, error) { return &OrdersClient{ client: client, diff --git a/ftx/rest/authentication.go b/ftx/rest/authentication.go index eeddf4f..2b4aa1e 100644 --- a/ftx/rest/authentication.go +++ b/ftx/rest/authentication.go @@ -8,18 +8,16 @@ import ( "net/http" "strconv" "time" - - ) // SignRequest is used to sign the request with the necessary information -func (c *Client) SignRequest(method string, endpoint string, body []byte) *http.Request { +func (c *Client) SignRequest(method, endpoint, url string, body []byte) *http.Request { ts := strconv.FormatInt(time.Now().UTC().Unix()*1000, 10) signaturePayload := ts + method + endpoint + string(body) signature := c.Sign(signaturePayload) - req, _ := http.NewRequest(method, endpoint, bytes.NewBuffer(body)) + req, _ := http.NewRequest(method, url, bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("FTX-KEY", c.config.Auth.Key) diff --git a/ftx/rest/client.go b/ftx/rest/client.go index c34d4f8..1317274 100644 --- a/ftx/rest/client.go +++ b/ftx/rest/client.go @@ -3,12 +3,12 @@ package rest import ( "encoding/json" "fmt" - "github.com/murlokito/ccex/auth" - "github.com/murlokito/ccex/ftx" - "golang.org/x/time/rate" "net/http" "time" + "github.com/murlokito/ccex/auth" + "golang.org/x/time/rate" + "github.com/murlokito/ccex/config" "github.com/murlokito/ccex/internal/rest" ) @@ -32,17 +32,19 @@ type Client struct { action must be taken before processing the request, which generally it does, due to authentication, etc. */ -func (c *Client) Get() (*http.Response, error) { +func (c *Client) Get(endpoint string) ([]byte, error) { reservation := c.limiter.Reserve() if !reservation.OK() { duration := reservation.DelayFrom(time.Now()) reservation.Cancel() - return nil, fmt.Errorf(ftx.ErrRateLimited, duration.Milliseconds()) + return nil, fmt.Errorf(ErrRateLimited, duration.Milliseconds()) } - preparedRequest := c.SignRequest("GET", c.client.BaseUrl, []byte("")) + reqUrl := c.client.BaseUrl + endpoint + + preparedRequest := c.SignRequest("GET", endpoint, reqUrl, []byte("")) resp, err := c.client.Submit(preparedRequest) if err != nil { @@ -50,7 +52,15 @@ func (c *Client) Get() (*http.Response, error) { } reservation.Cancel() - return resp, nil + + var buffer []byte + + _, err = resp.Body.Read(buffer) + if err != nil { + return nil, err + } + + return buffer, nil } /* @@ -60,14 +70,14 @@ func (c *Client) Get() (*http.Response, error) { action must be taken before processing the request, which generally it does, due to authentication, etc. */ -func (c *Client) Post(data map[string]interface{}) (*http.Response, error) { +func (c *Client) Post(endpoint string, data map[string]interface{}) ([]byte, error) { reservation := c.limiter.Reserve() if !reservation.OK() { duration := reservation.DelayFrom(time.Now()) reservation.Cancel() - return nil, fmt.Errorf(ftx.ErrRateLimited, duration.Milliseconds()) + return nil, fmt.Errorf(ErrRateLimited, duration.Milliseconds()) } payload, err := json.Marshal(data) @@ -75,7 +85,9 @@ func (c *Client) Post(data map[string]interface{}) (*http.Response, error) { return nil, err } - preparedRequest := c.SignRequest("POST", c.client.BaseUrl, payload) + reqUrl := c.client.BaseUrl + endpoint + + preparedRequest := c.SignRequest("POST", endpoint, reqUrl, payload) resp, err := c.client.Submit(preparedRequest) if err != nil { @@ -83,7 +95,15 @@ func (c *Client) Post(data map[string]interface{}) (*http.Response, error) { } reservation.Cancel() - return resp, nil + + var buffer []byte + + _, err = resp.Body.Read(buffer) + if err != nil { + return nil, err + } + + return buffer, nil } /* @@ -93,14 +113,14 @@ func (c *Client) Post(data map[string]interface{}) (*http.Response, error) { action must be taken before processing the request, which generally it does, due to authentication, etc. */ -func (c *Client) Put(data map[string]interface{}) (*http.Response, error) { +func (c *Client) Put(endpoint string, data map[string]interface{}) ([]byte, error) { reservation := c.limiter.Reserve() if !reservation.OK() { duration := reservation.DelayFrom(time.Now()) reservation.Cancel() - return nil, fmt.Errorf(ftx.ErrRateLimited, duration.Milliseconds()) + return nil, fmt.Errorf(ErrRateLimited, duration.Milliseconds()) } payload, err := json.Marshal(data) @@ -108,7 +128,9 @@ func (c *Client) Put(data map[string]interface{}) (*http.Response, error) { return nil, err } - preparedRequest := c.SignRequest("PUT", c.client.BaseUrl, payload) + reqUrl := c.client.BaseUrl + endpoint + + preparedRequest := c.SignRequest("PUT", endpoint, reqUrl, payload) resp, err := c.client.Submit(preparedRequest) if err != nil { @@ -116,7 +138,15 @@ func (c *Client) Put(data map[string]interface{}) (*http.Response, error) { } reservation.Cancel() - return resp, nil + + var buffer []byte + + _, err = resp.Body.Read(buffer) + if err != nil { + return nil, err + } + + return buffer, nil } /* @@ -126,14 +156,14 @@ func (c *Client) Put(data map[string]interface{}) (*http.Response, error) { action must be taken before processing the request, which generally it does, due to authentication, etc. */ -func (c *Client) Delete(data map[string]interface{}) (*http.Response, error) { +func (c *Client) Delete(endpoint string, data map[string]interface{}) ([]byte, error) { reservation := c.limiter.Reserve() if !reservation.OK() { duration := reservation.DelayFrom(time.Now()) reservation.Cancel() - return nil, fmt.Errorf(ftx.ErrRateLimited, duration.Milliseconds()) + return nil, fmt.Errorf(ErrRateLimited, duration.Milliseconds()) } payload, err := json.Marshal(data) @@ -141,7 +171,9 @@ func (c *Client) Delete(data map[string]interface{}) (*http.Response, error) { return nil, err } - preparedRequest := c.SignRequest("DELETE", c.client.BaseUrl, payload) + reqUrl := c.client.BaseUrl + endpoint + + preparedRequest := c.SignRequest("DELETE", endpoint, reqUrl, payload) resp, err := c.client.Submit(preparedRequest) if err != nil { @@ -149,7 +181,15 @@ func (c *Client) Delete(data map[string]interface{}) (*http.Response, error) { } reservation.Cancel() - return resp, nil + + var buffer []byte + + _, err = resp.Body.Read(buffer) + if err != nil { + return nil, err + } + + return buffer, nil } // NewClient returns a new rest client for ftx diff --git a/ftx/rest/constants.go b/ftx/rest/constants.go index 245ee20..b2d8129 100644 --- a/ftx/rest/constants.go +++ b/ftx/rest/constants.go @@ -115,7 +115,7 @@ const ( IndexCandlesEndpointFormat = "/indexes/%s/candles?resolution=%s&limit=%s&start_time=%s&end_time=%s" /* - Orders Endpoints + Orders Endpoints */ // The OrderEndpoint is used as GET to fetch all open orders and DELETE to cancel all orders @@ -205,6 +205,9 @@ const ( // The FundingPaymentsEndpoint FundingPaymentsEndpoint = "/funding_payments" + FundingPaymentsFutureEndpoint = "/funding_payments?future=%s" + FundingPaymentsFutureStartEndEndpoint = "/funding_payments?future=%s&start_time=%d&end_time=%d" + /* Leveraged Tokens Endpoints diff --git a/ftx/rest/errors.go b/ftx/rest/errors.go new file mode 100644 index 0000000..3daa7bd --- /dev/null +++ b/ftx/rest/errors.go @@ -0,0 +1,5 @@ +package rest + +const ( + ErrRateLimited = "ftx: rate limited, should wait %d ms" +) diff --git a/ftx/rest/models/account.go b/ftx/rest/models/account.go index 4b6dadc..1851277 100644 --- a/ftx/rest/models/account.go +++ b/ftx/rest/models/account.go @@ -23,10 +23,10 @@ type Position struct { // Account holds the necessary information to represent the account information type Account struct { - BackstopProvider bool `json:"backstopProvider"` - Collateral float64 `json:"collateral"` - FreeCollateral float64 `json:"freeCollateral"` - InitialMarginRequirement float64 `json:"initialMarginRequirement"` + BackstopProvider bool `json:"backstopProvider"` + Collateral float64 `json:"collateral"` + FreeCollateral float64 `json:"freeCollateral"` + InitialMarginRequirement float64 `json:"initialMarginRequirement"` Leverage int `json:"leverage"` Liquidating bool `json:"liquidating"` MaintenanceMarginRequirement float64 `json:"maintenanceMarginRequirement"` diff --git a/ftx/rest/models/common.go b/ftx/rest/models/common.go index 1ec461a..4187b5e 100644 --- a/ftx/rest/models/common.go +++ b/ftx/rest/models/common.go @@ -4,6 +4,6 @@ type BaseResponse struct { Success bool `json:"success"` } -func (b *BaseResponse) WasSuccessful() bool{ +func (b *BaseResponse) WasSuccessful() bool { return b.Success -} \ No newline at end of file +} diff --git a/ftx/rest/models/responses.go b/ftx/rest/models/responses.go index 82f3c4c..ad72d62 100644 --- a/ftx/rest/models/responses.go +++ b/ftx/rest/models/responses.go @@ -68,9 +68,6 @@ func (r *ResponseForPositions) GetResult() interface{} { Wallet requests responses */ - - - /* Market requests responses */ @@ -164,6 +161,3 @@ type ResponseForTrades struct { func (r *ResponseForTrades) GetResult() interface{} { return r.Result } - - - diff --git a/ftx/spot.go b/ftx/spot.go new file mode 100644 index 0000000..cb70645 --- /dev/null +++ b/ftx/spot.go @@ -0,0 +1,19 @@ +package ftx + +import "github.com/murlokito/ccex/ftx/rest" + +type ( + + // SpotClient represents the client for the FTX Spot API. + SpotClient struct { + client *rest.Client + } +) + +// NewSpotClient returns a new configured account client +func NewSpotClient(client *rest.Client) (*SpotClient, error) { + return &SpotClient{ + client: client, + }, nil +} + diff --git a/ftx/wallet.go b/ftx/wallet.go index ec53cf4..a1fec01 100644 --- a/ftx/wallet.go +++ b/ftx/wallet.go @@ -1,6 +1,10 @@ package ftx -import "github.com/murlokito/ccex/ftx/rest" +import ( + "github.com/murlokito/ccex/common" + "github.com/murlokito/ccex/ftx/rest" + "time" +) type ( @@ -10,9 +14,52 @@ type ( } ) +func (w WalletClient) GetWalletCoins() (common.Response, error) { + panic("implement me") +} + +func (w WalletClient) GetWalletBalances() (common.Response, error) { + panic("implement me") +} + +func (w WalletClient) GetAllWalletBalances() (common.Response, error) { + panic("implement me") +} + +func (w WalletClient) GetDepositAddress(coin, method string) (common.Response, error) { + panic("implement me") +} + +func (w WalletClient) GetWalletDepositHistory(limit int, start, end time.Time) (common.Response, error) { + panic("implement me") +} + +func (w WalletClient) GetWalletWithdrawalHistory(limit int, start, end time.Time) (common.Response, error) { + panic("implement me") +} + +func (w WalletClient) GetWalletAirdropHistory(limit int, start, end time.Time) (common.Response, error) { + panic("implement me") +} + +func (w WalletClient) GetSavedAddresses(coin string) (common.Response, error) { + panic("implement me") +} + +func (w WalletClient) PostCreateSavedAddress(coin, address, addressName, tag string, isPrimeTrust bool) (common.Response, error) { + panic("implement me") +} + +func (w WalletClient) DeleteSavedAddress(addressId int) (common.Response, error) { + panic("implement me") +} + +func (w WalletClient) PostWalletWithdrawal(coin, address, tag, password, code string, size int) (common.Response, error) { + panic("implement me") +} -func NewWalletClient(client *rest.Client) (*WalletClient, error){ +func NewWalletClient(client *rest.Client) (*WalletClient, error) { return &WalletClient{ client: client, }, nil -} \ No newline at end of file +} diff --git a/ftx/websocket/client.go b/ftx/websocket/client.go index f00360e..9ad8288 100644 --- a/ftx/websocket/client.go +++ b/ftx/websocket/client.go @@ -3,7 +3,9 @@ package websocket import ( "encoding/json" "fmt" - "github.com/murlokito/ccex" + "time" + + "github.com/murlokito/ccex/exchange" "github.com/murlokito/ccex/internal/logger" "github.com/murlokito/ccex/log" @@ -15,123 +17,155 @@ import ( // Client represents the websocket client for FTX type Client struct { // OnMarkets holds the handler for markets messages. - OnMarkets ccex.OnMarketsHandler + OnMarkets exchange.OnMarketsHandler // OnOrderBook holds the handler for order book messages. - OnOrderBook ccex.OnOrderBookHandler + OnOrderBook exchange.OnOrderBookHandler // OnTicker holds the handler for ticker messages. - OnTicker ccex.OnTickerHandler + OnTicker exchange.OnTickerHandler // OnTrade holds the handler for trade messages. - OnTrade ccex.OnTradeHandler + OnTrade exchange.OnTradeHandler // config holds the config used to establish the connection. config *config.Configuration // ws holds the underlying websocket connection. - ws *websocket.Client + ws *websocket.Client // logger holds a logger, the user can inject a logger as long as it implements the interface we specify. logger log.Logger // subscriptions holds all subscriptions across all channels and markets. - subscriptions map[string]string + subscriptions map[string][]string +} + +func (c *Client) Subscriptions() map[string][]string { + return c.subscriptions +} - // subscriptions holds all subscriptions with personalized handlers. - subscriptionHandlers []ccex.MessageDispatcher +// Connect performs the connection +func (c *Client) Connect() { + c.ws.Dial() } -func (c *Client) MarketHandler(handler ccex.OnMarketsHandler) { +// Reconnect attempts reconnection +func (c *Client) Reconnect() error { + return c.ws.CloseAndReconnect() +} + +// Connected returns a boolean representing the connection state +func (c *Client) Connected() bool { + return c.ws.Connected() +} + +// OnMarketHandler sets the handler for market messages +func (c *Client) OnMarketHandler(handler exchange.OnMarketsHandler) { c.OnMarkets = handler } -func (c *Client) OrderBookHandler(handler ccex.OnOrderBookHandler) { +// OnOrderBookHandler sets the handler for orderbook messages +func (c *Client) OnOrderBookHandler(handler exchange.OnOrderBookHandler) { c.OnOrderBook = handler } -func (c *Client) TradeHandler(handler ccex.OnTradeHandler) { +// OnTradesHandler sets the handler for trade messages +func (c *Client) OnTradesHandler(handler exchange.OnTradeHandler) { c.OnTrade = handler } -func (c *Client) TickerHandler(handler ccex.OnTickerHandler) { +// OnTickerHandler sets the handler for ticker messages +func (c *Client) OnTickerHandler(handler exchange.OnTickerHandler) { c.OnTicker = handler } // OnMessage is called by the underlying websocket client whenever it reads a message, similar to event-based actions. -func (c Client) OnMessage(message []byte) { - var v map[string]string +func (c Client) OnMessage(message []byte) error { + var v map[string]interface{} - err := json.Unmarshal(message, v) + err := json.Unmarshal(message, &v) if err != nil { - logger.Error(fmt.Errorf("")) + logger.Error(err.Error()) } - channel, ok := v["channel"] + msgType, ok := v["type"] if !ok { - c.logger.Error("Could not get message channel") - return + return fmt.Errorf("could not get message type") } - market, ok := v["market"] + if msgType == "error" { + code, ok := v["code"] + if !ok { + return fmt.Errorf("could not get message code") + } + + msg, ok := v["msg"] + if !ok { + return fmt.Errorf("could not get message") + } + + return fmt.Errorf("code: %v type: %v msg: %v", code, msgType, msg) + } + var ( + channel, market interface{} + ) + + channel, ok = v["channel"] if !ok { c.logger.Error("Could not get message channel") - return + return fmt.Errorf("could not get message channel") + } + + market, ok = v["market"] + if !ok { + c.logger.Error("Could not get message market") + return fmt.Errorf("could not get message market") } - if handler := c.GetHandlerFor(channel, market); handler != nil { - handler(v) - return + if msgType == "subscribed" || msgType == "unsubscribed" { + c.logger.Infof("Successfully %v to channel {%v} for market {%v}", msgType, channel, market) + return nil } switch channel { - case Markets: - if c.OnMarkets != nil { - var markets models.MarketMessage - err := json.Unmarshal(message, markets) - if err != nil { - logger.Error(fmt.Errorf("")) - } - c.OnMarkets(markets) + case Markets: + if c.OnMarkets != nil { + var markets models.MarketMessage + err = json.Unmarshal(message, &markets) + if err != nil { + return err } - break - case Trades: - if c.OnTrade != nil { - var trades models.TradeMessage - err := json.Unmarshal(message, trades) - if err != nil { - logger.Error(fmt.Errorf("")) - } - c.OnTrade(trades) + c.OnMarkets(markets) + } + break + case Trades: + if c.OnTrade != nil { + var trades models.TradeMessage + err = json.Unmarshal(message, &trades) + if err != nil { + return err } - break - case Orderbook: - if c.OnOrderBook != nil { - var orderbook models.OrderBookMessage - err := json.Unmarshal(message, orderbook) - if err != nil { - logger.Error(fmt.Errorf("")) - } - c.OnOrderBook(orderbook) + c.OnTrade(trades) + } + break + case Orderbook: + if c.OnOrderBook != nil { + var orderbook models.OrderBookMessage + err = json.Unmarshal(message, &orderbook) + if err != nil { + return err } - break - case Ticker: - if c.OnTicker != nil { - var ticker models.TickerMessage - err := json.Unmarshal(message, ticker) - if err != nil { - logger.Error(fmt.Errorf("")) - } - c.OnTicker(ticker) + c.OnOrderBook(orderbook) + } + break + case Ticker: + if c.OnTicker != nil { + var ticker models.TickerMessage + err = json.Unmarshal(message, &ticker) + if err != nil { + return err } - break - } - -} - -// GetHandlerFor fetches the personalized handler for a channel and market. -func (c Client) GetHandlerFor(channel string, market string) ccex.OnMessageHandler { - for _, dispatcher := range c.subscriptionHandlers { - if dispatcher.Channel == channel && market == dispatcher.Market { - return dispatcher.Handler + c.OnTicker(ticker) } + break } return nil } @@ -155,7 +189,7 @@ func (c Client) Authenticate() ([]byte, error) { if err != nil { return nil, err } - err = c.ws.WriteMessage(2, message) + err = c.ws.WriteMessage(1, message) if err != nil { return nil, err } @@ -178,62 +212,46 @@ func (c Client) Subscribe(channel string, market string) error { if err != nil { return err } - err = c.ws.WriteMessage(2, message) - if err != nil { - return err - } - - return nil -} - -// SubscribeWithHandler subscribes to a websocket channel. -func (c Client) SubscribeWithHandler(channel string, market string, handler ccex.OnMessageHandler) error { - - data := models.SubscribeMessage{ - BaseOperation: models.BaseOperation{ - Op: "subscribe", - }, - Channel: channel, - Market: market, - } - message, err := json.Marshal(data) - if err != nil { - return err - } - err = c.ws.WriteMessage(2, message) + err = c.ws.WriteMessage(1, message) if err != nil { return err } - msgDispatcher := ccex.MessageDispatcher{ - Channel: channel, - Market: market, - Handler: handler, - } - c.subscriptionHandlers = append(c.subscriptionHandlers, msgDispatcher) + c.subscriptions[channel] = append(c.subscriptions[channel], market) return nil } // NewClient returns a configured websocket client for FTX -func NewClient(config *config.Configuration) (*Client, error) { - ws, err := websocket.New(Url) +func NewClient(config *config.Configuration, marketsHandler exchange.OnMarketsHandler, + tickerHandler exchange.OnTickerHandler, tradesHandler exchange.OnTradeHandler, orderbookHandler exchange.OnOrderBookHandler) (*Client, error) { + clientLogger := logger.NewLogger() + ws, err := websocket.New(Url, clientLogger) if err != nil { return nil, err } client := &Client{ - config: config, - ws: ws, - subscriptionHandlers: []ccex.MessageDispatcher{}, + config: config, + ws: ws, + logger: clientLogger, + subscriptions: map[string][]string{}, + OnTicker: tickerHandler, + OnTrade: tradesHandler, + OnOrderBook: orderbookHandler, + OnMarkets: marketsHandler, } - if config.GetAuth() != nil { - ws.OnConnected = client.Authenticate + if config != nil { + if config.GetAuth() != nil { + ws.OnConnected = client.Authenticate + } } ws.OnMessage = client.OnMessage + ws.SetKeepAliveTimeout(15 * time.Second) + return client, nil } diff --git a/ftx/websocket/constants.go b/ftx/websocket/constants.go index 990094c..ebb22b2 100644 --- a/ftx/websocket/constants.go +++ b/ftx/websocket/constants.go @@ -2,21 +2,21 @@ package websocket const ( /* - General - */ + General + */ Url = "wss://ftx.com/ws/" /* - Public websocket channels + Public websocket channels */ - Ticker = "ticker" - Markets = "markets" - Trades = "trades" + Ticker = "ticker" + Markets = "markets" + Trades = "trades" Orderbook = "orderbook" /* - Private websocket channels + Private websocket channels */ - Fills = "fills" + Fills = "fills" Orders = "orders" ) diff --git a/ftx/websocket/models/requests.go b/ftx/websocket/models/requests.go index 2457bd4..30c92fb 100644 --- a/ftx/websocket/models/requests.go +++ b/ftx/websocket/models/requests.go @@ -1,9 +1,5 @@ package models -import ( - "github.com/murlokito/ccex" -) - // BaseOperation is the base of the message, all common attributes. type BaseOperation struct { Op string `json:"op"` @@ -25,6 +21,6 @@ type LoginMessage struct { // SubscribeMessage is used to request a subscription to a channel. type SubscribeMessage struct { BaseOperation - Channel ccex.Channel `json:"channel"` - Market ccex.Market `json:"market"` + Channel string `json:"channel"` + Market string `json:"market"` } diff --git a/ftx/websocket/models/responses.go b/ftx/websocket/models/responses.go index 4a609c6..e6d6e95 100644 --- a/ftx/websocket/models/responses.go +++ b/ftx/websocket/models/responses.go @@ -12,32 +12,32 @@ type ( // FutureData holds future data from the markets message FutureData struct { - Name string `json:"name"` - Underlying string `json:"underlying"` - Type string `json:"type"` - Expiry time.Time `json:"expiry"` - ExpiryDescription bool `json:"expiryDescription"` - Expired bool `json:"expired"` - Perpetual bool `json:"perpetual"` - PostOnly bool `json:"postOnly"` - ImfFactor float64 `json:"imfFactor"` - UnderlyingDescription bool `json:"underlyingDescription"` - Group string `json:"group"` - PositionLimitWeight float64 `json:"positionLimitWeight"` + Name string `json:"name"` + Underlying string `json:"underlying"` + Type string `json:"type"` + Expiry time.Time `json:"expiry"` + ExpiryDescription bool `json:"expiryDescription"` + Expired bool `json:"expired"` + Perpetual bool `json:"perpetual"` + PostOnly bool `json:"postOnly"` + ImfFactor float64 `json:"imfFactor"` + UnderlyingDescription bool `json:"underlyingDescription"` + Group string `json:"group"` + PositionLimitWeight float64 `json:"positionLimitWeight"` } // MarketData holds the data from the markets message MarketData struct { - Name string `json:"name"` - Enabled bool `json:"enabled"` + Name string `json:"name"` + Enabled bool `json:"enabled"` PriceIncrement float64 `json:"priceIncrement"` - SizeIncrement float64 `json:"sizeIncrement"` - Type string `json:"type"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - Underlying string `json:"underlying"` - Restricted bool `json:"restricted"` - FutureData `json:"future"` + SizeIncrement float64 `json:"sizeIncrement"` + Type string `json:"type"` + BaseCurrency string `json:"baseCurrency"` + QuoteCurrency string `json:"quoteCurrency"` + Underlying string `json:"underlying"` + Restricted bool `json:"restricted"` + FutureData `json:"future"` } // MarketMessage @@ -57,7 +57,7 @@ type ( // TradeMessage TradeMessage struct { BaseMessage - Data TradeData `json:"data"` + Data []TradeData `json:"data"` } // OrderBookData holds the data from the markets message @@ -72,10 +72,10 @@ type ( // TickerData holds the data from the markets message TickerData struct { - Bid float64 `json:"bid"` - Ask float64 `json:"ask"` - Last float64 `json:"last"` - Timestamp time.Time `json:"timestamp"` + Bid float64 `json:"bid"` + Ask float64 `json:"ask"` + Last float64 `json:"last"` + Timestamp float64 `json:"timestamp"` } // TickerMessage diff --git a/internal/util/util.go b/internal/util/util.go index 5e44e9d..b9567d7 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -23,6 +23,6 @@ func ProcessResponse(resp *http.Response, result interface{}) error { } // FormatUrl formats a url according to the passed format -func FormatUrl(baseUrl, format, endpoint string, params ...string) string{ +func FormatUrl(baseUrl, format, endpoint string, params ...string) string { return fmt.Sprintf("%s%s", baseUrl, fmt.Sprintf(format, endpoint, params)) } diff --git a/internal/websocket/client.go b/internal/websocket/client.go index 7b46748..7390baf 100644 --- a/internal/websocket/client.go +++ b/internal/websocket/client.go @@ -15,7 +15,7 @@ import ( ) // OnMessageHandler is a handler that is dispatched when the client reads a message from the websocket. -type OnMessageHandler func(message []byte) +type OnMessageHandler func(message []byte) error /* OnConnectedHandler is a handler that is dispatched when the client reads a message from the websocket. @@ -23,7 +23,6 @@ This can be the authentication message and/or something else. */ type OnConnectedHandler func() ([]byte, error) - // ErrNotConnected is returned when the application read/writes // a message and the connection is closed var ErrNotConnected = errors.New("websocket: not connected") @@ -69,31 +68,31 @@ type Client struct { logger log.Logger // mu is the mutex to prevent issues associated with goroutine concurrency. - mu sync.RWMutex + mu sync.RWMutex // reqHeader holds the http request header to be used during connection. reqHeader http.Header // httpResp holds the http response that is received from the connection - httpResp *http.Response + httpResp *http.Response // keepAliveTimeout is an interval for sending ping/pong messages disabled if 0. keepAliveTimeout time.Duration // url holds the url for the connection. - url string + url string // dialErr is used to hold a possible error caught when dialing the server. dialErr error // verbose is used to define if the client should log actions verbosely and/or log more actions. - verbose bool + verbose bool // connected is used to define if the client has an established connection. connected bool // connected is used to define if the client's connection is closed. - closed bool + closed bool } // CloseAndReconnect will try to reconnect. @@ -120,6 +119,14 @@ func (c *Client) GetBackoff() *backoff.Backoff { } } +// SetKeepAliveTimeout sets the interval for the keep alive message +func (c *Client) SetKeepAliveTimeout(interval time.Duration) { + c.mu.RLock() + defer c.mu.RUnlock() + + c.keepAliveTimeout = interval +} + // Connect performs the connection. This function should be executed by a goroutine. func (c *Client) Connect() { b := c.GetBackoff() @@ -154,13 +161,13 @@ func (c *Client) Connect() { if err == nil { if !c.GetVerbose() { - c.logger.Info("Connection was successfully established with %s", c.url) + c.logger.Info("Connection was successfully established with ", c.url) if c.OnConnected != nil { message, err := c.OnConnected() if err == nil { err = c.WriteMessage(1, message) - if !c.getVerbose() && err != nil { + if c.getVerbose() && err != nil { c.logger.Error(err.Error()) // In case connection ended for some reason. @@ -176,23 +183,35 @@ func (c *Client) Connect() { } if c.getKeepAliveTimeout() != 0 { + c.logger.Info("Keeping connection alive with timeout ", c.getKeepAliveTimeout()) c.keepAlive() } + + err = c.ReadMessages() + if err != nil { + c.logger.Error(err.Error()) + } + + if err == ErrNotConnected { + if err := c.CloseAndReconnect(); err != nil { + c.logger.Error(err.Error()) + } + return + } } return } - if !c.getVerbose() { + if c.getVerbose() { c.logger.Error(err.Error()) - c.logger.Info("Will try again in", nextReconnect, "seconds.") + c.logger.Info("Will try again in ", nextReconnect, " seconds.") } time.Sleep(nextReconnect) } } - // Connected returns the WebSocket connection state func (c *Client) Connected() bool { c.mu.RLock() @@ -251,6 +270,25 @@ func (c *Client) Close() error { return nil } +// ReadMessages reads messages while the connection is active +func (c *Client) ReadMessages() error { + for { + if c.Closed() { + return ErrNotConnected + } + + _, msg, err := c.ReadMessage() + if err != nil { + return err + } + + err = c.OnMessage(msg) + if err != nil { + c.logger.Error(err.Error()) + } + } +} + // ReadMessage is a helper method for reading a message from the underlying connection. // If the connection is closed ErrNotConnected is returned func (c *Client) ReadMessage() (messageType int, message []byte, err error) { @@ -326,24 +364,8 @@ func (c *Client) ReadJSON(v interface{}) (err error) { return err } -// setUrl sets the url for the underlying connection. -func (c *Client) setUrl(url string) { - c.mu.Lock() - defer c.mu.Unlock() - - c.url = url -} - -// setReqHeader sets the http request header for the underlying connection. -func (c *Client) setReqHeader(reqHeader http.Header) { - c.mu.Lock() - defer c.mu.Unlock() - - c.reqHeader = reqHeader -} - // validateUrl validates passed rawUrl. -func (c *Client) validateUrl(rawUrl string) (string, error) { +func validateUrl(rawUrl string) (string, error) { if rawUrl == "" { return "", errors.New("dial: url cannot be empty") } @@ -427,16 +449,9 @@ func (c *Client) getHandshakeTimeout() time.Duration { // The URL url specifies the host and request URI. Use requestHeader to specify // the origin (Origin), sub-protocols (Sec-WebSocket-Protocol) and cookies // (Cookie). -func (c *Client) Dial(url string, reqHeader http.Header) { - urlStr, err := c.validateUrl(url) - - if err != nil { - c.logger.Fatal("Dial: %v", err) - } +func (c *Client) Dial() { // Config - c.setUrl(urlStr) - c.setReqHeader(reqHeader) c.setDefaultRecIntvlMin() c.setDefaultRecIntvlMax() c.setDefaultRecIntvlFactor() @@ -500,6 +515,10 @@ func (c *Client) keepAlive() { defer ticker.Stop() for { + if c.getVerbose() { + c.logger.Info("Writing ping message") + } + if err := c.writeControlPingMessage(); err != nil { c.logger.Error(err.Error()) } @@ -518,12 +537,17 @@ func (c *Client) keepAlive() { } // New returns a new configured websocket client for the passed url. -func New(url string) (*Client, error) { - client := &Client{ - url: url, +func New(url string, logger log.Logger) (*Client, error) { + + validatedUrl, err := validateUrl(url) + if err != nil { + return nil, err } - client.Connect() + client := &Client{ + url: validatedUrl, + logger: logger, + } return client, nil } diff --git a/orders.go b/orders.go deleted file mode 100644 index 8c2e2eb..0000000 --- a/orders.go +++ /dev/null @@ -1,8 +0,0 @@ -package ccex - -type ( - // Orders specifies functionality for the orders API - Orders interface { - - } -) \ No newline at end of file diff --git a/wallet.go b/wallet.go deleted file mode 100644 index 6950f34..0000000 --- a/wallet.go +++ /dev/null @@ -1,8 +0,0 @@ -package ccex - -type ( - // Wallet specifies functionality for the wallet API - Wallet interface { - - } -)